[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: MHSanaei\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: mhsanaei\ncustom: https://nowpayments.io/donation/hsanaei\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "name: Bug report\ndescription: Create a report to help us improve\ntitle: \"Bug report\"\nlabels: [\"bug\"]\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for reporting a bug! Please fill out the following information.\n\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of what the bug is.\n      placeholder: My problem is...\n    validations:\n      required: true\n\n  - type: textarea\n    id: how-repeat-problem\n    attributes:\n      label: How to repeat the problem?\n      description: Sequence of actions that allow you to reproduce the bug\n      placeholder: |\n        1. Open `Inbounds` page\n        2. ...\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected-action\n    attributes:\n      label: Expected action\n      description: What's going to happen\n      placeholder: Must be...\n    validations:\n      required: false\n\n  - type: textarea\n    id: received-action\n    attributes:\n      label: Received action\n      description: What's really happening\n      placeholder: It's actually happening...\n    validations:\n      required: false\n\n  - type: input\n    id: xui-version\n    attributes:\n      label: 3x-ui Version\n      description: Which version of 3x-ui are you using?\n      placeholder: 2.X.X\n    validations:\n      required: true\n\n  - type: input\n    id: xray-version\n    attributes:\n      label: Xray-core Version\n      description: Which version of Xray-core are you using?\n      placeholder: 2.X.X\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      description: Please check all the checkboxes\n      options:\n        - label: This bug report is written entirely in English.\n          required: true\n        - label: This bug report is new and no one has reported it before me.\n          required: true"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "content": "name: Feature request\r\ndescription: Suggest an idea for this project\r\ntitle: \"Feature request\"\r\nlabels: [\"enhancement\"]\r\n\r\nbody:\r\n  - type: textarea\r\n    id: is-related-problem\r\n    attributes:\r\n      label: Is your feature request related to a problem?\r\n      description: A clear and concise description of what the problem is.\r\n      placeholder: I'm always frustrated when...\r\n    validations:\r\n      required: true\r\n\r\n  - type: textarea\r\n    id: solution\r\n    attributes:\r\n      label: Describe the solution you'd like\r\n      description: A clear and concise description of what you want to happen.\r\n    validations:\r\n      required: true\r\n\r\n  - type: textarea\r\n    id: alternatives\r\n    attributes:\r\n      label: Describe alternatives you've considered\r\n      description: A clear and concise description of any alternative solutions or features you've considered.\r\n    validations:\r\n      required: false\r\n\r\n  - type: checkboxes\r\n    id: checklist\r\n    attributes:\r\n      label: Checklist\r\n      description: Please check all the checkboxes\r\n      options:\r\n        - label: This feature report is written entirely in English.\r\n          required: true"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.yaml",
    "content": "name: Question\ndescription: Describe this issue template's purpose here.\ntitle: \"Question\"\nlabels: [\"question\"]\n\nbody:\n  - type: textarea\n    id: question\n    attributes:\n      label: Question\n      placeholder: I have a question, ..., how can I solve it?\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      description: Please check all the checkboxes\n      options:\n        - label: This question is written entirely in English.\n          required: true\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# 3X-UI Development Guide\n\n## Project Overview\n3X-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.\n\n## Architecture\n\n### Core Components\n- **main.go**: Entry point that initializes database, web server, and subscription server. Handles graceful shutdown via SIGHUP/SIGTERM signals\n- **web/**: Primary web server with Gin router, HTML templates, and static assets embedded via `//go:embed`\n- **xray/**: Xray-core process management and API communication for traffic monitoring\n- **database/**: GORM-based SQLite database with models in `database/model/`\n- **sub/**: Subscription server running alongside main web server (separate port)\n- **web/service/**: Business logic layer containing InboundService, SettingService, TgBot, etc.\n- **web/controller/**: HTTP handlers using Gin context (`*gin.Context`)\n- **web/job/**: Cron-based background jobs for traffic monitoring, CPU checks, LDAP sync\n\n### Key Architectural Patterns\n1. **Embedded Resources**: All web assets (HTML, CSS, JS, translations) are embedded at compile time using `embed.FS`:\n   - `web/assets` → `assetsFS`\n   - `web/html` → `htmlFS`\n   - `web/translation` → `i18nFS`\n\n2. **Dual Server Design**: Main web panel + subscription server run concurrently, managed by `web/global` package\n\n3. **Xray Integration**: Panel generates `config.json` for Xray binary, communicates via gRPC API for real-time traffic stats\n\n4. **Signal-Based Restart**: SIGHUP triggers graceful restart. **Critical**: Always call `service.StopBot()` before restart to prevent Telegram bot 409 conflicts\n\n5. **Database Seeders**: Uses `HistoryOfSeeders` model to track one-time migrations (e.g., password bcrypt migration)\n\n## Development Workflows\n\n### Building & Running\n```bash\n# Build (creates bin/3x-ui.exe)\ngo run tasks.json → \"go: build\" task\n\n# Run with debug logging\nXUI_DEBUG=true go run ./main.go\n# Or use task: \"go: run\"\n\n# Test\ngo test ./...\n```\n\n### Command-Line Operations\nThe main.go accepts flags for admin tasks:\n- `-reset` - Reset all panel settings to defaults\n- `-show` - Display current settings (port, paths)\n- Use these by running the binary directly, not via web interface\n\n### Database Management\n- DB path: Configured via `config.GetDBPath()`, typically `/etc/x-ui/x-ui.db`\n- Models: Located in `database/model/model.go` - Auto-migrated on startup\n- Seeders: Use `HistoryOfSeeders` to prevent re-running migrations\n- Default credentials: admin/admin (hashed with bcrypt)\n\n### Telegram Bot Development\n- Bot instance in `web/service/tgbot.go` (3700+ lines)\n- Uses `telego` library with long polling\n- **Critical Pattern**: Must call `service.StopBot()` before any server restart to prevent 409 bot conflicts\n- Bot handlers use `telegohandler.BotHandler` for routing\n- i18n via embedded `i18nFS` passed to bot startup\n\n## Code Conventions\n\n### Service Layer Pattern\nServices inject dependencies (like xray.XrayAPI) and operate on GORM models:\n```go\ntype InboundService struct {\n    xrayApi xray.XrayAPI\n}\n\nfunc (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {\n    // Business logic here\n}\n```\n\n### Controller Pattern\nControllers use Gin context and inherit from BaseController:\n```go\nfunc (a *InboundController) getInbounds(c *gin.Context) {\n    // Use I18nWeb(c, \"key\") for translations\n    // Check auth via checkLogin middleware\n}\n```\n\n### Configuration Management\n- Environment vars: `XUI_DEBUG`, `XUI_LOG_LEVEL`, `XUI_MAIN_FOLDER`\n- Config embedded files: `config/version`, `config/name`\n- Use `config.GetLogLevel()`, `config.GetDBPath()` helpers\n\n### Internationalization\n- Translation files: `web/translation/translate.*.toml`\n- Access via `I18nWeb(c, \"pages.login.loginAgain\")` in controllers\n- Use `locale.I18nType` enum (Web, Api, etc.)\n\n## External Dependencies & Integration\n\n### Xray-core\n- Binary management: Download platform-specific binary (`xray-{os}-{arch}`) to bin folder\n- Config generation: Panel creates `config.json` dynamically from inbound/outbound settings\n- Process control: Start/stop via `xray/process.go`\n- gRPC API: Real-time stats via `xray/api.go` using `google.golang.org/grpc`\n\n### Critical External Paths\n- Xray binary: `{bin_folder}/xray-{os}-{arch}`\n- Xray config: `{bin_folder}/config.json`\n- GeoIP/GeoSite: `{bin_folder}/geoip.dat`, `geosite.dat`\n- Logs: `{log_folder}/3xipl.log`, `3xipl-banned.log`\n\n### Job Scheduling\nUses `robfig/cron/v3` for periodic tasks:\n- Traffic monitoring: `xray_traffic_job.go`\n- CPU alerts: `check_cpu_usage.go`\n- IP tracking: `check_client_ip_job.go`\n- LDAP sync: `ldap_sync_job.go`\n\nJobs registered in `web/web.go` during server initialization\n\n## Deployment & Scripts\n\n### Installation Script Pattern\nBoth `install.sh` and `x-ui.sh` follow these patterns:\n- Multi-distro support via `$release` variable (ubuntu, debian, centos, arch, etc.)\n- Port detection with `is_port_in_use()` using ss/netstat/lsof\n- Systemd service management with distro-specific unit files (`.service.debian`, `.service.arch`, `.service.rhel`)\n\n### Docker Build\nMulti-stage Dockerfile:\n1. **Builder**: CGO-enabled build, runs `DockerInit.sh` to download Xray binary\n2. **Final**: Alpine-based with fail2ban pre-configured\n\n### Key File Locations (Production)\n- Binary: `/usr/local/x-ui/`\n- Database: `/etc/x-ui/x-ui.db`\n- Logs: `/var/log/x-ui/`\n- Service: `/etc/systemd/system/x-ui.service.*`\n\n## Testing & Debugging\n- Set `XUI_DEBUG=true` for detailed logging\n- Check Xray process: `x-ui.sh` script provides menu for status/logs\n- Database inspection: Direct SQLite access to x-ui.db\n- Traffic debugging: Check `3xipl.log` for IP limit tracking\n- Telegram bot: Logs show bot initialization and command handling\n\n## Common Gotchas\n1. **Bot Restart**: Always stop Telegram bot before server restart to avoid 409 conflict\n2. **Embedded Assets**: Changes to HTML/CSS require recompilation (not hot-reload)\n3. **Password Migration**: Seeder system tracks bcrypt migration - check `HistoryOfSeeders` table\n4. **Port Binding**: Subscription server uses different port from main panel\n5. **Xray Binary**: Must match OS/arch exactly - managed by installer scripts\n6. **Session Management**: Uses `gin-contrib/sessions` with cookie store\n7. **IP Limitation**: Implements \"last IP wins\" - when client exceeds LimitIP, oldest connections are automatically disconnected via Xray API to allow newest IPs\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"github-actions\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/pull_request_template.yml",
    "content": "## What is the pull request?\r\n\r\n<!-- Briefly describe the changes introduced by this pull request -->\r\n\r\n## Which part of the application is affected by the change?\r\n\r\n- [ ] Frontend\r\n- [ ] Backend\r\n\r\n## Type of Changes\r\n\r\n- [ ] Bug fix\r\n- [ ] New feature\r\n- [ ] Refactoring\r\n- [ ] Other\r\n\r\n## Screenshots\r\n\r\n<!-- Add screenshots to illustrate the changes -->\r\n<!-- Remove this section if it is not applicable. -->"
  },
  {
    "path": ".github/workflows/cleanup_caches.yml",
    "content": "name: Cleanup Caches\non:\n  schedule:\n    - cron: '0 3 * * 0'   # every Sunday\n  workflow_dispatch:\n\njobs:\n  cleanup:\n    runs-on: ubuntu-latest\n    permissions:\n      actions: write\n    steps:\n      - name: Delete caches older than 3 days\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          CUTOFF_DATE=$(date -d \"3 days ago\" -Ins --utc | sed 's/+0000/Z/')\n          echo \"Deleting caches older than: $CUTOFF_DATE\"\n          \n          CACHE_IDS=$(gh api --paginate repos/${{ github.repository }}/actions/caches \\\n            --jq \".actions_caches[] | select(.last_accessed_at < \\\"$CUTOFF_DATE\\\") | .id\" 2>/dev/null)\n          \n          if [ -z \"$CACHE_IDS\" ]; then\n            echo \"No old caches found to delete.\"\n          else\n            echo \"$CACHE_IDS\" | while read CACHE_ID; do\n              echo \"Deleting cache: $CACHE_ID\"\n              gh api -X DELETE repos/${{ github.repository }}/actions/caches/$CACHE_ID\n            done\n            echo \"Old caches deleted successfully.\"\n          fi"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Release 3X-UI for Docker\n\npermissions:\n  contents: read\n  packages: write\n\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - \"v*.*.*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          submodules: true\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v6\n        with:\n          images: |\n            hsanaeii/3x-ui\n            ghcr.io/mhsanaei/3x-ui\n          tags: |\n            type=ref,event=branch\n            type=ref,event=tag\n            type=semver,pattern={{version}}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n        with:\n          install: true\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v4\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_TOKEN }}\n\n      - name: Login to GHCR\n        uses: docker/login-action@v4\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v7\n        with:\n          context: .\n          push: true\n          platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release 3X-UI\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - '**'\n    tags:\n      - \"v*.*.*\"\n    paths:\n      - '**.js'\n      - '**.css'\n      - '**.html'\n      - '**.sh'\n      - '**.go'\n      - 'go.mod'\n      - 'go.sum'\n      - 'x-ui.service.debian'\n      - 'x-ui.service.arch'\n      - 'x-ui.service.rhel'\n  pull_request:\n\njobs:\n  analyze:\n    name: Analyze Go code\n    permissions:\n      contents: read\n    runs-on: ubuntu-latest\n    timeout-minutes: 20\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          cache: true\n\n      - name: Check formatting\n        run: |\n          unformatted=$(gofmt -l .)\n          if [ -n \"$unformatted\" ]; then\n            echo \"These files are not gofmt-formatted:\"\n            echo \"$unformatted\"\n            exit 1\n          fi\n\n      - name: Run go vet\n        run: go vet ./...\n\n      - name: Run staticcheck\n        uses: dominikh/staticcheck-action@v1\n        with:\n          version: \"latest\"\n          install-go: false\n\n      - name: Run tests\n        run: go test -race -shuffle=on ./...\n\n  build:\n    needs: analyze\n    permissions:\n      contents: write\n    strategy:\n      matrix:\n        platform:\n          - amd64\n          - arm64\n          - armv7\n          - armv6\n          - 386\n          - armv5\n          - s390x\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n\n      - name: Build 3X-UI\n        run: |\n          export CGO_ENABLED=1\n          export GOOS=linux\n          export GOARCH=${{ matrix.platform }}\n          # Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series)\n          case \"${{ matrix.platform }}\" in\n            amd64) BOOTLIN_ARCH=\"x86-64\" ;;\n            arm64) BOOTLIN_ARCH=\"aarch64\" ;;\n            armv7) BOOTLIN_ARCH=\"armv7-eabihf\"; export GOARCH=arm GOARM=7 ;;\n            armv6) BOOTLIN_ARCH=\"armv6-eabihf\"; export GOARCH=arm GOARM=6 ;;\n            armv5) BOOTLIN_ARCH=\"armv5-eabi\"; export GOARCH=arm GOARM=5 ;;\n            386) BOOTLIN_ARCH=\"x86-i686\" ;;\n            s390x) BOOTLIN_ARCH=\"s390x-z13\" ;;\n          esac\n          echo \"Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})\"\n          TARBALL_BASE=\"https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/\"\n          TARBALL_URL=$(curl -fsSL \"$TARBALL_BASE\" | grep -oE \"${BOOTLIN_ARCH}--musl--stable-[^\\\"]+\\\\.tar\\\\.xz\" | sort -r | head -n1)\n          [ -z \"$TARBALL_URL\" ] && { echo \"Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH\" >&2; exit 1; }\n          echo \"Downloading: $TARBALL_URL\"\n          cd /tmp\n          curl -fL -sS -o \"$(basename \"$TARBALL_URL\")\" \"$TARBALL_BASE/$TARBALL_URL\"\n          tar -xf \"$(basename \"$TARBALL_URL\")\"\n          TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name \"${BOOTLIN_ARCH}--musl--stable-*\" | head -n1)\n          export PATH=\"$(realpath \"$TOOLCHAIN_DIR\")/bin:$PATH\"\n          export CC=$(realpath \"$(find \"$TOOLCHAIN_DIR/bin\" -name '*-gcc.br_real' -type f -executable | head -n1)\")\n          [ -z \"$CC\" ] && { echo \"No gcc.br_real found in $TOOLCHAIN_DIR/bin\" >&2; exit 1; }\n          cd -\n          go build -ldflags \"-w -s -linkmode external -extldflags '-static'\" -o xui-release -v main.go\n          file xui-release\n          ldd xui-release || echo \"Static binary confirmed\"\n          \n          mkdir x-ui\n          cp xui-release x-ui/\n          cp x-ui.service.debian x-ui/\n          cp x-ui.service.arch x-ui/\n          cp x-ui.service.rhel x-ui/\n          cp x-ui.sh x-ui/\n          mv x-ui/xui-release x-ui/x-ui\n          mkdir x-ui/bin\n          cd x-ui/bin\n          \n          # Download dependencies\n          Xray_URL=\"https://github.com/XTLS/Xray-core/releases/download/v26.2.6/\"\n          if [ \"${{ matrix.platform }}\" == \"amd64\" ]; then\n            wget -q ${Xray_URL}Xray-linux-64.zip\n            unzip Xray-linux-64.zip\n            rm -f Xray-linux-64.zip\n          elif [ \"${{ matrix.platform }}\" == \"arm64\" ]; then\n            wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip\n            unzip Xray-linux-arm64-v8a.zip\n            rm -f Xray-linux-arm64-v8a.zip\n          elif [ \"${{ matrix.platform }}\" == \"armv7\" ]; then\n            wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip\n            unzip Xray-linux-arm32-v7a.zip\n            rm -f Xray-linux-arm32-v7a.zip\n          elif [ \"${{ matrix.platform }}\" == \"armv6\" ]; then\n            wget -q ${Xray_URL}Xray-linux-arm32-v6.zip\n            unzip Xray-linux-arm32-v6.zip\n            rm -f Xray-linux-arm32-v6.zip\n          elif [ \"${{ matrix.platform }}\" == \"386\" ]; then\n            wget -q ${Xray_URL}Xray-linux-32.zip\n            unzip Xray-linux-32.zip\n            rm -f Xray-linux-32.zip\n          elif [ \"${{ matrix.platform }}\" == \"armv5\" ]; then\n            wget -q ${Xray_URL}Xray-linux-arm32-v5.zip\n            unzip Xray-linux-arm32-v5.zip\n            rm -f Xray-linux-arm32-v5.zip\n          elif [ \"${{ matrix.platform }}\" == \"s390x\" ]; then\n            wget -q ${Xray_URL}Xray-linux-s390x.zip\n            unzip Xray-linux-s390x.zip\n            rm -f Xray-linux-s390x.zip\n          fi\n          rm -f geoip.dat geosite.dat\n          wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat\n          wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat\n          wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat\n          wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat\n          wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat\n          wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat\n          mv xray xray-linux-${{ matrix.platform }}\n          cd ../..\n          \n      - name: Package\n        run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui\n\n      - name: Upload files to Artifacts\n        uses: actions/upload-artifact@v7\n        with:\n          name: x-ui-linux-${{ matrix.platform }}\n          path: ./x-ui-linux-${{ matrix.platform }}.tar.gz\n\n      - name: Upload files to GH release\n        uses: svenstaro/upload-release-action@v2\n        if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          tag: ${{ github.ref_name }}\n          file: x-ui-linux-${{ matrix.platform }}.tar.gz\n          asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz\n          overwrite: true\n          prerelease: true\n\n  # =================================\n  #  Windows Build\n  # =================================\n  build-windows:\n    name: Build for Windows\n    needs: analyze\n    permissions:\n      contents: write\n    strategy:\n      matrix:\n        platform:\n          - amd64\n    runs-on: windows-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n\n      - name: Install MSYS2\n        uses: msys2/setup-msys2@v2\n        with:\n          msystem: MINGW64\n          update: true\n          install: >-\n            mingw-w64-x86_64-gcc\n            mingw-w64-x86_64-sqlite3\n            mingw-w64-x86_64-pkg-config\n\n      - name: Build 3X-UI for Windows (CGO)\n        shell: msys2 {0}\n        run: |\n          export PATH=\"/c/hostedtoolcache/windows/go/$(ls /c/hostedtoolcache/windows/go | sort -V | tail -n1)/x64/bin:$PATH\"\n\n          export CGO_ENABLED=1\n          export GOOS=windows\n          export GOARCH=amd64\n          export CC=x86_64-w64-mingw32-gcc\n\n          which go\n          go version\n          gcc --version\n\n          go build -ldflags \"-w -s\" -o xui-release.exe -v main.go\n\n      - name: Copy and download resources\n        shell: pwsh\n        run: |\n          mkdir x-ui\n          Copy-Item xui-release.exe x-ui\\x-ui.exe\n          mkdir x-ui\\bin\n          cd x-ui\\bin\n          \n          # Download Xray for Windows\n          $Xray_URL = \"https://github.com/XTLS/Xray-core/releases/download/v26.2.6/\"\n          Invoke-WebRequest -Uri \"${Xray_URL}Xray-windows-64.zip\" -OutFile \"Xray-windows-64.zip\"\n          Expand-Archive -Path \"Xray-windows-64.zip\" -DestinationPath .\n          Remove-Item \"Xray-windows-64.zip\"\n          Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue\n          Invoke-WebRequest -Uri \"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat\" -OutFile \"geoip.dat\"\n          Invoke-WebRequest -Uri \"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat\" -OutFile \"geosite.dat\"\n          Invoke-WebRequest -Uri \"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat\" -OutFile \"geoip_IR.dat\"\n          Invoke-WebRequest -Uri \"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat\" -OutFile \"geosite_IR.dat\"\n          Invoke-WebRequest -Uri \"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat\" -OutFile \"geoip_RU.dat\"\n          Invoke-WebRequest -Uri \"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat\" -OutFile \"geosite_RU.dat\"\n          Rename-Item xray.exe xray-windows-amd64.exe\n          cd ..\n          Copy-Item -Path ..\\windows_files\\* -Destination . -Recurse\n          cd ..\n\n      - name: Package to Zip\n        shell: pwsh\n        run: |\n          Compress-Archive -Path .\\x-ui -DestinationPath \"x-ui-windows-amd64.zip\"\n\n      - name: Upload files to Artifacts\n        uses: actions/upload-artifact@v7\n        with:\n          name: x-ui-windows-amd64\n          path: ./x-ui-windows-amd64.zip\n\n      - name: Upload files to GH release\n        uses: svenstaro/upload-release-action@v2\n        if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          tag: ${{ github.ref_name }}\n          file: x-ui-windows-amd64.zip\n          asset_name: x-ui-windows-amd64.zip\n          overwrite: true\n          prerelease: true\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore editor and IDE settings\n.idea/\n.vscode/\n.cache/\n.sync*\n\n# Ignore log files\n*.log\n\n# Ignore temporary files\ntmp/\n*.tar.gz\n\n# Ignore build and distribution directories\nbackup/\nbin/\ndist/\nrelease/\nnode_modules/\n\n# Ignore compiled binaries\nmain\n\n# Ignore script and executable files\n/release.sh\n/x-ui\n\n# Ignore OS specific files\n.DS_Store\nThumbs.db\n\n# Ignore Go build files\n*.exe\nx-ui.db\n\n# Ignore Docker specific files\ndocker-compose.override.yml\n\n# Ignore .env (Environment Variables) file\n.env"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Local Development Setup\n\n- Create a directory named `x-ui` in the project root \n- Rename `.env.example` to `.env `\n- Run `main.go`"
  },
  {
    "path": "DockerEntrypoint.sh",
    "content": "#!/bin/sh\n\n# Start fail2ban\n[ $XUI_ENABLE_FAIL2BAN == \"true\" ] && fail2ban-client -x start\n\n# Run x-ui\nexec /app/x-ui\n"
  },
  {
    "path": "DockerInit.sh",
    "content": "#!/bin/sh\ncase $1 in\n    amd64)\n        ARCH=\"64\"\n        FNAME=\"amd64\"\n        ;;\n    i386)\n        ARCH=\"32\"\n        FNAME=\"i386\"\n        ;;\n    armv8 | arm64 | aarch64)\n        ARCH=\"arm64-v8a\"\n        FNAME=\"arm64\"\n        ;;\n    armv7 | arm | arm32)\n        ARCH=\"arm32-v7a\"\n        FNAME=\"arm32\"\n        ;;\n    armv6)\n        ARCH=\"arm32-v6\"\n        FNAME=\"armv6\"\n        ;;\n    *)\n        ARCH=\"64\"\n        FNAME=\"amd64\"\n        ;;\nesac\nmkdir -p build/bin\ncd build/bin\ncurl -sfLRO \"https://github.com/XTLS/Xray-core/releases/download/v26.2.6/Xray-linux-${ARCH}.zip\"\nunzip \"Xray-linux-${ARCH}.zip\"\nrm -f \"Xray-linux-${ARCH}.zip\" geoip.dat geosite.dat\nmv xray \"xray-linux-${FNAME}\"\ncurl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat\ncurl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat\ncurl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat\ncurl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat\ncurl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat\ncurl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat\ncd ../../"
  },
  {
    "path": "Dockerfile",
    "content": "# ========================================================\n# Stage: Builder\n# ========================================================\nFROM golang:1.26-alpine AS builder\nWORKDIR /app\nARG TARGETARCH\n\nRUN apk --no-cache --update add \\\n  build-base \\\n  gcc \\\n  curl \\\n  unzip\n\nCOPY . .\n\nENV CGO_ENABLED=1\nENV CGO_CFLAGS=\"-D_LARGEFILE64_SOURCE\"\nRUN go build -ldflags \"-w -s\" -o build/x-ui main.go\nRUN ./DockerInit.sh \"$TARGETARCH\"\n\n# ========================================================\n# Stage: Final Image of 3x-ui\n# ========================================================\nFROM alpine\nENV TZ=Asia/Tehran\nWORKDIR /app\n\nRUN apk add --no-cache --update \\\n  ca-certificates \\\n  tzdata \\\n  fail2ban \\\n  bash \\\n  curl \\\n  openssl\n\nCOPY --from=builder /app/build/ /app/\nCOPY --from=builder /app/DockerEntrypoint.sh /app/\nCOPY --from=builder /app/x-ui.sh /usr/bin/x-ui\n\n\n# Configure fail2ban\nRUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \\\n  && cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \\\n  && sed -i \"s/^\\[ssh\\]$/&\\nenabled = false/\" /etc/fail2ban/jail.local \\\n  && sed -i \"s/^\\[sshd\\]$/&\\nenabled = false/\" /etc/fail2ban/jail.local \\\n  && sed -i \"s/#allowipv6 = auto/allowipv6 = auto/g\" /etc/fail2ban/fail2ban.conf\n\nRUN chmod +x \\\n  /app/DockerEntrypoint.sh \\\n  /app/x-ui \\\n  /usr/bin/x-ui\n\nENV XUI_ENABLE_FAIL2BAN=\"true\"\nEXPOSE 2053\nVOLUME [ \"/etc/x-ui\" ]\nCMD [ \"./x-ui\" ]\nENTRYPOINT [ \"/app/DockerEntrypoint.sh\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.ar_EG.md",
    "content": "[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)\n\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"./media/3x-ui-dark.png\">\n    <img alt=\"3x-ui\" src=\"./media/3x-ui-light.png\">\n  </picture>\n</p>\n\n[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)\n[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)\n[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)\n[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)\n[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)\n[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)\n[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)\n\n**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.\n\n> [!IMPORTANT]\n> هذا المشروع مخصص للاستخدام الشخصي والاتصال فقط، يرجى عدم استخدامه لأغراض غير قانونية، يرجى عدم استخدامه في بيئة الإنتاج.\n\nكمشروع محسن من مشروع X-UI الأصلي، يوفر 3X-UI استقرارًا محسنًا ودعمًا أوسع للبروتوكولات وميزات إضافية.\n\n## البدء السريع\n\n```\nbash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)\n```\n\nللحصول على الوثائق الكاملة، يرجى زيارة [ويكي المشروع](https://github.com/MHSanaei/3x-ui/wiki).\n\n## شكر خاص إلى\n\n- [alireza0](https://github.com/alireza0/)\n\n## الاعتراف\n\n- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (الترخيص: **GPL-3.0**): _قواعد توجيه v2ray/xray و v2ray/xray-clients المحسنة مع النطاقات الإيرانية المدمجة وتركيز على الأمان وحظر الإعلانات._\n- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (الترخيص: **GPL-3.0**): _يحتوي هذا المستودع على قواعد توجيه V2Ray محدثة تلقائيًا بناءً على بيانات النطاقات والعناوين المحظورة في روسيا._\n\n## دعم المشروع\n\n**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:\n\n<a href=\"https://www.buymeacoffee.com/MHSanaei\" target=\"_blank\">\n<img src=\"./media/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 70px !important;width: 277px !important;\" >\n</a>\n</br>\n<a href=\"https://nowpayments.io/donation/hsanaei\" target=\"_blank\" rel=\"noreferrer noopener\">\n   <img src=\"./media/donation-button-black.svg\" alt=\"Crypto donation button by NOWPayments\">\n</a>\n\n## النجوم عبر الزمن\n\n[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) \n"
  },
  {
    "path": "README.es_ES.md",
    "content": "[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)\n\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"./media/3x-ui-dark.png\">\n    <img alt=\"3x-ui\" src=\"./media/3x-ui-light.png\">\n  </picture>\n</p>\n\n[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)\n[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)\n[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)\n[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)\n[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)\n[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)\n[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)\n\n**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.\n\n> [!IMPORTANT]\n> 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.\n\nComo una versión mejorada del proyecto X-UI original, 3X-UI proporciona mayor estabilidad, soporte más amplio de protocolos y características adicionales.\n\n## Inicio Rápido\n\n```\nbash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)\n```\n\nPara documentación completa, visita la [Wiki del proyecto](https://github.com/MHSanaei/3x-ui/wiki).\n\n## Un Agradecimiento Especial a\n\n- [alireza0](https://github.com/alireza0/)\n\n## Reconocimientos\n\n- [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._\n- [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._\n\n## Apoyar el Proyecto\n\n**Si este proyecto te es útil, puedes darle una**:star2:\n\n<a href=\"https://www.buymeacoffee.com/MHSanaei\" target=\"_blank\">\n<img src=\"./media/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 70px !important;width: 277px !important;\" >\n</a>\n\n</br>\n<a href=\"https://nowpayments.io/donation/hsanaei\" target=\"_blank\" rel=\"noreferrer noopener\">\n   <img src=\"./media/donation-button-black.svg\" alt=\"Crypto donation button by NOWPayments\">\n</a>\n\n## Estrellas a lo Largo del Tiempo\n\n[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) \n"
  },
  {
    "path": "README.fa_IR.md",
    "content": "[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)\n\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"./media/3x-ui-dark.png\">\n    <img alt=\"3x-ui\" src=\"./media/3x-ui-light.png\">\n  </picture>\n</p>\n\n[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)\n[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)\n[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)\n[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)\n[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)\n[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)\n[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)\n\n**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکل‌های مختلف VPN و پراکسی ارائه می‌دهد.\n\n> [!IMPORTANT]\n> این پروژه فقط برای استفاده شخصی و ارتباطات است، لطفاً از آن برای اهداف غیرقانونی استفاده نکنید، لطفاً از آن در محیط تولید استفاده نکنید.\n\nبه عنوان یک نسخه بهبود یافته از پروژه اصلی X-UI، 3X-UI پایداری بهتر، پشتیبانی گسترده‌تر از پروتکل‌ها و ویژگی‌های اضافی را ارائه می‌دهد.\n\n## شروع سریع\n\n```\nbash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)\n```\n\nبرای مستندات کامل، لطفاً به [ویکی پروژه](https://github.com/MHSanaei/3x-ui/wiki) مراجعه کنید.\n\n## تشکر ویژه از\n\n- [alireza0](https://github.com/alireza0/)\n\n## قدردانی\n\n- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**): _قوانین مسیریابی بهبود یافته v2ray/xray و v2ray/xray-clients با دامنه‌های ایرانی داخلی و تمرکز بر امنیت و مسدود کردن تبلیغات._\n- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**): _این مخزن شامل قوانین مسیریابی V2Ray به‌روزرسانی شده خودکار بر اساس داده‌های دامنه‌ها و آدرس‌های مسدود شده در روسیه است._\n\n## پشتیبانی از پروژه\n\n**اگر این پروژه برای شما مفید است، می‌توانید به آن یک**:star2: بدهید\n\n<a href=\"https://www.buymeacoffee.com/MHSanaei\" target=\"_blank\">\n<img src=\"./media/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 70px !important;width: 277px !important;\" >\n</a>\n\n</br>\n<a href=\"https://nowpayments.io/donation/hsanaei\" target=\"_blank\" rel=\"noreferrer noopener\">\n   <img src=\"./media/donation-button-black.svg\" alt=\"Crypto donation button by NOWPayments\">\n</a>\n\n## ستاره‌ها در طول زمان\n\n[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) \n"
  },
  {
    "path": "README.md",
    "content": "[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)\n\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"./media/3x-ui-dark.png\">\n    <img alt=\"3x-ui\" src=\"./media/3x-ui-light.png\">\n  </picture>\n</p>\n\n[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)\n[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)\n[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)\n[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)\n[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)\n[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)\n[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)\n\n**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.\n\n> [!IMPORTANT]\n> 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.\n\nAs an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features.\n\n## Quick Start\n\n```bash\nbash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)\n```\n\nFor full documentation, please visit the [project Wiki](https://github.com/MHSanaei/3x-ui/wiki).\n\n## A Special Thanks to\n\n- [alireza0](https://github.com/alireza0/)\n\n## Acknowledgment\n\n- [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._\n- [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._\n\n## Support project\n\n**If this project is helpful to you, you may wish to give it a**:star2:\n\n<a href=\"https://www.buymeacoffee.com/MHSanaei\" target=\"_blank\">\n<img src=\"./media/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 70px !important;width: 277px !important;\" >\n</a>\n\n</br>\n<a href=\"https://nowpayments.io/donation/hsanaei\" target=\"_blank\" rel=\"noreferrer noopener\">\n   <img src=\"./media/donation-button-black.svg\" alt=\"Crypto donation button by NOWPayments\">\n</a>\n\n## Stargazers over Time\n\n[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)\n"
  },
  {
    "path": "README.ru_RU.md",
    "content": "[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)\n\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"./media/3x-ui-dark.png\">\n    <img alt=\"3x-ui\" src=\"./media/3x-ui-light.png\">\n  </picture>\n</p>\n\n[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)\n[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)\n[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)\n[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)\n[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)\n[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)\n[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)\n\n**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.\n\n> [!IMPORTANT]\n> Этот проект предназначен только для личного использования, пожалуйста, не используйте его в незаконных целях и в производственной среде.\n\nКак улучшенная версия оригинального проекта X-UI, 3X-UI обеспечивает повышенную стабильность, более широкую поддержку протоколов и дополнительные функции.\n\n## Быстрый старт\n\n```\nbash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)\n```\n\nПолную документацию смотрите в [вики проекта](https://github.com/MHSanaei/3x-ui/wiki).\n\n## Особая благодарность\n\n- [alireza0](https://github.com/alireza0/)\n\n## Благодарности\n\n- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Лицензия: **GPL-3.0**): _Улучшенные правила маршрутизации для v2ray/xray и v2ray/xray-clients со встроенными иранскими доменами и фокусом на безопасность и блокировку рекламы._\n- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Лицензия: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._\n\n## Поддержка проекта\n\n**Если этот проект полезен для вас, вы можете поставить ему**:star2:\n\n<a href=\"https://www.buymeacoffee.com/MHSanaei\" target=\"_blank\">\n<img src=\"./media/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 70px !important;width: 277px !important;\" >\n</a>\n\n</br>\n<a href=\"https://nowpayments.io/donation/hsanaei\" target=\"_blank\" rel=\"noreferrer noopener\">\n   <img src=\"./media/donation-button-black.svg\" alt=\"Crypto donation button by NOWPayments\">\n</a>\n\n## Звезды с течением времени\n\n[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) \n"
  },
  {
    "path": "README.zh_CN.md",
    "content": "[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)\n\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"./media/3x-ui-dark.png\">\n    <img alt=\"3x-ui\" src=\"./media/3x-ui-light.png\">\n  </picture>\n</p>\n\n[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)\n[![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions)\n[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)\n[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest)\n[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)\n[![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)\n[![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)\n\n**3X-UI** — 一个基于网页的高级开源控制面板，专为管理 Xray-core 服务器而设计。它提供了用户友好的界面，用于配置和监控各种 VPN 和代理协议。\n\n> [!IMPORTANT]\n> 本项目仅用于个人使用和通信，请勿将其用于非法目的，请勿在生产环境中使用。\n\n作为原始 X-UI 项目的增强版本，3X-UI 提供了更好的稳定性、更广泛的协议支持和额外的功能。\n\n## 快速开始\n\n```\nbash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)\n```\n\n完整文档请参阅 [项目Wiki](https://github.com/MHSanaei/3x-ui/wiki)。\n\n## 特别感谢\n\n- [alireza0](https://github.com/alireza0/)\n\n## 致谢\n\n- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (许可证: **GPL-3.0**): _增强的 v2ray/xray 和 v2ray/xray-clients 路由规则，内置伊朗域名，专注于安全性和广告拦截。_\n- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (许可证: **GPL-3.0**): _此仓库包含基于俄罗斯被阻止域名和地址数据自动更新的 V2Ray 路由规则。_\n\n## 支持项目\n\n**如果这个项目对您有帮助，您可以给它一个**:star2:\n\n<a href=\"https://www.buymeacoffee.com/MHSanaei\" target=\"_blank\">\n<img src=\"./media/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 70px !important;width: 277px !important;\" >\n</a>\n\n</br>\n<a href=\"https://nowpayments.io/donation/hsanaei\" target=\"_blank\" rel=\"noreferrer noopener\">\n   <img src=\"./media/donation-button-black.svg\" alt=\"Crypto donation button by NOWPayments\">\n</a>\n\n## 随时间变化的星标数\n\n[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) \n"
  },
  {
    "path": "config/config.go",
    "content": "// Package config provides configuration management utilities for the 3x-ui panel,\n// including version information, logging levels, database paths, and environment variable handling.\npackage config\n\nimport (\n\t_ \"embed\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n)\n\n//go:embed version\nvar version string\n\n//go:embed name\nvar name string\n\n// LogLevel represents the logging level for the application.\ntype LogLevel string\n\n// Logging level constants\nconst (\n\tDebug   LogLevel = \"debug\"\n\tInfo    LogLevel = \"info\"\n\tNotice  LogLevel = \"notice\"\n\tWarning LogLevel = \"warning\"\n\tError   LogLevel = \"error\"\n)\n\n// GetVersion returns the version string of the 3x-ui application.\nfunc GetVersion() string {\n\treturn strings.TrimSpace(version)\n}\n\n// GetName returns the name of the 3x-ui application.\nfunc GetName() string {\n\treturn strings.TrimSpace(name)\n}\n\n// GetLogLevel returns the current logging level based on environment variables or defaults to Info.\nfunc GetLogLevel() LogLevel {\n\tif IsDebug() {\n\t\treturn Debug\n\t}\n\tlogLevel := os.Getenv(\"XUI_LOG_LEVEL\")\n\tif logLevel == \"\" {\n\t\treturn Info\n\t}\n\treturn LogLevel(logLevel)\n}\n\n// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.\nfunc IsDebug() bool {\n\treturn os.Getenv(\"XUI_DEBUG\") == \"true\"\n}\n\n// GetBinFolderPath returns the path to the binary folder, defaulting to \"bin\" if not set via XUI_BIN_FOLDER.\nfunc GetBinFolderPath() string {\n\tbinFolderPath := os.Getenv(\"XUI_BIN_FOLDER\")\n\tif binFolderPath == \"\" {\n\t\tbinFolderPath = \"bin\"\n\t}\n\treturn binFolderPath\n}\n\nfunc getBaseDir() string {\n\texePath, err := os.Executable()\n\tif err != nil {\n\t\treturn \".\"\n\t}\n\texeDir := filepath.Dir(exePath)\n\texeDirLower := strings.ToLower(filepath.ToSlash(exeDir))\n\tif strings.Contains(exeDirLower, \"/appdata/local/temp/\") || strings.Contains(exeDirLower, \"/go-build\") {\n\t\twd, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn \".\"\n\t\t}\n\t\treturn wd\n\t}\n\treturn exeDir\n}\n\n// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.\nfunc GetDBFolderPath() string {\n\tdbFolderPath := os.Getenv(\"XUI_DB_FOLDER\")\n\tif dbFolderPath != \"\" {\n\t\treturn dbFolderPath\n\t}\n\tif runtime.GOOS == \"windows\" {\n\t\treturn getBaseDir()\n\t}\n\treturn \"/etc/x-ui\"\n}\n\n// GetDBPath returns the full path to the database file.\nfunc GetDBPath() string {\n\treturn fmt.Sprintf(\"%s/%s.db\", GetDBFolderPath(), GetName())\n}\n\n// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.\nfunc GetLogFolder() string {\n\tlogFolderPath := os.Getenv(\"XUI_LOG_FOLDER\")\n\tif logFolderPath != \"\" {\n\t\treturn logFolderPath\n\t}\n\tif runtime.GOOS == \"windows\" {\n\t\treturn filepath.Join(\".\", \"log\")\n\t}\n\treturn \"/var/log/x-ui\"\n}\n\nfunc copyFile(src, dst string) error {\n\tin, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer in.Close()\n\n\tout, err := os.Create(dst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\t_, err = io.Copy(out, in)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn out.Sync()\n}\n\nfunc init() {\n\tif runtime.GOOS != \"windows\" {\n\t\treturn\n\t}\n\tif os.Getenv(\"XUI_DB_FOLDER\") != \"\" {\n\t\treturn\n\t}\n\toldDBFolder := \"/etc/x-ui\"\n\toldDBPath := fmt.Sprintf(\"%s/%s.db\", oldDBFolder, GetName())\n\tnewDBFolder := GetDBFolderPath()\n\tnewDBPath := fmt.Sprintf(\"%s/%s.db\", newDBFolder, GetName())\n\t_, err := os.Stat(newDBPath)\n\tif err == nil {\n\t\treturn // new exists\n\t}\n\t_, err = os.Stat(oldDBPath)\n\tif os.IsNotExist(err) {\n\t\treturn // old does not exist\n\t}\n\t_ = copyFile(oldDBPath, newDBPath) // ignore error\n}\n"
  },
  {
    "path": "config/name",
    "content": "x-ui"
  },
  {
    "path": "config/version",
    "content": "2.8.11"
  },
  {
    "path": "database/db.go",
    "content": "// Package database provides database initialization, migration, and management utilities\n// for the 3x-ui panel using GORM with SQLite.\npackage database\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/crypto\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n)\n\nvar db *gorm.DB\n\nconst (\n\tdefaultUsername = \"admin\"\n\tdefaultPassword = \"admin\"\n)\n\nfunc initModels() error {\n\tmodels := []any{\n\t\t&model.User{},\n\t\t&model.Inbound{},\n\t\t&model.OutboundTraffics{},\n\t\t&model.Setting{},\n\t\t&model.InboundClientIps{},\n\t\t&xray.ClientTraffic{},\n\t\t&model.HistoryOfSeeders{},\n\t}\n\tfor _, model := range models {\n\t\tif err := db.AutoMigrate(model); err != nil {\n\t\t\tlog.Printf(\"Error auto migrating model: %v\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// initUser creates a default admin user if the users table is empty.\nfunc initUser() error {\n\tempty, err := isTableEmpty(\"users\")\n\tif err != nil {\n\t\tlog.Printf(\"Error checking if users table is empty: %v\", err)\n\t\treturn err\n\t}\n\tif empty {\n\t\thashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error hashing default password: %v\", err)\n\t\t\treturn err\n\t\t}\n\n\t\tuser := &model.User{\n\t\t\tUsername: defaultUsername,\n\t\t\tPassword: hashedPassword,\n\t\t}\n\t\treturn db.Create(user).Error\n\t}\n\treturn nil\n}\n\n// runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.\nfunc runSeeders(isUsersEmpty bool) error {\n\tempty, err := isTableEmpty(\"history_of_seeders\")\n\tif err != nil {\n\t\tlog.Printf(\"Error checking if users table is empty: %v\", err)\n\t\treturn err\n\t}\n\n\tif empty && isUsersEmpty {\n\t\thashSeeder := &model.HistoryOfSeeders{\n\t\t\tSeederName: \"UserPasswordHash\",\n\t\t}\n\t\treturn db.Create(hashSeeder).Error\n\t} else {\n\t\tvar seedersHistory []string\n\t\tdb.Model(&model.HistoryOfSeeders{}).Pluck(\"seeder_name\", &seedersHistory)\n\n\t\tif !slices.Contains(seedersHistory, \"UserPasswordHash\") && !isUsersEmpty {\n\t\t\tvar users []model.User\n\t\t\tdb.Find(&users)\n\n\t\t\tfor _, user := range users {\n\t\t\t\thashedPassword, err := crypto.HashPasswordAsBcrypt(user.Password)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error hashing password for user '%s': %v\", user.Username, err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdb.Model(&user).Update(\"password\", hashedPassword)\n\t\t\t}\n\n\t\t\thashSeeder := &model.HistoryOfSeeders{\n\t\t\t\tSeederName: \"UserPasswordHash\",\n\t\t\t}\n\t\t\treturn db.Create(hashSeeder).Error\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// isTableEmpty returns true if the named table contains zero rows.\nfunc isTableEmpty(tableName string) (bool, error) {\n\tvar count int64\n\terr := db.Table(tableName).Count(&count).Error\n\treturn count == 0, err\n}\n\n// InitDB sets up the database connection, migrates models, and runs seeders.\nfunc InitDB(dbPath string) error {\n\tdir := path.Dir(dbPath)\n\terr := os.MkdirAll(dir, fs.ModePerm)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar gormLogger logger.Interface\n\n\tif config.IsDebug() {\n\t\tgormLogger = logger.Default\n\t} else {\n\t\tgormLogger = logger.Discard\n\t}\n\n\tc := &gorm.Config{\n\t\tLogger: gormLogger,\n\t}\n\tdb, err = gorm.Open(sqlite.Open(dbPath), c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := initModels(); err != nil {\n\t\treturn err\n\t}\n\n\tisUsersEmpty, err := isTableEmpty(\"users\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := initUser(); err != nil {\n\t\treturn err\n\t}\n\treturn runSeeders(isUsersEmpty)\n}\n\n// CloseDB closes the database connection if it exists.\nfunc CloseDB() error {\n\tif db != nil {\n\t\tsqlDB, err := db.DB()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn sqlDB.Close()\n\t}\n\treturn nil\n}\n\n// GetDB returns the global GORM database instance.\nfunc GetDB() *gorm.DB {\n\treturn db\n}\n\n// IsNotFound checks if the given error is a GORM record not found error.\nfunc IsNotFound(err error) bool {\n\treturn err == gorm.ErrRecordNotFound\n}\n\n// IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature.\nfunc IsSQLiteDB(file io.ReaderAt) (bool, error) {\n\tsignature := []byte(\"SQLite format 3\\x00\")\n\tbuf := make([]byte, len(signature))\n\t_, err := file.ReadAt(buf, 0)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn bytes.Equal(buf, signature), nil\n}\n\n// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.\nfunc Checkpoint() error {\n\t// Update WAL\n\terr := db.Exec(\"PRAGMA wal_checkpoint;\").Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection\n// and runs a PRAGMA integrity_check to ensure the file is structurally sound.\n// It does not mutate global state or run migrations.\nfunc ValidateSQLiteDB(dbPath string) error {\n\tif _, err := os.Stat(dbPath); err != nil { // file must exist\n\t\treturn err\n\t}\n\tgdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})\n\tif err != nil {\n\t\treturn err\n\t}\n\tsqlDB, err := gdb.DB()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sqlDB.Close()\n\tvar res string\n\tif err := gdb.Raw(\"PRAGMA integrity_check;\").Scan(&res).Error; err != nil {\n\t\treturn err\n\t}\n\tif res != \"ok\" {\n\t\treturn errors.New(\"sqlite integrity check failed: \" + res)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "database/model/model.go",
    "content": "// Package model defines the database models and data structures used by the 3x-ui panel.\npackage model\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/util/json_util\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n)\n\n// Protocol represents the protocol type for Xray inbounds.\ntype Protocol string\n\n// Protocol constants for different Xray inbound protocols\nconst (\n\tVMESS       Protocol = \"vmess\"\n\tVLESS       Protocol = \"vless\"\n\tTunnel      Protocol = \"tunnel\"\n\tHTTP        Protocol = \"http\"\n\tTrojan      Protocol = \"trojan\"\n\tShadowsocks Protocol = \"shadowsocks\"\n\tMixed       Protocol = \"mixed\"\n\tWireGuard   Protocol = \"wireguard\"\n)\n\n// User represents a user account in the 3x-ui panel.\ntype User struct {\n\tId       int    `json:\"id\" gorm:\"primaryKey;autoIncrement\"`\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\n// Inbound represents an Xray inbound configuration with traffic statistics and settings.\ntype Inbound struct {\n\tId                   int                  `json:\"id\" form:\"id\" gorm:\"primaryKey;autoIncrement\"`                                                    // Unique identifier\n\tUserId               int                  `json:\"-\"`                                                                                               // Associated user ID\n\tUp                   int64                `json:\"up\" form:\"up\"`                                                                                    // Upload traffic in bytes\n\tDown                 int64                `json:\"down\" form:\"down\"`                                                                                // Download traffic in bytes\n\tTotal                int64                `json:\"total\" form:\"total\"`                                                                              // Total traffic limit in bytes\n\tAllTime              int64                `json:\"allTime\" form:\"allTime\" gorm:\"default:0\"`                                                         // All-time traffic usage\n\tRemark               string               `json:\"remark\" form:\"remark\"`                                                                            // Human-readable remark\n\tEnable               bool                 `json:\"enable\" form:\"enable\" gorm:\"index:idx_enable_traffic_reset,priority:1\"`                           // Whether the inbound is enabled\n\tExpiryTime           int64                `json:\"expiryTime\" form:\"expiryTime\"`                                                                    // Expiration timestamp\n\tTrafficReset         string               `json:\"trafficReset\" form:\"trafficReset\" gorm:\"default:never;index:idx_enable_traffic_reset,priority:2\"` // Traffic reset schedule\n\tLastTrafficResetTime int64                `json:\"lastTrafficResetTime\" form:\"lastTrafficResetTime\" gorm:\"default:0\"`                               // Last traffic reset timestamp\n\tClientStats          []xray.ClientTraffic `gorm:\"foreignKey:InboundId;references:Id\" json:\"clientStats\" form:\"clientStats\"`                        // Client traffic statistics\n\n\t// Xray configuration fields\n\tListen         string   `json:\"listen\" form:\"listen\"`\n\tPort           int      `json:\"port\" form:\"port\"`\n\tProtocol       Protocol `json:\"protocol\" form:\"protocol\"`\n\tSettings       string   `json:\"settings\" form:\"settings\"`\n\tStreamSettings string   `json:\"streamSettings\" form:\"streamSettings\"`\n\tTag            string   `json:\"tag\" form:\"tag\" gorm:\"unique\"`\n\tSniffing       string   `json:\"sniffing\" form:\"sniffing\"`\n}\n\n// OutboundTraffics tracks traffic statistics for Xray outbound connections.\ntype OutboundTraffics struct {\n\tId    int    `json:\"id\" form:\"id\" gorm:\"primaryKey;autoIncrement\"`\n\tTag   string `json:\"tag\" form:\"tag\" gorm:\"unique\"`\n\tUp    int64  `json:\"up\" form:\"up\" gorm:\"default:0\"`\n\tDown  int64  `json:\"down\" form:\"down\" gorm:\"default:0\"`\n\tTotal int64  `json:\"total\" form:\"total\" gorm:\"default:0\"`\n}\n\n// InboundClientIps stores IP addresses associated with inbound clients for access control.\ntype InboundClientIps struct {\n\tId          int    `json:\"id\" gorm:\"primaryKey;autoIncrement\"`\n\tClientEmail string `json:\"clientEmail\" form:\"clientEmail\" gorm:\"unique\"`\n\tIps         string `json:\"ips\" form:\"ips\"`\n}\n\n// HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.\ntype HistoryOfSeeders struct {\n\tId         int    `json:\"id\" gorm:\"primaryKey;autoIncrement\"`\n\tSeederName string `json:\"seederName\"`\n}\n\n// GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.\nfunc (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {\n\tlisten := i.Listen\n\t// Default to 0.0.0.0 (all interfaces) when listen is empty\n\t// This ensures proper dual-stack IPv4/IPv6 binding in systems where bindv6only=0\n\tif listen == \"\" {\n\t\tlisten = \"0.0.0.0\"\n\t}\n\tlisten = fmt.Sprintf(\"\\\"%v\\\"\", listen)\n\treturn &xray.InboundConfig{\n\t\tListen:         json_util.RawMessage(listen),\n\t\tPort:           i.Port,\n\t\tProtocol:       string(i.Protocol),\n\t\tSettings:       json_util.RawMessage(i.Settings),\n\t\tStreamSettings: json_util.RawMessage(i.StreamSettings),\n\t\tTag:            i.Tag,\n\t\tSniffing:       json_util.RawMessage(i.Sniffing),\n\t}\n}\n\n// Setting stores key-value configuration settings for the 3x-ui panel.\ntype Setting struct {\n\tId    int    `json:\"id\" form:\"id\" gorm:\"primaryKey;autoIncrement\"`\n\tKey   string `json:\"key\" form:\"key\"`\n\tValue string `json:\"value\" form:\"value\"`\n}\n\n// Client represents a client configuration for Xray inbounds with traffic limits and settings.\ntype Client struct {\n\tID         string `json:\"id\"`                           // Unique client identifier\n\tSecurity   string `json:\"security\"`                     // Security method (e.g., \"auto\", \"aes-128-gcm\")\n\tPassword   string `json:\"password\"`                     // Client password\n\tFlow       string `json:\"flow\"`                         // Flow control (XTLS)\n\tEmail      string `json:\"email\"`                        // Client email identifier\n\tLimitIP    int    `json:\"limitIp\"`                      // IP limit for this client\n\tTotalGB    int64  `json:\"totalGB\" form:\"totalGB\"`       // Total traffic limit in GB\n\tExpiryTime int64  `json:\"expiryTime\" form:\"expiryTime\"` // Expiration timestamp\n\tEnable     bool   `json:\"enable\" form:\"enable\"`         // Whether the client is enabled\n\tTgID       int64  `json:\"tgId\" form:\"tgId\"`             // Telegram user ID for notifications\n\tSubID      string `json:\"subId\" form:\"subId\"`           // Subscription identifier\n\tComment    string `json:\"comment\" form:\"comment\"`       // Client comment\n\tReset      int    `json:\"reset\" form:\"reset\"`           // Reset period in days\n\tCreatedAt  int64  `json:\"created_at,omitempty\"`         // Creation timestamp\n\tUpdatedAt  int64  `json:\"updated_at,omitempty\"`         // Last update timestamp\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  3xui:\n    build:\n      context: .\n      dockerfile: ./Dockerfile\n    container_name: 3xui_app\n    # hostname: yourhostname <- optional\n    volumes:\n      - $PWD/db/:/etc/x-ui/\n      - $PWD/cert/:/root/cert/\n    environment:\n      XRAY_VMESS_AEAD_FORCED: \"false\"\n      XUI_ENABLE_FAIL2BAN: \"true\"\n    tty: true\n    network_mode: host\n    restart: unless-stopped\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/mhsanaei/3x-ui/v2\n\ngo 1.26.0\n\nrequire (\n\tgithub.com/gin-contrib/gzip v1.2.5\n\tgithub.com/gin-contrib/sessions v1.0.4\n\tgithub.com/gin-gonic/gin v1.12.0\n\tgithub.com/go-ldap/ldap/v3 v3.4.12\n\tgithub.com/goccy/go-json v0.10.5\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/mymmrac/telego v1.7.0\n\tgithub.com/nicksnyder/go-i18n/v2 v2.6.1\n\tgithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7\n\tgithub.com/pelletier/go-toml/v2 v2.2.4\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/shirou/gopsutil/v4 v4.26.2\n\tgithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e\n\tgithub.com/valyala/fasthttp v1.69.0\n\tgithub.com/xlzd/gotp v0.1.0\n\tgithub.com/xtls/xray-core v1.260206.0\n\tgo.uber.org/atomic v1.11.0\n\tgolang.org/x/crypto v0.48.0\n\tgolang.org/x/sys v0.41.0\n\tgolang.org/x/text v0.34.0\n\tgoogle.golang.org/grpc v1.79.1\n\tgorm.io/driver/sqlite v1.6.0\n\tgorm.io/gorm v1.31.1\n)\n\nrequire (\n\tgithub.com/Azure/go-ntlmssp v0.1.0 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 // indirect\n\tgithub.com/bytedance/gopkg v0.1.3 // indirect\n\tgithub.com/bytedance/sonic v1.15.0 // indirect\n\tgithub.com/bytedance/sonic/loader v0.5.0 // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.13 // indirect\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.30.1 // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/gorilla/context v1.1.2 // indirect\n\tgithub.com/gorilla/securecookie v1.1.2 // indirect\n\tgithub.com/gorilla/sessions v1.4.0 // indirect\n\tgithub.com/grbit/go-json v0.11.0 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/juju/ratelimit v1.0.2 // indirect\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-sqlite3 v1.14.34 // indirect\n\tgithub.com/miekg/dns v1.1.72 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pires/go-proxyproto v0.11.0 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/quic-go/qpack v0.6.0 // indirect\n\tgithub.com/quic-go/quic-go v0.59.0 // indirect\n\tgithub.com/refraction-networking/utls v1.8.2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/sagernet/sing v0.8.1 // indirect\n\tgithub.com/sagernet/sing-shadowsocks v0.2.9 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.1 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fastjson v1.6.10 // indirect\n\tgithub.com/vishvananda/netlink v1.3.1 // indirect\n\tgithub.com/vishvananda/netns v0.0.5 // indirect\n\tgithub.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.mongodb.org/mongo-driver/v2 v2.5.0 // indirect\n\tgo4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect\n\tgolang.org/x/arch v0.24.0 // indirect\n\tgolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect\n\tgolang.org/x/mod v0.33.0 // indirect\n\tgolang.org/x/net v0.51.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect\n\tgolang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect\n\tlukechampine.com/blake3 v1.4.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=\ngithub.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=\ngithub.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=\ngithub.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E=\ngithub.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU=\ngithub.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=\ngithub.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=\ngithub.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=\ngithub.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=\ngithub.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=\ngithub.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=\ngithub.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=\ngithub.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=\ngithub.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=\ngithub.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=\ngithub.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=\ngithub.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=\ngithub.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=\ngithub.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=\ngithub.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=\ngithub.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=\ngithub.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=\ngithub.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=\ngithub.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=\ngithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=\ngithub.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=\ngithub.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=\ngithub.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=\ngithub.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=\ngithub.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=\ngithub.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=\ngithub.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=\ngithub.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=\ngithub.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=\ngithub.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=\ngithub.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=\ngithub.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=\ngithub.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo=\ngithub.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=\ngithub.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=\ngithub.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=\ngithub.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=\ngithub.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=\ngithub.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=\ngithub.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=\ngithub.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=\ngithub.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/sagernet/sing v0.8.1 h1:Li+zg4xdiMsvdX4j50TPqmSG8LF/TB9US2qlAN40izU=\ngithub.com/sagernet/sing v0.8.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=\ngithub.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=\ngithub.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=\ngithub.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=\ngithub.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=\ngithub.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=\ngithub.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=\ngithub.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=\ngithub.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=\ngithub.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=\ngithub.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=\ngithub.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=\ngithub.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=\ngithub.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=\ngithub.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=\ngithub.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM=\ngithub.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=\ngithub.com/xtls/xray-core v1.260206.0 h1:gY8IV6u76CW93txL9QmacgZ0Udxr2Q3e9qUxXAhdHqI=\ngithub.com/xtls/xray-core v1.260206.0/go.mod h1:GyFIgVGRJkt3eyV/NMcdxOKXcJPqGGpyupHzy16uJhU=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=\ngo.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=\ngolang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=\ngolang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=\ngolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=\ngolang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=\ngolang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=\ngoogle.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=\ngorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=\ngorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=\ngorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=\ngvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk=\ngvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=\nlukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=\nlukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/bash\n\nred='\\033[0;31m'\ngreen='\\033[0;32m'\nblue='\\033[0;34m'\nyellow='\\033[0;33m'\nplain='\\033[0m'\n\ncur_dir=$(pwd)\n\nxui_folder=\"${XUI_MAIN_FOLDER:=/usr/local/x-ui}\"\nxui_service=\"${XUI_SERVICE:=/etc/systemd/system}\"\n\n# check root\n[[ $EUID -ne 0 ]] && echo -e \"${red}Fatal error: ${plain} Please run this script with root privilege \\n \" && exit 1\n\n# Check OS and set release variable\nif [[ -f /etc/os-release ]]; then\n    source /etc/os-release\n    release=$ID\n    elif [[ -f /usr/lib/os-release ]]; then\n    source /usr/lib/os-release\n    release=$ID\nelse\n    echo \"Failed to check the system OS, please contact the author!\" >&2\n    exit 1\nfi\necho \"The OS release is: $release\"\n\narch() {\n    case \"$(uname -m)\" in\n        x86_64 | x64 | amd64) echo 'amd64' ;;\n        i*86 | x86) echo '386' ;;\n        armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;\n        armv7* | armv7 | arm) echo 'armv7' ;;\n        armv6* | armv6) echo 'armv6' ;;\n        armv5* | armv5) echo 'armv5' ;;\n        s390x) echo 's390x' ;;\n        *) echo -e \"${green}Unsupported CPU architecture! ${plain}\" && rm -f install.sh && exit 1 ;;\n    esac\n}\n\necho \"Arch: $(arch)\"\n\n# Simple helpers\nis_ipv4() {\n    [[ \"$1\" =~ ^([0-9]{1,3}\\.){3}[0-9]{1,3}$ ]] && return 0 || return 1\n}\nis_ipv6() {\n    [[ \"$1\" =~ : ]] && return 0 || return 1\n}\nis_ip() {\n    is_ipv4 \"$1\" || is_ipv6 \"$1\"\n}\nis_domain() {\n    [[ \"$1\" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1\n}\n\n# Port helpers\nis_port_in_use() {\n    local port=\"$1\"\n    if command -v ss >/dev/null 2>&1; then\n        ss -ltn 2>/dev/null | awk -v p=\":${port}$\" '$4 ~ p {exit 0} END {exit 1}'\n        return\n    fi\n    if command -v netstat >/dev/null 2>&1; then\n        netstat -lnt 2>/dev/null | awk -v p=\":${port} \" '$4 ~ p {exit 0} END {exit 1}'\n        return\n    fi\n    if command -v lsof >/dev/null 2>&1; then\n        lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0\n    fi\n    return 1\n}\n\ninstall_base() {\n    case \"${release}\" in\n        ubuntu | debian | armbian)\n            apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates\n        ;;\n        fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)\n            dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates\n        ;;\n        centos)\n            if [[ \"${VERSION_ID}\" =~ ^7 ]]; then\n                yum -y update && yum install -y curl tar tzdata socat ca-certificates\n            else\n                dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates\n            fi\n        ;;\n        arch | manjaro | parch)\n            pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ca-certificates\n        ;;\n        opensuse-tumbleweed | opensuse-leap)\n            zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates\n        ;;\n        alpine)\n            apk update && apk add curl tar tzdata socat ca-certificates\n        ;;\n        *)\n            apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates\n        ;;\n    esac\n}\n\ngen_random_string() {\n    local length=\"$1\"\n    local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w \"$length\" | head -n 1)\n    echo \"$random_string\"\n}\n\ninstall_acme() {\n    echo -e \"${green}Installing acme.sh for SSL certificate management...${plain}\"\n    cd ~ || return 1\n    curl -s https://get.acme.sh | sh >/dev/null 2>&1\n    if [ $? -ne 0 ]; then\n        echo -e \"${red}Failed to install acme.sh${plain}\"\n        return 1\n    else\n        echo -e \"${green}acme.sh installed successfully${plain}\"\n    fi\n    return 0\n}\n\nsetup_ssl_certificate() {\n    local domain=\"$1\"\n    local server_ip=\"$2\"\n    local existing_port=\"$3\"\n    local existing_webBasePath=\"$4\"\n    \n    echo -e \"${green}Setting up SSL certificate...${plain}\"\n    \n    # Check if acme.sh is installed\n    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        install_acme\n        if [ $? -ne 0 ]; then\n            echo -e \"${yellow}Failed to install acme.sh, skipping SSL setup${plain}\"\n            return 1\n        fi\n    fi\n    \n    # Create certificate directory\n    local certPath=\"/root/cert/${domain}\"\n    mkdir -p \"$certPath\"\n    \n    # Issue certificate\n    echo -e \"${green}Issuing SSL certificate for ${domain}...${plain}\"\n    echo -e \"${yellow}Note: Port 80 must be open and accessible from the internet${plain}\"\n    \n    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1\n    ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force\n    \n    if [ $? -ne 0 ]; then\n        echo -e \"${yellow}Failed to issue certificate for ${domain}${plain}\"\n        echo -e \"${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}\"\n        rm -rf ~/.acme.sh/${domain} 2>/dev/null\n        rm -rf \"$certPath\" 2>/dev/null\n        return 1\n    fi\n    \n    # Install certificate\n    ~/.acme.sh/acme.sh --installcert -d ${domain} \\\n        --key-file /root/cert/${domain}/privkey.pem \\\n        --fullchain-file /root/cert/${domain}/fullchain.pem \\\n        --reloadcmd \"systemctl restart x-ui\" >/dev/null 2>&1\n    \n    if [ $? -ne 0 ]; then\n        echo -e \"${yellow}Failed to install certificate${plain}\"\n        return 1\n    fi\n    \n    # Enable auto-renew\n    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1\n    # Secure permissions: private key readable only by owner\n    chmod 600 $certPath/privkey.pem 2>/dev/null\n    chmod 644 $certPath/fullchain.pem 2>/dev/null\n    \n    # Set certificate for panel\n    local webCertFile=\"/root/cert/${domain}/fullchain.pem\"\n    local webKeyFile=\"/root/cert/${domain}/privkey.pem\"\n    \n    if [[ -f \"$webCertFile\" && -f \"$webKeyFile\" ]]; then\n        ${xui_folder}/x-ui cert -webCert \"$webCertFile\" -webCertKey \"$webKeyFile\" >/dev/null 2>&1\n        echo -e \"${green}SSL certificate installed and configured successfully!${plain}\"\n        return 0\n    else\n        echo -e \"${yellow}Certificate files not found${plain}\"\n        return 1\n    fi\n}\n\n# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)\n# Requires acme.sh and port 80 open for HTTP-01 challenge\nsetup_ip_certificate() {\n    local ipv4=\"$1\"\n    local ipv6=\"$2\"  # optional\n\n    echo -e \"${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}\"\n    echo -e \"${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}\"\n    echo -e \"${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}\"\n\n    # Check for acme.sh\n    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        install_acme\n        if [ $? -ne 0 ]; then\n            echo -e \"${red}Failed to install acme.sh${plain}\"\n            return 1\n        fi\n    fi\n\n    # Validate IP address\n    if [[ -z \"$ipv4\" ]]; then\n        echo -e \"${red}IPv4 address is required${plain}\"\n        return 1\n    fi\n\n    if ! is_ipv4 \"$ipv4\"; then\n        echo -e \"${red}Invalid IPv4 address: $ipv4${plain}\"\n        return 1\n    fi\n\n    # Create certificate directory\n    local certDir=\"/root/cert/ip\"\n    mkdir -p \"$certDir\"\n\n    # Build domain arguments\n    local domain_args=\"-d ${ipv4}\"\n    if [[ -n \"$ipv6\" ]] && is_ipv6 \"$ipv6\"; then\n        domain_args=\"${domain_args} -d ${ipv6}\"\n        echo -e \"${green}Including IPv6 address: ${ipv6}${plain}\"\n    fi\n\n    # Set reload command for auto-renewal (add || true so it doesn't fail during first install)\n    local reloadCmd=\"systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true\"\n\n    # Choose port for HTTP-01 listener (default 80, prompt override)\n    local WebPort=\"\"\n    read -rp \"Port to use for ACME HTTP-01 listener (default 80): \" WebPort\n    WebPort=\"${WebPort:-80}\"\n    if ! [[ \"${WebPort}\" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then\n        echo -e \"${red}Invalid port provided. Falling back to 80.${plain}\"\n        WebPort=80\n    fi\n    echo -e \"${green}Using port ${WebPort} for standalone validation.${plain}\"\n    if [[ \"${WebPort}\" -ne 80 ]]; then\n        echo -e \"${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}\"\n    fi\n\n    # Ensure chosen port is available\n    while true; do\n        if is_port_in_use \"${WebPort}\"; then\n            echo -e \"${yellow}Port ${WebPort} is in use.${plain}\"\n\n            local alt_port=\"\"\n            read -rp \"Enter another port for acme.sh standalone listener (leave empty to abort): \" alt_port\n            alt_port=\"${alt_port// /}\"\n            if [[ -z \"${alt_port}\" ]]; then\n                echo -e \"${red}Port ${WebPort} is busy; cannot proceed.${plain}\"\n                return 1\n            fi\n            if ! [[ \"${alt_port}\" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then\n                echo -e \"${red}Invalid port provided.${plain}\"\n                return 1\n            fi\n            WebPort=\"${alt_port}\"\n            continue\n        else\n            echo -e \"${green}Port ${WebPort} is free and ready for standalone validation.${plain}\"\n            break\n        fi\n    done\n\n    # Issue certificate with shortlived profile\n    echo -e \"${green}Issuing IP certificate for ${ipv4}...${plain}\"\n    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1\n    \n    ~/.acme.sh/acme.sh --issue \\\n        ${domain_args} \\\n        --standalone \\\n        --server letsencrypt \\\n        --certificate-profile shortlived \\\n        --days 6 \\\n        --httpport ${WebPort} \\\n        --force\n\n    if [ $? -ne 0 ]; then\n        echo -e \"${red}Failed to issue IP certificate${plain}\"\n        echo -e \"${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}\"\n        # Cleanup acme.sh data for both IPv4 and IPv6 if specified\n        rm -rf ~/.acme.sh/${ipv4} 2>/dev/null\n        [[ -n \"$ipv6\" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null\n        rm -rf ${certDir} 2>/dev/null\n        return 1\n    fi\n\n    echo -e \"${green}Certificate issued successfully, installing...${plain}\"\n\n    # Install certificate\n    # Note: acme.sh may report \"Reload error\" and exit non-zero if reloadcmd fails,\n    # but the cert files are still installed. We check for files instead of exit code.\n    ~/.acme.sh/acme.sh --installcert -d ${ipv4} \\\n        --key-file \"${certDir}/privkey.pem\" \\\n        --fullchain-file \"${certDir}/fullchain.pem\" \\\n        --reloadcmd \"${reloadCmd}\" 2>&1 || true\n\n    # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)\n    if [[ ! -f \"${certDir}/fullchain.pem\" || ! -f \"${certDir}/privkey.pem\" ]]; then\n        echo -e \"${red}Certificate files not found after installation${plain}\"\n        # Cleanup acme.sh data for both IPv4 and IPv6 if specified\n        rm -rf ~/.acme.sh/${ipv4} 2>/dev/null\n        [[ -n \"$ipv6\" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null\n        rm -rf ${certDir} 2>/dev/null\n        return 1\n    fi\n    \n    echo -e \"${green}Certificate files installed successfully${plain}\"\n\n    # Enable auto-upgrade for acme.sh (ensures cron job runs)\n    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1\n\n    # Secure permissions: private key readable only by owner\n    chmod 600 ${certDir}/privkey.pem 2>/dev/null\n    chmod 644 ${certDir}/fullchain.pem 2>/dev/null\n\n    # Configure panel to use the certificate\n    echo -e \"${green}Setting certificate paths for the panel...${plain}\"\n    ${xui_folder}/x-ui cert -webCert \"${certDir}/fullchain.pem\" -webCertKey \"${certDir}/privkey.pem\"\n    \n    if [ $? -ne 0 ]; then\n        echo -e \"${yellow}Warning: Could not set certificate paths automatically${plain}\"\n        echo -e \"${yellow}Certificate files are at:${plain}\"\n        echo -e \"  Cert: ${certDir}/fullchain.pem\"\n        echo -e \"  Key:  ${certDir}/privkey.pem\"\n    else\n        echo -e \"${green}Certificate paths configured successfully${plain}\"\n    fi\n\n    echo -e \"${green}IP certificate installed and configured successfully!${plain}\"\n    echo -e \"${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}\"\n    echo -e \"${yellow}acme.sh will automatically renew and reload x-ui before expiry.${plain}\"\n    return 0\n}\n\n# Comprehensive manual SSL certificate issuance via acme.sh\nssl_cert_issue() {\n    local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')\n    local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')\n    \n    # check for acme.sh first\n    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        echo \"acme.sh could not be found. Installing now...\"\n        cd ~ || return 1\n        curl -s https://get.acme.sh | sh\n        if [ $? -ne 0 ]; then\n            echo -e \"${red}Failed to install acme.sh${plain}\"\n            return 1\n        else\n            echo -e \"${green}acme.sh installed successfully${plain}\"\n        fi\n    fi\n\n    # get the domain here, and we need to verify it\n    local domain=\"\"\n    while true; do\n        read -rp \"Please enter your domain name: \" domain\n        domain=\"${domain// /}\"  # Trim whitespace\n        \n        if [[ -z \"$domain\" ]]; then\n            echo -e \"${red}Domain name cannot be empty. Please try again.${plain}\"\n            continue\n        fi\n        \n        if ! is_domain \"$domain\"; then\n            echo -e \"${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}\"\n            continue\n        fi\n        \n        break\n    done\n    echo -e \"${green}Your domain is: ${domain}, checking it...${plain}\"\n\n    # check if there already exists a certificate\n    local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')\n    if [ \"${currentCert}\" == \"${domain}\" ]; then\n        local certInfo=$(~/.acme.sh/acme.sh --list)\n        echo -e \"${red}System already has certificates for this domain. Cannot issue again.${plain}\"\n        echo -e \"${yellow}Current certificate details:${plain}\"\n        echo \"$certInfo\"\n        return 1\n    else\n        echo -e \"${green}Your domain is ready for issuing certificates now...${plain}\"\n    fi\n\n    # create a directory for the certificate\n    certPath=\"/root/cert/${domain}\"\n    if [ ! -d \"$certPath\" ]; then\n        mkdir -p \"$certPath\"\n    else\n        rm -rf \"$certPath\"\n        mkdir -p \"$certPath\"\n    fi\n\n    # get the port number for the standalone server\n    local WebPort=80\n    read -rp \"Please choose which port to use (default is 80): \" WebPort\n    if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then\n        echo -e \"${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}\"\n        WebPort=80\n    fi\n    echo -e \"${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}\"\n\n    # Stop panel temporarily\n    echo -e \"${yellow}Stopping panel temporarily...${plain}\"\n    systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null\n\n    # issue the certificate\n    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force\n    ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force\n    if [ $? -ne 0 ]; then\n        echo -e \"${red}Issuing certificate failed, please check logs.${plain}\"\n        rm -rf ~/.acme.sh/${domain}\n        systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null\n        return 1\n    else\n        echo -e \"${green}Issuing certificate succeeded, installing certificates...${plain}\"\n    fi\n\n    # Setup reload command\n    reloadCmd=\"systemctl restart x-ui || rc-service x-ui restart\"\n    echo -e \"${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}\"\n    echo -e \"${green}This command will run on every certificate issue and renew.${plain}\"\n    read -rp \"Would you like to modify --reloadcmd for ACME? (y/n): \" setReloadcmd\n    if [[ \"$setReloadcmd\" == \"y\" || \"$setReloadcmd\" == \"Y\" ]]; then\n        echo -e \"\\n${green}\\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui\"\n        echo -e \"${green}\\t2.${plain} Input your own command\"\n        echo -e \"${green}\\t0.${plain} Keep default reloadcmd\"\n        read -rp \"Choose an option: \" choice\n        case \"$choice\" in\n        1)\n            echo -e \"${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}\"\n            reloadCmd=\"systemctl reload nginx ; systemctl restart x-ui\"\n            ;;\n        2)\n            echo -e \"${yellow}It's recommended to put x-ui restart at the end${plain}\"\n            read -rp \"Please enter your custom reloadcmd: \" reloadCmd\n            echo -e \"${green}Reloadcmd is: ${reloadCmd}${plain}\"\n            ;;\n        *)\n            echo -e \"${green}Keeping default reloadcmd${plain}\"\n            ;;\n        esac\n    fi\n\n    # install the certificate\n    ~/.acme.sh/acme.sh --installcert -d ${domain} \\\n        --key-file /root/cert/${domain}/privkey.pem \\\n        --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd \"${reloadCmd}\"\n\n    if [ $? -ne 0 ]; then\n        echo -e \"${red}Installing certificate failed, exiting.${plain}\"\n        rm -rf ~/.acme.sh/${domain}\n        systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null\n        return 1\n    else\n        echo -e \"${green}Installing certificate succeeded, enabling auto renew...${plain}\"\n    fi\n\n    # enable auto-renew\n    ~/.acme.sh/acme.sh --upgrade --auto-upgrade\n    if [ $? -ne 0 ]; then\n        echo -e \"${yellow}Auto renew setup had issues, certificate details:${plain}\"\n        ls -lah /root/cert/${domain}/\n        # Secure permissions: private key readable only by owner\n        chmod 600 $certPath/privkey.pem 2>/dev/null\n        chmod 644 $certPath/fullchain.pem 2>/dev/null\n    else\n        echo -e \"${green}Auto renew succeeded, certificate details:${plain}\"\n        ls -lah /root/cert/${domain}/\n        # Secure permissions: private key readable only by owner\n        chmod 600 $certPath/privkey.pem 2>/dev/null\n        chmod 644 $certPath/fullchain.pem 2>/dev/null\n    fi\n\n    # start panel\n    systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null\n\n    # Prompt user to set panel paths after successful certificate installation\n    read -rp \"Would you like to set this certificate for the panel? (y/n): \" setPanel\n    if [[ \"$setPanel\" == \"y\" || \"$setPanel\" == \"Y\" ]]; then\n        local webCertFile=\"/root/cert/${domain}/fullchain.pem\"\n        local webKeyFile=\"/root/cert/${domain}/privkey.pem\"\n\n        if [[ -f \"$webCertFile\" && -f \"$webKeyFile\" ]]; then\n            ${xui_folder}/x-ui cert -webCert \"$webCertFile\" -webCertKey \"$webKeyFile\"\n            echo -e \"${green}Certificate paths set for the panel${plain}\"\n            echo -e \"${green}Certificate File: $webCertFile${plain}\"\n            echo -e \"${green}Private Key File: $webKeyFile${plain}\"\n            echo \"\"\n            echo -e \"${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}\"\n            echo -e \"${yellow}Panel will restart to apply SSL certificate...${plain}\"\n            systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null\n        else\n            echo -e \"${red}Error: Certificate or private key file not found for domain: $domain.${plain}\"\n        fi\n    else\n        echo -e \"${yellow}Skipping panel path setting.${plain}\"\n    fi\n    \n    return 0\n}\n\n# Reusable interactive SSL setup (domain or IP)\n# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage\nprompt_and_setup_ssl() {\n    local panel_port=\"$1\"\n    local web_base_path=\"$2\"   # expected without leading slash\n    local server_ip=\"$3\"\n\n    local ssl_choice=\"\"\n\n    echo -e \"${yellow}Choose SSL certificate setup method:${plain}\"\n    echo -e \"${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)\"\n    echo -e \"${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)\"\n    echo -e \"${green}3.${plain} Custom SSL Certificate (Path to existing files)\"\n    echo -e \"${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths.\"\n    read -rp \"Choose an option (default 2 for IP): \" ssl_choice\n    ssl_choice=\"${ssl_choice// /}\"  # Trim whitespace\n    \n    # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)\n    if [[ \"$ssl_choice\" != \"1\" && \"$ssl_choice\" != \"3\" ]]; then\n        ssl_choice=\"2\"\n    fi\n\n    case \"$ssl_choice\" in\n    1)\n        # User chose Let's Encrypt domain option\n        echo -e \"${green}Using Let's Encrypt for domain certificate...${plain}\"\n        ssl_cert_issue\n        # Extract the domain that was used from the certificate\n        local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')\n        if [[ -n \"${cert_domain}\" ]]; then\n            SSL_HOST=\"${cert_domain}\"\n            echo -e \"${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}\"\n        else\n            echo -e \"${yellow}SSL setup may have completed, but domain extraction failed${plain}\"\n            SSL_HOST=\"${server_ip}\"\n        fi\n        ;;\n    2)\n        # User chose Let's Encrypt IP certificate option\n        echo -e \"${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}\"\n        \n        # Ask for optional IPv6\n        local ipv6_addr=\"\"\n        read -rp \"Do you have an IPv6 address to include? (leave empty to skip): \" ipv6_addr\n        ipv6_addr=\"${ipv6_addr// /}\"  # Trim whitespace\n        \n        # Stop panel if running (port 80 needed)\n        if [[ $release == \"alpine\" ]]; then\n            rc-service x-ui stop >/dev/null 2>&1\n        else\n            systemctl stop x-ui >/dev/null 2>&1\n        fi\n        \n        setup_ip_certificate \"${server_ip}\" \"${ipv6_addr}\"\n        if [ $? -eq 0 ]; then\n            SSL_HOST=\"${server_ip}\"\n            echo -e \"${green}✓ Let's Encrypt IP certificate configured successfully${plain}\"\n        else\n            echo -e \"${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}\"\n            SSL_HOST=\"${server_ip}\"\n        fi\n        ;;\n    3)\n        # User chose Custom Paths (User Provided) option\n        echo -e \"${green}Using custom existing certificate...${plain}\"\n        local custom_cert=\"\"\n        local custom_key=\"\"\n        local custom_domain=\"\"\n\n        # 3.1 Request Domain to compose Panel URL later\n        read -rp \"Please enter domain name certificate issued for: \" custom_domain\n        custom_domain=\"${custom_domain// /}\" # Убираем пробелы\n\n        # 3.2 Loop for Certificate Path\n        while true; do\n            read -rp \"Input certificate path (keywords: .crt / fullchain): \" custom_cert\n            # Strip quotes if present\n            custom_cert=$(echo \"$custom_cert\" | tr -d '\"' | tr -d \"'\")\n\n            if [[ -f \"$custom_cert\" && -r \"$custom_cert\" && -s \"$custom_cert\" ]]; then\n                break\n            elif [[ ! -f \"$custom_cert\" ]]; then\n                echo -e \"${red}Error: File does not exist! Try again.${plain}\"\n            elif [[ ! -r \"$custom_cert\" ]]; then\n                echo -e \"${red}Error: File exists but is not readable (check permissions)!${plain}\"\n            else\n                echo -e \"${red}Error: File is empty!${plain}\"\n            fi\n        done\n\n        # 3.3 Loop for Private Key Path\n        while true; do\n            read -rp \"Input private key path (keywords: .key / privatekey): \" custom_key\n            # Strip quotes if present\n            custom_key=$(echo \"$custom_key\" | tr -d '\"' | tr -d \"'\")\n\n            if [[ -f \"$custom_key\" && -r \"$custom_key\" && -s \"$custom_key\" ]]; then\n                break\n            elif [[ ! -f \"$custom_key\" ]]; then\n                echo -e \"${red}Error: File does not exist! Try again.${plain}\"\n            elif [[ ! -r \"$custom_key\" ]]; then\n                echo -e \"${red}Error: File exists but is not readable (check permissions)!${plain}\"\n            else\n                echo -e \"${red}Error: File is empty!${plain}\"\n            fi\n        done\n\n        # 3.4 Apply Settings via x-ui binary\n        ${xui_folder}/x-ui cert -webCert \"$custom_cert\" -webCertKey \"$custom_key\" >/dev/null 2>&1\n        \n        # Set SSL_HOST for composing Panel URL\n        if [[ -n \"$custom_domain\" ]]; then\n            SSL_HOST=\"$custom_domain\"\n        else\n            SSL_HOST=\"${server_ip}\"\n        fi\n\n        echo -e \"${green}✓ Custom certificate paths applied.${plain}\"\n        echo -e \"${yellow}Note: You are responsible for renewing these files externally.${plain}\"\n\n        systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1\n        ;;\n    *)\n        echo -e \"${red}Invalid option. Skipping SSL setup.${plain}\"\n        SSL_HOST=\"${server_ip}\"\n        ;;\n    esac\n}\n\nconfig_after_install() {\n    local existing_hasDefaultCredential=$(${xui_folder}/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')\n    local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')\n    local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')\n    # Properly detect empty cert by checking if cert: line exists and has content after it\n    local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')\n    local URL_lists=(\n        \"https://api4.ipify.org\"\n        \"https://ipv4.icanhazip.com\"\n        \"https://v4.api.ipinfo.io/ip\"\n        \"https://ipv4.myexternalip.com/raw\"\n        \"https://4.ident.me\"\n        \"https://check-host.net/ip\"\n    )\n    local server_ip=\"\"\n    for ip_address in \"${URL_lists[@]}\"; do\n        local response=$(curl -s -w \"\\n%{http_code}\" --max-time 3 \"${ip_address}\" 2>/dev/null)\n        local http_code=$(echo \"$response\" | tail -n1)\n        local ip_result=$(echo \"$response\" | head -n-1 | tr -d '[:space:]')\n        if [[ \"${http_code}\" == \"200\" && -n \"${ip_result}\" ]]; then\n            server_ip=\"${ip_result}\"\n            break\n        fi\n    done\n    \n    if [[ ${#existing_webBasePath} -lt 4 ]]; then\n        if [[ \"$existing_hasDefaultCredential\" == \"true\" ]]; then\n            local config_webBasePath=$(gen_random_string 18)\n            local config_username=$(gen_random_string 10)\n            local config_password=$(gen_random_string 10)\n            \n            read -rp \"Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: \" config_confirm\n            if [[ \"${config_confirm}\" == \"y\" || \"${config_confirm}\" == \"Y\" ]]; then\n                read -rp \"Please set up the panel port: \" config_port\n                echo -e \"${yellow}Your Panel Port is: ${config_port}${plain}\"\n            else\n                local config_port=$(shuf -i 1024-62000 -n 1)\n                echo -e \"${yellow}Generated random port: ${config_port}${plain}\"\n            fi\n            \n            ${xui_folder}/x-ui setting -username \"${config_username}\" -password \"${config_password}\" -port \"${config_port}\" -webBasePath \"${config_webBasePath}\"\n            \n            echo \"\"\n            echo -e \"${green}═══════════════════════════════════════════${plain}\"\n            echo -e \"${green}     SSL Certificate Setup (MANDATORY)     ${plain}\"\n            echo -e \"${green}═══════════════════════════════════════════${plain}\"\n            echo -e \"${yellow}For security, SSL certificate is required for all panels.${plain}\"\n            echo -e \"${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}\"\n            echo \"\"\n\n            prompt_and_setup_ssl \"${config_port}\" \"${config_webBasePath}\" \"${server_ip}\"\n            \n            # Display final credentials and access information\n            echo \"\"\n            echo -e \"${green}═══════════════════════════════════════════${plain}\"\n            echo -e \"${green}     Panel Installation Complete!         ${plain}\"\n            echo -e \"${green}═══════════════════════════════════════════${plain}\"\n            echo -e \"${green}Username:    ${config_username}${plain}\"\n            echo -e \"${green}Password:    ${config_password}${plain}\"\n            echo -e \"${green}Port:        ${config_port}${plain}\"\n            echo -e \"${green}WebBasePath: ${config_webBasePath}${plain}\"\n            echo -e \"${green}Access URL:  https://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}\"\n            echo -e \"${green}═══════════════════════════════════════════${plain}\"\n            echo -e \"${yellow}⚠ IMPORTANT: Save these credentials securely!${plain}\"\n            echo -e \"${yellow}⚠ SSL Certificate: Enabled and configured${plain}\"\n        else\n            local config_webBasePath=$(gen_random_string 18)\n            echo -e \"${yellow}WebBasePath is missing or too short. Generating a new one...${plain}\"\n            ${xui_folder}/x-ui setting -webBasePath \"${config_webBasePath}\"\n            echo -e \"${green}New WebBasePath: ${config_webBasePath}${plain}\"\n\n            # If the panel is already installed but no certificate is configured, prompt for SSL now\n            if [[ -z \"${existing_cert}\" ]]; then\n                echo \"\"\n                echo -e \"${green}═══════════════════════════════════════════${plain}\"\n                echo -e \"${green}     SSL Certificate Setup (RECOMMENDED)   ${plain}\"\n                echo -e \"${green}═══════════════════════════════════════════${plain}\"\n                echo -e \"${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}\"\n                echo \"\"\n                prompt_and_setup_ssl \"${existing_port}\" \"${config_webBasePath}\" \"${server_ip}\"\n                echo -e \"${green}Access URL:  https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}\"\n            else\n                # If a cert already exists, just show the access URL\n                echo -e \"${green}Access URL: https://${server_ip}:${existing_port}/${config_webBasePath}${plain}\"\n            fi\n        fi\n    else\n        if [[ \"$existing_hasDefaultCredential\" == \"true\" ]]; then\n            local config_username=$(gen_random_string 10)\n            local config_password=$(gen_random_string 10)\n            \n            echo -e \"${yellow}Default credentials detected. Security update required...${plain}\"\n            ${xui_folder}/x-ui setting -username \"${config_username}\" -password \"${config_password}\"\n            echo -e \"Generated new random login credentials:\"\n            echo -e \"###############################################\"\n            echo -e \"${green}Username: ${config_username}${plain}\"\n            echo -e \"${green}Password: ${config_password}${plain}\"\n            echo -e \"###############################################\"\n        else\n            echo -e \"${green}Username, Password, and WebBasePath are properly set.${plain}\"\n        fi\n\n        # Existing install: if no cert configured, prompt user for SSL setup\n        # Properly detect empty cert by checking if cert: line exists and has content after it\n        existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')\n        if [[ -z \"$existing_cert\" ]]; then\n            echo \"\"\n            echo -e \"${green}═══════════════════════════════════════════${plain}\"\n            echo -e \"${green}     SSL Certificate Setup (RECOMMENDED)   ${plain}\"\n            echo -e \"${green}═══════════════════════════════════════════${plain}\"\n            echo -e \"${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}\"\n            echo \"\"\n            prompt_and_setup_ssl \"${existing_port}\" \"${existing_webBasePath}\" \"${server_ip}\"\n            echo -e \"${green}Access URL:  https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}\"\n        else\n            echo -e \"${green}SSL certificate already configured. No action needed.${plain}\"\n        fi\n    fi\n    \n    ${xui_folder}/x-ui migrate\n}\n\ninstall_x-ui() {\n    cd ${xui_folder%/x-ui}/\n    \n    # Download resources\n    if [ $# == 0 ]; then\n        tag_version=$(curl -Ls \"https://api.github.com/repos/MHSanaei/3x-ui/releases/latest\" | grep '\"tag_name\":' | sed -E 's/.*\"([^\"]+)\".*/\\1/')\n        if [[ ! -n \"$tag_version\" ]]; then\n            echo -e \"${yellow}Trying to fetch version with IPv4...${plain}\"\n            tag_version=$(curl -4 -Ls \"https://api.github.com/repos/MHSanaei/3x-ui/releases/latest\" | grep '\"tag_name\":' | sed -E 's/.*\"([^\"]+)\".*/\\1/')\n            if [[ ! -n \"$tag_version\" ]]; then\n                echo -e \"${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}\"\n                exit 1\n            fi\n        fi\n        echo -e \"Got x-ui latest version: ${tag_version}, beginning the installation...\"\n        curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz\n        if [[ $? -ne 0 ]]; then\n            echo -e \"${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}\"\n            exit 1\n        fi\n    else\n        tag_version=$1\n        tag_version_numeric=${tag_version#v}\n        min_version=\"2.3.5\"\n        \n        if [[ \"$(printf '%s\\n' \"$min_version\" \"$tag_version_numeric\" | sort -V | head -n1)\" != \"$min_version\" ]]; then\n            echo -e \"${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}\"\n            exit 1\n        fi\n        \n        url=\"https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz\"\n        echo -e \"Beginning to install x-ui $1\"\n        curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url}\n        if [[ $? -ne 0 ]]; then\n            echo -e \"${red}Download x-ui $1 failed, please check if the version exists ${plain}\"\n            exit 1\n        fi\n    fi\n    curl -4fLRo /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh\n    if [[ $? -ne 0 ]]; then\n        echo -e \"${red}Failed to download x-ui.sh${plain}\"\n        exit 1\n    fi\n    \n    # Stop x-ui service and remove old resources\n    if [[ -e ${xui_folder}/ ]]; then\n        if [[ $release == \"alpine\" ]]; then\n            rc-service x-ui stop\n        else\n            systemctl stop x-ui\n        fi\n        rm ${xui_folder}/ -rf\n    fi\n    \n    # Extract resources and set permissions\n    tar zxvf x-ui-linux-$(arch).tar.gz\n    rm x-ui-linux-$(arch).tar.gz -f\n    \n    cd x-ui\n    chmod +x x-ui\n    chmod +x x-ui.sh\n    \n    # Check the system's architecture and rename the file accordingly\n    if [[ $(arch) == \"armv5\" || $(arch) == \"armv6\" || $(arch) == \"armv7\" ]]; then\n        mv bin/xray-linux-$(arch) bin/xray-linux-arm\n        chmod +x bin/xray-linux-arm\n    fi\n    chmod +x x-ui bin/xray-linux-$(arch)\n    \n    # Update x-ui cli and se set permission\n    mv -f /usr/bin/x-ui-temp /usr/bin/x-ui\n    chmod +x /usr/bin/x-ui\n    mkdir -p /var/log/x-ui\n    config_after_install\n\n    # Etckeeper compatibility\n    if [ -d \"/etc/.git\" ]; then\n        if [ -f \"/etc/.gitignore\" ]; then\n            if ! grep -q \"x-ui/x-ui.db\" \"/etc/.gitignore\"; then\n                echo \"\" >> \"/etc/.gitignore\"\n                echo \"x-ui/x-ui.db\" >> \"/etc/.gitignore\"\n                echo -e \"${green}Added x-ui.db to /etc/.gitignore for etckeeper${plain}\"\n            fi\n        else\n            echo \"x-ui/x-ui.db\" > \"/etc/.gitignore\"\n            echo -e \"${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}\"\n        fi\n    fi\n    \n    if [[ $release == \"alpine\" ]]; then\n        curl -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc\n        if [[ $? -ne 0 ]]; then\n            echo -e \"${red}Failed to download x-ui.rc${plain}\"\n            exit 1\n        fi\n        chmod +x /etc/init.d/x-ui\n        rc-update add x-ui\n        rc-service x-ui start\n    else\n        # Install systemd service file\n        service_installed=false\n        \n        if [ -f \"x-ui.service\" ]; then\n            echo -e \"${green}Found x-ui.service in extracted files, installing...${plain}\"\n            cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1\n            if [[ $? -eq 0 ]]; then\n                service_installed=true\n            fi\n        fi\n        \n        if [ \"$service_installed\" = false ]; then\n            case \"${release}\" in\n                ubuntu | debian | armbian)\n                    if [ -f \"x-ui.service.debian\" ]; then\n                        echo -e \"${green}Found x-ui.service.debian in extracted files, installing...${plain}\"\n                        cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1\n                        if [[ $? -eq 0 ]]; then\n                            service_installed=true\n                        fi\n                    fi\n                ;;\n                arch | manjaro | parch)\n                    if [ -f \"x-ui.service.arch\" ]; then\n                        echo -e \"${green}Found x-ui.service.arch in extracted files, installing...${plain}\"\n                        cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1\n                        if [[ $? -eq 0 ]]; then\n                            service_installed=true\n                        fi\n                    fi\n                ;;\n                *)\n                    if [ -f \"x-ui.service.rhel\" ]; then\n                        echo -e \"${green}Found x-ui.service.rhel in extracted files, installing...${plain}\"\n                        cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1\n                        if [[ $? -eq 0 ]]; then\n                            service_installed=true\n                        fi\n                    fi\n                ;;\n            esac\n        fi\n        \n        # If service file not found in tar.gz, download from GitHub\n        if [ \"$service_installed\" = false ]; then\n            echo -e \"${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}\"\n            case \"${release}\" in\n                ubuntu | debian | armbian)\n                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1\n                ;;\n                arch | manjaro | parch)\n                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1\n                ;;\n                *)\n                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1\n                ;;\n            esac\n            \n            if [[ $? -ne 0 ]]; then\n                echo -e \"${red}Failed to install x-ui.service from GitHub${plain}\"\n                exit 1\n            fi\n            service_installed=true\n        fi\n        \n        if [ \"$service_installed\" = true ]; then\n            echo -e \"${green}Setting up systemd unit...${plain}\"\n            chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1\n            chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1\n            systemctl daemon-reload\n            systemctl enable x-ui\n            systemctl start x-ui\n        else\n            echo -e \"${red}Failed to install x-ui.service file${plain}\"\n            exit 1\n        fi\n    fi\n    \n    echo -e \"${green}x-ui ${tag_version}${plain} installation finished, it is running now...\"\n    echo -e \"\"\n    echo -e \"┌───────────────────────────────────────────────────────┐\n│  ${blue}x-ui control menu usages (subcommands):${plain}              │\n│                                                       │\n│  ${blue}x-ui${plain}              - Admin Management Script          │\n│  ${blue}x-ui start${plain}        - Start                            │\n│  ${blue}x-ui stop${plain}         - Stop                             │\n│  ${blue}x-ui restart${plain}      - Restart                          │\n│  ${blue}x-ui status${plain}       - Current Status                   │\n│  ${blue}x-ui settings${plain}     - Current Settings                 │\n│  ${blue}x-ui enable${plain}       - Enable Autostart on OS Startup   │\n│  ${blue}x-ui disable${plain}      - Disable Autostart on OS Startup  │\n│  ${blue}x-ui log${plain}          - Check logs                       │\n│  ${blue}x-ui banlog${plain}       - Check Fail2ban ban logs          │\n│  ${blue}x-ui update${plain}       - Update                           │\n│  ${blue}x-ui legacy${plain}       - Legacy version                   │\n│  ${blue}x-ui install${plain}      - Install                          │\n│  ${blue}x-ui uninstall${plain}    - Uninstall                        │\n└───────────────────────────────────────────────────────┘\"\n}\n\necho -e \"${green}Running...${plain}\"\ninstall_base\ninstall_x-ui $1\n"
  },
  {
    "path": "logger/logger.go",
    "content": "// Package logger provides logging functionality for the 3x-ui panel with\n// dual-backend logging (console/syslog and file) and buffered log storage for web UI.\npackage logger\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/op/go-logging\"\n)\n\nconst (\n\tmaxLogBufferSize = 10240                 // Maximum log entries kept in memory\n\tlogFileName      = \"3xui.log\"            // Log file name\n\ttimeFormat       = \"2006/01/02 15:04:05\" // Log timestamp format\n)\n\nvar (\n\tlogger  *logging.Logger\n\tlogFile *os.File\n\n\t// logBuffer maintains recent log entries in memory for web UI retrieval\n\tlogBuffer []struct {\n\t\ttime  string\n\t\tlevel logging.Level\n\t\tlog   string\n\t}\n)\n\n// InitLogger initializes dual logging backends: console/syslog and file.\n// Console logging uses the specified level, file logging always uses DEBUG level.\nfunc InitLogger(level logging.Level) {\n\tnewLogger := logging.MustGetLogger(\"x-ui\")\n\tbackends := make([]logging.Backend, 0, 2)\n\n\t// Console/syslog backend with configurable level\n\tif consoleBackend := initDefaultBackend(); consoleBackend != nil {\n\t\tleveledBackend := logging.AddModuleLevel(consoleBackend)\n\t\tleveledBackend.SetLevel(level, \"x-ui\")\n\t\tbackends = append(backends, leveledBackend)\n\t}\n\n\t// File backend with DEBUG level for comprehensive logging\n\tif fileBackend := initFileBackend(); fileBackend != nil {\n\t\tleveledBackend := logging.AddModuleLevel(fileBackend)\n\t\tleveledBackend.SetLevel(logging.DEBUG, \"x-ui\")\n\t\tbackends = append(backends, leveledBackend)\n\t}\n\n\tmultiBackend := logging.MultiLogger(backends...)\n\tnewLogger.SetBackend(multiBackend)\n\tlogger = newLogger\n}\n\n// initDefaultBackend creates the console/syslog logging backend.\n// Windows: Uses stderr directly (no syslog support)\n// Unix-like: Attempts syslog, falls back to stderr\nfunc initDefaultBackend() logging.Backend {\n\tvar backend logging.Backend\n\tincludeTime := false\n\n\tif runtime.GOOS == \"windows\" {\n\t\t// Windows: Use stderr directly (no syslog support)\n\t\tbackend = logging.NewLogBackend(os.Stderr, \"\", 0)\n\t\tincludeTime = true\n\t} else {\n\t\t// Unix-like: Try syslog, fallback to stderr\n\t\tif syslogBackend, err := logging.NewSyslogBackend(\"\"); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"syslog backend disabled: %v\\n\", err)\n\t\t\tbackend = logging.NewLogBackend(os.Stderr, \"\", 0)\n\t\t\tincludeTime = os.Getppid() > 0\n\t\t} else {\n\t\t\tbackend = syslogBackend\n\t\t}\n\t}\n\n\treturn logging.NewBackendFormatter(backend, newFormatter(includeTime))\n}\n\n// initFileBackend creates the file logging backend.\n// Creates log directory and truncates log file on startup for fresh logs.\nfunc initFileBackend() logging.Backend {\n\tlogDir := config.GetLogFolder()\n\tif err := os.MkdirAll(logDir, 0o750); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed to create log folder %s: %v\\n\", logDir, err)\n\t\treturn nil\n\t}\n\n\tlogPath := filepath.Join(logDir, logFileName)\n\tfile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed to open log file %s: %v\\n\", logPath, err)\n\t\treturn nil\n\t}\n\n\t// Close previous log file if exists\n\tif logFile != nil {\n\t\t_ = logFile.Close()\n\t}\n\tlogFile = file\n\n\tbackend := logging.NewLogBackend(file, \"\", 0)\n\treturn logging.NewBackendFormatter(backend, newFormatter(true))\n}\n\n// newFormatter creates a log formatter with optional timestamp.\nfunc newFormatter(withTime bool) logging.Formatter {\n\tformat := `%{level} - %{message}`\n\tif withTime {\n\t\tformat = `%{time:` + timeFormat + `} %{level} - %{message}`\n\t}\n\treturn logging.MustStringFormatter(format)\n}\n\n// CloseLogger closes the log file and cleans up resources.\n// Should be called during application shutdown.\nfunc CloseLogger() {\n\tif logFile != nil {\n\t\t_ = logFile.Close()\n\t\tlogFile = nil\n\t}\n}\n\n// Debug logs a debug message and adds it to the log buffer.\nfunc Debug(args ...any) {\n\tlogger.Debug(args...)\n\taddToBuffer(\"DEBUG\", fmt.Sprint(args...))\n}\n\n// Debugf logs a formatted debug message and adds it to the log buffer.\nfunc Debugf(format string, args ...any) {\n\tlogger.Debugf(format, args...)\n\taddToBuffer(\"DEBUG\", fmt.Sprintf(format, args...))\n}\n\n// Info logs an info message and adds it to the log buffer.\nfunc Info(args ...any) {\n\tlogger.Info(args...)\n\taddToBuffer(\"INFO\", fmt.Sprint(args...))\n}\n\n// Infof logs a formatted info message and adds it to the log buffer.\nfunc Infof(format string, args ...any) {\n\tlogger.Infof(format, args...)\n\taddToBuffer(\"INFO\", fmt.Sprintf(format, args...))\n}\n\n// Notice logs a notice message and adds it to the log buffer.\nfunc Notice(args ...any) {\n\tlogger.Notice(args...)\n\taddToBuffer(\"NOTICE\", fmt.Sprint(args...))\n}\n\n// Noticef logs a formatted notice message and adds it to the log buffer.\nfunc Noticef(format string, args ...any) {\n\tlogger.Noticef(format, args...)\n\taddToBuffer(\"NOTICE\", fmt.Sprintf(format, args...))\n}\n\n// Warning logs a warning message and adds it to the log buffer.\nfunc Warning(args ...any) {\n\tlogger.Warning(args...)\n\taddToBuffer(\"WARNING\", fmt.Sprint(args...))\n}\n\n// Warningf logs a formatted warning message and adds it to the log buffer.\nfunc Warningf(format string, args ...any) {\n\tlogger.Warningf(format, args...)\n\taddToBuffer(\"WARNING\", fmt.Sprintf(format, args...))\n}\n\n// Error logs an error message and adds it to the log buffer.\nfunc Error(args ...any) {\n\tlogger.Error(args...)\n\taddToBuffer(\"ERROR\", fmt.Sprint(args...))\n}\n\n// Errorf logs a formatted error message and adds it to the log buffer.\nfunc Errorf(format string, args ...any) {\n\tlogger.Errorf(format, args...)\n\taddToBuffer(\"ERROR\", fmt.Sprintf(format, args...))\n}\n\n// addToBuffer adds a log entry to the in-memory ring buffer for web UI retrieval.\nfunc addToBuffer(level string, newLog string) {\n\tt := time.Now()\n\tif len(logBuffer) >= maxLogBufferSize {\n\t\tlogBuffer = logBuffer[1:]\n\t}\n\n\tlogLevel, _ := logging.LogLevel(level)\n\tlogBuffer = append(logBuffer, struct {\n\t\ttime  string\n\t\tlevel logging.Level\n\t\tlog   string\n\t}{\n\t\ttime:  t.Format(timeFormat),\n\t\tlevel: logLevel,\n\t\tlog:   newLog,\n\t})\n}\n\n// GetLogs retrieves up to c log entries from the buffer that are at or below the specified level.\nfunc GetLogs(c int, level string) []string {\n\tvar output []string\n\tlogLevel, _ := logging.LogLevel(level)\n\n\tfor i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- {\n\t\tif logBuffer[i].level <= logLevel {\n\t\t\toutput = append(output, fmt.Sprintf(\"%s %s - %s\", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log))\n\t\t}\n\t}\n\treturn output\n}\n"
  },
  {
    "path": "main.go",
    "content": "// Package main is the entry point for the 3x-ui web panel application.\n// It initializes the database, web server, and handles command-line operations for managing the panel.\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t_ \"unsafe\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/sub\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/crypto\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/sys\"\n\t\"github.com/mhsanaei/3x-ui/v2/web\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/global\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\n\t\"github.com/joho/godotenv\"\n\t\"github.com/op/go-logging\"\n)\n\n// runWebServer initializes and starts the web server for the 3x-ui panel.\nfunc runWebServer() {\n\tlog.Printf(\"Starting %v %v\", config.GetName(), config.GetVersion())\n\n\tswitch config.GetLogLevel() {\n\tcase config.Debug:\n\t\tlogger.InitLogger(logging.DEBUG)\n\tcase config.Info:\n\t\tlogger.InitLogger(logging.INFO)\n\tcase config.Notice:\n\t\tlogger.InitLogger(logging.NOTICE)\n\tcase config.Warning:\n\t\tlogger.InitLogger(logging.WARNING)\n\tcase config.Error:\n\t\tlogger.InitLogger(logging.ERROR)\n\tdefault:\n\t\tlog.Fatalf(\"Unknown log level: %v\", config.GetLogLevel())\n\t}\n\n\tgodotenv.Load()\n\n\terr := database.InitDB(config.GetDBPath())\n\tif err != nil {\n\t\tlog.Fatalf(\"Error initializing database: %v\", err)\n\t}\n\n\tvar server *web.Server\n\tserver = web.NewServer()\n\tglobal.SetWebServer(server)\n\terr = server.Start()\n\tif err != nil {\n\t\tlog.Fatalf(\"Error starting web server: %v\", err)\n\t\treturn\n\t}\n\n\tvar subServer *sub.Server\n\tsubServer = sub.NewServer()\n\tglobal.SetSubServer(subServer)\n\terr = subServer.Start()\n\tif err != nil {\n\t\tlog.Fatalf(\"Error starting sub server: %v\", err)\n\t\treturn\n\t}\n\n\tsigCh := make(chan os.Signal, 1)\n\t// Trap shutdown signals\n\tsignal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1)\n\tfor {\n\t\tsig := <-sigCh\n\n\t\tswitch sig {\n\t\tcase syscall.SIGHUP:\n\t\t\tlogger.Info(\"Received SIGHUP signal. Restarting servers...\")\n\n\t\t\t// --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart ---\n\t\t\tservice.StopBot()\n\t\t\t// --\n\n\t\t\terr := server.Stop()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Debug(\"Error stopping web server:\", err)\n\t\t\t}\n\t\t\terr = subServer.Stop()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Debug(\"Error stopping sub server:\", err)\n\t\t\t}\n\n\t\t\tserver = web.NewServer()\n\t\t\tglobal.SetWebServer(server)\n\t\t\terr = server.Start()\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Error restarting web server: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Println(\"Web server restarted successfully.\")\n\n\t\t\tsubServer = sub.NewServer()\n\t\t\tglobal.SetSubServer(subServer)\n\t\t\terr = subServer.Start()\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Error restarting sub server: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Println(\"Sub server restarted successfully.\")\n\t\tcase sys.SIGUSR1:\n\t\t\tlogger.Info(\"Received USR1 signal, restarting xray-core...\")\n\t\t\terr := server.RestartXray()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(\"Failed to restart xray-core:\", err)\n\t\t\t}\n\n\t\tdefault:\n\t\t\t// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---\n\t\t\tservice.StopBot()\n\t\t\t// ------------------------------------------------------------\n\n\t\t\tserver.Stop()\n\t\t\tsubServer.Stop()\n\t\t\tlog.Println(\"Shutting down servers.\")\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// resetSetting resets all panel settings to their default values.\nfunc resetSetting() {\n\terr := database.InitDB(config.GetDBPath())\n\tif err != nil {\n\t\tfmt.Println(\"Failed to initialize database:\", err)\n\t\treturn\n\t}\n\n\tsettingService := service.SettingService{}\n\terr = settingService.ResetSettings()\n\tif err != nil {\n\t\tfmt.Println(\"Failed to reset settings:\", err)\n\t} else {\n\t\tfmt.Println(\"Settings successfully reset.\")\n\t}\n}\n\n// showSetting displays the current panel settings if show is true.\nfunc showSetting(show bool) {\n\tif show {\n\t\tsettingService := service.SettingService{}\n\t\tport, err := settingService.GetPort()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"get current port failed, error info:\", err)\n\t\t}\n\n\t\twebBasePath, err := settingService.GetBasePath()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"get webBasePath failed, error info:\", err)\n\t\t}\n\n\t\tcertFile, err := settingService.GetCertFile()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"get cert file failed, error info:\", err)\n\t\t}\n\t\tkeyFile, err := settingService.GetKeyFile()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"get key file failed, error info:\", err)\n\t\t}\n\n\t\tuserService := service.UserService{}\n\t\tuserModel, err := userService.GetFirstUser()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"get current user info failed, error info:\", err)\n\t\t}\n\n\t\tif userModel.Username == \"\" || userModel.Password == \"\" {\n\t\t\tfmt.Println(\"current username or password is empty\")\n\t\t}\n\n\t\tfmt.Println(\"current panel settings as follows:\")\n\t\tif certFile == \"\" || keyFile == \"\" {\n\t\t\tfmt.Println(\"Warning: Panel is not secure with SSL\")\n\t\t} else {\n\t\t\tfmt.Println(\"Panel is secure with SSL\")\n\t\t}\n\n\t\thasDefaultCredential := func() bool {\n\t\t\treturn userModel.Username == \"admin\" && crypto.CheckPasswordHash(userModel.Password, \"admin\")\n\t\t}()\n\n\t\tfmt.Println(\"hasDefaultCredential:\", hasDefaultCredential)\n\t\tfmt.Println(\"port:\", port)\n\t\tfmt.Println(\"webBasePath:\", webBasePath)\n\t}\n}\n\n// updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter.\nfunc updateTgbotEnableSts(status bool) {\n\tsettingService := service.SettingService{}\n\tcurrentTgSts, err := settingService.GetTgbotEnabled()\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn\n\t}\n\tlogger.Infof(\"current enabletgbot status[%v],need update to status[%v]\", currentTgSts, status)\n\tif currentTgSts != status {\n\t\terr := settingService.SetTgbotEnabled(status)\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t} else {\n\t\t\tlogger.Infof(\"SetTgbotEnabled[%v] success\", status)\n\t\t}\n\t}\n}\n\n// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.\nfunc updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {\n\terr := database.InitDB(config.GetDBPath())\n\tif err != nil {\n\t\tfmt.Println(\"Error initializing database:\", err)\n\t\treturn\n\t}\n\n\tsettingService := service.SettingService{}\n\n\tif tgBotToken != \"\" {\n\t\terr := settingService.SetTgBotToken(tgBotToken)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error setting Telegram bot token: %v\\n\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Info(\"Successfully updated Telegram bot token.\")\n\t}\n\n\tif tgBotRuntime != \"\" {\n\t\terr := settingService.SetTgbotRuntime(tgBotRuntime)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error setting Telegram bot runtime: %v\\n\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Infof(\"Successfully updated Telegram bot runtime to [%s].\", tgBotRuntime)\n\t}\n\n\tif tgBotChatid != \"\" {\n\t\terr := settingService.SetTgBotChatId(tgBotChatid)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error setting Telegram bot chat ID: %v\\n\", err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Info(\"Successfully updated Telegram bot chat ID.\")\n\t}\n}\n\n// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.\nfunc updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {\n\terr := database.InitDB(config.GetDBPath())\n\tif err != nil {\n\t\tfmt.Println(\"Database initialization failed:\", err)\n\t\treturn\n\t}\n\n\tsettingService := service.SettingService{}\n\tuserService := service.UserService{}\n\n\tif port > 0 {\n\t\terr := settingService.SetPort(port)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Failed to set port:\", err)\n\t\t} else {\n\t\t\tfmt.Printf(\"Port set successfully: %v\\n\", port)\n\t\t}\n\t}\n\n\tif username != \"\" || password != \"\" {\n\t\terr := userService.UpdateFirstUser(username, password)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Failed to update username and password:\", err)\n\t\t} else {\n\t\t\tfmt.Println(\"Username and password updated successfully\")\n\t\t}\n\t}\n\n\tif webBasePath != \"\" {\n\t\terr := settingService.SetBasePath(webBasePath)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Failed to set base URI path:\", err)\n\t\t} else {\n\t\t\tfmt.Println(\"Base URI path set successfully\")\n\t\t}\n\t}\n\n\tif resetTwoFactor {\n\t\terr := settingService.SetTwoFactorEnable(false)\n\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Failed to reset two-factor authentication:\", err)\n\t\t} else {\n\t\t\tsettingService.SetTwoFactorToken(\"\")\n\t\t\tfmt.Println(\"Two-factor authentication reset successfully\")\n\t\t}\n\t}\n\n\tif listenIP != \"\" {\n\t\terr := settingService.SetListen(listenIP)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Failed to set listen IP:\", err)\n\t\t} else {\n\t\t\tfmt.Printf(\"listen %v set successfully\", listenIP)\n\t\t}\n\t}\n}\n\n// updateCert updates the SSL certificate files for the panel.\nfunc updateCert(publicKey string, privateKey string) {\n\terr := database.InitDB(config.GetDBPath())\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn\n\t}\n\n\tif (privateKey != \"\" && publicKey != \"\") || (privateKey == \"\" && publicKey == \"\") {\n\t\tsettingService := service.SettingService{}\n\t\terr = settingService.SetCertFile(publicKey)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"set certificate public key failed:\", err)\n\t\t} else {\n\t\t\tfmt.Println(\"set certificate public key success\")\n\t\t}\n\n\t\terr = settingService.SetKeyFile(privateKey)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"set certificate private key failed:\", err)\n\t\t} else {\n\t\t\tfmt.Println(\"set certificate private key success\")\n\t\t}\n\n\t\terr = settingService.SetSubCertFile(publicKey)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"set certificate for subscription public key failed:\", err)\n\t\t} else {\n\t\t\tfmt.Println(\"set certificate for subscription public key success\")\n\t\t}\n\n\t\terr = settingService.SetSubKeyFile(privateKey)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"set certificate for subscription private key failed:\", err)\n\t\t} else {\n\t\t\tfmt.Println(\"set certificate for subscription private key success\")\n\t\t}\n\t} else {\n\t\tfmt.Println(\"both public and private key should be entered.\")\n\t}\n}\n\n// GetCertificate displays the current SSL certificate settings if getCert is true.\nfunc GetCertificate(getCert bool) {\n\tif getCert {\n\t\tsettingService := service.SettingService{}\n\t\tcertFile, err := settingService.GetCertFile()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"get cert file failed, error info:\", err)\n\t\t}\n\t\tkeyFile, err := settingService.GetKeyFile()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"get key file failed, error info:\", err)\n\t\t}\n\n\t\tfmt.Println(\"cert:\", certFile)\n\t\tfmt.Println(\"key:\", keyFile)\n\t}\n}\n\n// GetListenIP displays the current panel listen IP address if getListen is true.\nfunc GetListenIP(getListen bool) {\n\tif getListen {\n\n\t\tsettingService := service.SettingService{}\n\t\tListenIP, err := settingService.GetListen()\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Failed to retrieve listen IP: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(\"listenIP:\", ListenIP)\n\t}\n}\n\n// migrateDb performs database migration operations for the 3x-ui panel.\nfunc migrateDb() {\n\tinboundService := service.InboundService{}\n\n\terr := database.InitDB(config.GetDBPath())\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfmt.Println(\"Start migrating database...\")\n\tinboundService.MigrateDB()\n\tfmt.Println(\"Migration done!\")\n}\n\n// main is the entry point of the 3x-ui application.\n// It parses command-line arguments to run the web server, migrate database, or update settings.\nfunc main() {\n\tif len(os.Args) < 2 {\n\t\trunWebServer()\n\t\treturn\n\t}\n\n\tvar showVersion bool\n\tflag.BoolVar(&showVersion, \"v\", false, \"show version\")\n\n\trunCmd := flag.NewFlagSet(\"run\", flag.ExitOnError)\n\n\tsettingCmd := flag.NewFlagSet(\"setting\", flag.ExitOnError)\n\tvar port int\n\tvar username string\n\tvar password string\n\tvar webBasePath string\n\tvar listenIP string\n\tvar getListen bool\n\tvar webCertFile string\n\tvar webKeyFile string\n\tvar tgbottoken string\n\tvar tgbotchatid string\n\tvar enabletgbot bool\n\tvar tgbotRuntime string\n\tvar reset bool\n\tvar show bool\n\tvar getCert bool\n\tvar resetTwoFactor bool\n\tsettingCmd.BoolVar(&reset, \"reset\", false, \"Reset all settings\")\n\tsettingCmd.BoolVar(&show, \"show\", false, \"Display current settings\")\n\tsettingCmd.IntVar(&port, \"port\", 0, \"Set panel port number\")\n\tsettingCmd.StringVar(&username, \"username\", \"\", \"Set login username\")\n\tsettingCmd.StringVar(&password, \"password\", \"\", \"Set login password\")\n\tsettingCmd.StringVar(&webBasePath, \"webBasePath\", \"\", \"Set base path for Panel\")\n\tsettingCmd.StringVar(&listenIP, \"listenIP\", \"\", \"set panel listenIP IP\")\n\tsettingCmd.BoolVar(&resetTwoFactor, \"resetTwoFactor\", false, \"Reset two-factor authentication settings\")\n\tsettingCmd.BoolVar(&getListen, \"getListen\", false, \"Display current panel listenIP IP\")\n\tsettingCmd.BoolVar(&getCert, \"getCert\", false, \"Display current certificate settings\")\n\tsettingCmd.StringVar(&webCertFile, \"webCert\", \"\", \"Set path to public key file for panel\")\n\tsettingCmd.StringVar(&webKeyFile, \"webCertKey\", \"\", \"Set path to private key file for panel\")\n\tsettingCmd.StringVar(&tgbottoken, \"tgbottoken\", \"\", \"Set token for Telegram bot\")\n\tsettingCmd.StringVar(&tgbotRuntime, \"tgbotRuntime\", \"\", \"Set cron time for Telegram bot notifications\")\n\tsettingCmd.StringVar(&tgbotchatid, \"tgbotchatid\", \"\", \"Set chat ID for Telegram bot notifications\")\n\tsettingCmd.BoolVar(&enabletgbot, \"enabletgbot\", false, \"Enable notifications via Telegram bot\")\n\n\toldUsage := flag.Usage\n\tflag.Usage = func() {\n\t\toldUsage()\n\t\tfmt.Println()\n\t\tfmt.Println(\"Commands:\")\n\t\tfmt.Println(\"    run            run web panel\")\n\t\tfmt.Println(\"    migrate        migrate form other/old x-ui\")\n\t\tfmt.Println(\"    setting        set settings\")\n\t}\n\n\tflag.Parse()\n\tif showVersion {\n\t\tfmt.Println(config.GetVersion())\n\t\treturn\n\t}\n\n\tswitch os.Args[1] {\n\tcase \"run\":\n\t\terr := runCmd.Parse(os.Args[2:])\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\t\trunWebServer()\n\tcase \"migrate\":\n\t\tmigrateDb()\n\tcase \"setting\":\n\t\terr := settingCmd.Parse(os.Args[2:])\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\t\tif reset {\n\t\t\tresetSetting()\n\t\t} else {\n\t\t\tupdateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor)\n\t\t}\n\t\tif show {\n\t\t\tshowSetting(show)\n\t\t}\n\t\tif getListen {\n\t\t\tGetListenIP(getListen)\n\t\t}\n\t\tif getCert {\n\t\t\tGetCertificate(getCert)\n\t\t}\n\t\tif (tgbottoken != \"\") || (tgbotchatid != \"\") || (tgbotRuntime != \"\") {\n\t\t\tupdateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)\n\t\t}\n\t\tif enabletgbot {\n\t\t\tupdateTgbotEnableSts(enabletgbot)\n\t\t}\n\tcase \"cert\":\n\t\terr := settingCmd.Parse(os.Args[2:])\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\treturn\n\t\t}\n\t\tif reset {\n\t\t\tupdateCert(\"\", \"\")\n\t\t} else {\n\t\t\tupdateCert(webCertFile, webKeyFile)\n\t\t}\n\tdefault:\n\t\tfmt.Println(\"Invalid subcommands\")\n\t\tfmt.Println()\n\t\trunCmd.Usage()\n\t\tfmt.Println()\n\t\tsettingCmd.Usage()\n\t}\n}\n"
  },
  {
    "path": "sub/default.json",
    "content": "{\n  \"remarks\": \"\",\n  \"dns\": {\n    \"tag\": \"dns_out\",\n    \"queryStrategy\": \"UseIP\",\n    \"servers\": [\n      {\n        \"address\": \"8.8.8.8\",\n        \"skipFallback\": false\n      }\n    ]\n  },\n  \"inbounds\": [\n    {\n      \"port\": 10808,\n      \"protocol\": \"mixed\",\n      \"settings\": {\n        \"auth\": \"noauth\",\n        \"udp\": true,\n        \"userLevel\": 8\n      },\n      \"sniffing\": {\n        \"destOverride\": [\n          \"http\",\n          \"tls\",\n          \"quic\",\n          \"fakedns\"\n        ],\n        \"enabled\": true\n      },\n      \"tag\": \"mixed\"\n    },\n    {\n      \"port\": 10809,\n      \"protocol\": \"http\",\n      \"settings\": {\n        \"userLevel\": 8\n      },\n      \"tag\": \"http\"\n    }\n  ],\n  \"log\": {\n    \"loglevel\": \"warning\"\n  },\n  \"outbounds\": [\n    {\n      \"tag\": \"direct\",\n      \"protocol\": \"freedom\",\n      \"settings\": {\n        \"domainStrategy\": \"AsIs\",\n        \"redirect\": \"\",\n        \"noises\": []\n      }\n    },\n    {\n      \"tag\": \"block\",\n      \"protocol\": \"blackhole\",\n      \"settings\": {\n        \"response\": {\n          \"type\": \"http\"\n        }\n      }\n    }\n  ],\n  \"policy\": {\n    \"levels\": {\n      \"8\": {\n        \"connIdle\": 300,\n        \"downlinkOnly\": 1,\n        \"handshake\": 4,\n        \"uplinkOnly\": 1\n      }\n    },\n    \"system\": {\n      \"statsOutboundUplink\": true,\n      \"statsOutboundDownlink\": true\n    }\n  },\n  \"routing\": {\n    \"domainStrategy\": \"AsIs\",\n    \"rules\": [\n      {\n        \"type\": \"field\",\n        \"network\": \"tcp,udp\",\n        \"outboundTag\": \"proxy\"\n      }\n    ]\n  },\n  \"stats\": {}\n}"
  },
  {
    "path": "sub/sub.go",
    "content": "// Package sub provides subscription server functionality for the 3x-ui panel,\n// including HTTP/HTTPS servers for serving subscription links and JSON configurations.\npackage sub\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"html/template\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\twebpkg \"github.com/mhsanaei/3x-ui/v2/web\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/locale\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/middleware\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/network\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// setEmbeddedTemplates parses and sets embedded templates on the engine\nfunc setEmbeddedTemplates(engine *gin.Engine) error {\n\tt, err := template.New(\"\").Funcs(engine.FuncMap).ParseFS(\n\t\twebpkg.EmbeddedHTML(),\n\t\t\"html/common/page.html\",\n\t\t\"html/component/aThemeSwitch.html\",\n\t\t\"html/settings/panel/subscription/subpage.html\",\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tengine.SetHTMLTemplate(t)\n\treturn nil\n}\n\n// Server represents the subscription server that serves subscription links and JSON configurations.\ntype Server struct {\n\thttpServer *http.Server\n\tlistener   net.Listener\n\n\tsub            *SUBController\n\tsettingService service.SettingService\n\n\tctx    context.Context\n\tcancel context.CancelFunc\n}\n\n// NewServer creates a new subscription server instance with a cancellable context.\nfunc NewServer() *Server {\n\tctx, cancel := context.WithCancel(context.Background())\n\treturn &Server{\n\t\tctx:    ctx,\n\t\tcancel: cancel,\n\t}\n}\n\n// initRouter configures the subscription server's Gin engine, middleware,\n// templates and static assets and returns the ready-to-use engine.\nfunc (s *Server) initRouter() (*gin.Engine, error) {\n\t// Always run in release mode for the subscription server\n\tgin.DefaultWriter = io.Discard\n\tgin.DefaultErrorWriter = io.Discard\n\tgin.SetMode(gin.ReleaseMode)\n\n\tengine := gin.Default()\n\n\tsubDomain, err := s.settingService.GetSubDomain()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif subDomain != \"\" {\n\t\tengine.Use(middleware.DomainValidatorMiddleware(subDomain))\n\t}\n\n\tLinksPath, err := s.settingService.GetSubPath()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tJsonPath, err := s.settingService.GetSubJsonPath()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Determine if JSON subscription endpoint is enabled\n\tsubJsonEnable, err := s.settingService.GetSubJsonEnable()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set base_path based on LinksPath for template rendering\n\t// Ensure LinksPath ends with \"/\" for proper asset URL generation\n\tbasePath := LinksPath\n\tif basePath != \"/\" && !strings.HasSuffix(basePath, \"/\") {\n\t\tbasePath += \"/\"\n\t}\n\t// logger.Debug(\"sub: Setting base_path to:\", basePath)\n\tengine.Use(func(c *gin.Context) {\n\t\tc.Set(\"base_path\", basePath)\n\t})\n\n\tEncrypt, err := s.settingService.GetSubEncrypt()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tShowInfo, err := s.settingService.GetSubShowInfo()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tRemarkModel, err := s.settingService.GetRemarkModel()\n\tif err != nil {\n\t\tRemarkModel = \"-ieo\"\n\t}\n\n\tSubUpdates, err := s.settingService.GetSubUpdates()\n\tif err != nil {\n\t\tSubUpdates = \"10\"\n\t}\n\n\tSubJsonFragment, err := s.settingService.GetSubJsonFragment()\n\tif err != nil {\n\t\tSubJsonFragment = \"\"\n\t}\n\n\tSubJsonNoises, err := s.settingService.GetSubJsonNoises()\n\tif err != nil {\n\t\tSubJsonNoises = \"\"\n\t}\n\n\tSubJsonMux, err := s.settingService.GetSubJsonMux()\n\tif err != nil {\n\t\tSubJsonMux = \"\"\n\t}\n\n\tSubJsonRules, err := s.settingService.GetSubJsonRules()\n\tif err != nil {\n\t\tSubJsonRules = \"\"\n\t}\n\n\tSubTitle, err := s.settingService.GetSubTitle()\n\tif err != nil {\n\t\tSubTitle = \"\"\n\t}\n\n\tSubSupportUrl, err := s.settingService.GetSubSupportUrl()\n\tif err != nil {\n\t\tSubSupportUrl = \"\"\n\t}\n\n\tSubProfileUrl, err := s.settingService.GetSubProfileUrl()\n\tif err != nil {\n\t\tSubProfileUrl = \"\"\n\t}\n\n\tSubAnnounce, err := s.settingService.GetSubAnnounce()\n\tif err != nil {\n\t\tSubAnnounce = \"\"\n\t}\n\n\tSubEnableRouting, err := s.settingService.GetSubEnableRouting()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tSubRoutingRules, err := s.settingService.GetSubRoutingRules()\n\tif err != nil {\n\t\tSubRoutingRules = \"\"\n\t}\n\n\t// set per-request localizer from headers/cookies\n\tengine.Use(locale.LocalizerMiddleware())\n\n\t// register i18n function similar to web server\n\ti18nWebFunc := func(key string, params ...string) string {\n\t\treturn locale.I18n(locale.Web, key, params...)\n\t}\n\tengine.SetFuncMap(map[string]any{\"i18n\": i18nWebFunc})\n\n\t// Templates: prefer embedded; fallback to disk if necessary\n\tif err := setEmbeddedTemplates(engine); err != nil {\n\t\tlogger.Warning(\"sub: failed to parse embedded templates:\", err)\n\t\tif files, derr := s.getHtmlFiles(); derr == nil {\n\t\t\tengine.LoadHTMLFiles(files...)\n\t\t} else {\n\t\t\tlogger.Error(\"sub: no templates available (embedded parse and disk load failed)\", err, derr)\n\t\t}\n\t}\n\n\t// Assets: use disk if present, fallback to embedded\n\t// Serve under both root (/assets) and under the subscription path prefix (LinksPath + \"assets\")\n\t// so reverse proxies with a URI prefix can load assets correctly.\n\t// Determine LinksPath earlier to compute prefixed assets mount.\n\t// Note: LinksPath always starts and ends with \"/\" (validated in settings).\n\tvar linksPathForAssets string\n\tif LinksPath == \"/\" {\n\t\tlinksPathForAssets = \"/assets\"\n\t} else {\n\t\t// ensure single slash join\n\t\tlinksPathForAssets = strings.TrimRight(LinksPath, \"/\") + \"/assets\"\n\t}\n\n\t// Mount assets in multiple paths to handle different URL patterns\n\tvar assetsFS http.FileSystem\n\tif _, err := os.Stat(\"web/assets\"); err == nil {\n\t\tassetsFS = http.FS(os.DirFS(\"web/assets\"))\n\t} else {\n\t\tif subFS, err := fs.Sub(webpkg.EmbeddedAssets(), \"assets\"); err == nil {\n\t\t\tassetsFS = http.FS(subFS)\n\t\t} else {\n\t\t\tlogger.Error(\"sub: failed to mount embedded assets:\", err)\n\t\t}\n\t}\n\n\tif assetsFS != nil {\n\t\tengine.StaticFS(\"/assets\", assetsFS)\n\t\tif linksPathForAssets != \"/assets\" {\n\t\t\tengine.StaticFS(linksPathForAssets, assetsFS)\n\t\t}\n\n\t\t// Add middleware to handle dynamic asset paths with subid\n\t\tif LinksPath != \"/\" {\n\t\t\tengine.Use(func(c *gin.Context) {\n\t\t\t\tpath := c.Request.URL.Path\n\t\t\t\t// Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...\n\t\t\t\tpathPrefix := strings.TrimRight(LinksPath, \"/\") + \"/\"\n\t\t\t\tif strings.HasPrefix(path, pathPrefix) && strings.Contains(path, \"/assets/\") {\n\t\t\t\t\t// Extract the asset path after /assets/\n\t\t\t\t\tassetsIndex := strings.Index(path, \"/assets/\")\n\t\t\t\t\tif assetsIndex != -1 {\n\t\t\t\t\t\tassetPath := path[assetsIndex+8:] // +8 to skip \"/assets/\"\n\t\t\t\t\t\tif assetPath != \"\" {\n\t\t\t\t\t\t\t// Serve the asset file\n\t\t\t\t\t\t\tc.FileFromFS(assetPath, assetsFS)\n\t\t\t\t\t\t\tc.Abort()\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tc.Next()\n\t\t\t})\n\t\t}\n\t}\n\n\tg := engine.Group(\"/\")\n\n\ts.sub = NewSUBController(\n\t\tg, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,\n\t\tSubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,\n\t\tSubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)\n\n\treturn engine, nil\n}\n\n// getHtmlFiles loads templates from local folder (used in debug mode)\nfunc (s *Server) getHtmlFiles() ([]string, error) {\n\tdir, _ := os.Getwd()\n\tfiles := []string{}\n\t// common layout\n\tcommon := filepath.Join(dir, \"web\", \"html\", \"common\", \"page.html\")\n\tif _, err := os.Stat(common); err == nil {\n\t\tfiles = append(files, common)\n\t}\n\t// components used\n\ttheme := filepath.Join(dir, \"web\", \"html\", \"component\", \"aThemeSwitch.html\")\n\tif _, err := os.Stat(theme); err == nil {\n\t\tfiles = append(files, theme)\n\t}\n\t// page itself\n\tpage := filepath.Join(dir, \"web\", \"html\", \"subpage.html\")\n\tif _, err := os.Stat(page); err == nil {\n\t\tfiles = append(files, page)\n\t} else {\n\t\treturn nil, err\n\t}\n\treturn files, nil\n}\n\n// Start initializes and starts the subscription server with configured settings.\nfunc (s *Server) Start() (err error) {\n\t// This is an anonymous function, no function name\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ts.Stop()\n\t\t}\n\t}()\n\n\tsubEnable, err := s.settingService.GetSubEnable()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !subEnable {\n\t\treturn nil\n\t}\n\n\tengine, err := s.initRouter()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcertFile, err := s.settingService.GetSubCertFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\tkeyFile, err := s.settingService.GetSubKeyFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\tlisten, err := s.settingService.GetSubListen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tport, err := s.settingService.GetSubPort()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlistenAddr := net.JoinHostPort(listen, strconv.Itoa(port))\n\tlistener, err := net.Listen(\"tcp\", listenAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif certFile != \"\" || keyFile != \"\" {\n\t\tcert, err := tls.LoadX509KeyPair(certFile, keyFile)\n\t\tif err == nil {\n\t\t\tc := &tls.Config{\n\t\t\t\tCertificates: []tls.Certificate{cert},\n\t\t\t}\n\t\t\tlistener = network.NewAutoHttpsListener(listener)\n\t\t\tlistener = tls.NewListener(listener, c)\n\t\t\tlogger.Info(\"Sub server running HTTPS on\", listener.Addr())\n\t\t} else {\n\t\t\tlogger.Error(\"Error loading certificates:\", err)\n\t\t\tlogger.Info(\"Sub server running HTTP on\", listener.Addr())\n\t\t}\n\t} else {\n\t\tlogger.Info(\"Sub server running HTTP on\", listener.Addr())\n\t}\n\ts.listener = listener\n\n\ts.httpServer = &http.Server{\n\t\tHandler: engine,\n\t}\n\n\tgo func() {\n\t\ts.httpServer.Serve(listener)\n\t}()\n\n\treturn nil\n}\n\n// Stop gracefully shuts down the subscription server and closes the listener.\nfunc (s *Server) Stop() error {\n\ts.cancel()\n\n\tvar err1 error\n\tvar err2 error\n\tif s.httpServer != nil {\n\t\terr1 = s.httpServer.Shutdown(s.ctx)\n\t}\n\tif s.listener != nil {\n\t\terr2 = s.listener.Close()\n\t}\n\treturn common.Combine(err1, err2)\n}\n\n// GetCtx returns the server's context for cancellation and deadline management.\nfunc (s *Server) GetCtx() context.Context {\n\treturn s.ctx\n}\n"
  },
  {
    "path": "sub/subController.go",
    "content": "package sub\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// SUBController handles HTTP requests for subscription links and JSON configurations.\ntype SUBController struct {\n\tsubTitle         string\n\tsubSupportUrl    string\n\tsubProfileUrl    string\n\tsubAnnounce      string\n\tsubEnableRouting bool\n\tsubRoutingRules  string\n\tsubPath          string\n\tsubJsonPath      string\n\tjsonEnabled      bool\n\tsubEncrypt       bool\n\tupdateInterval   string\n\n\tsubService     *SubService\n\tsubJsonService *SubJsonService\n}\n\n// NewSUBController creates a new subscription controller with the given configuration.\nfunc NewSUBController(\n\tg *gin.RouterGroup,\n\tsubPath string,\n\tjsonPath string,\n\tjsonEnabled bool,\n\tencrypt bool,\n\tshowInfo bool,\n\trModel string,\n\tupdate string,\n\tjsonFragment string,\n\tjsonNoise string,\n\tjsonMux string,\n\tjsonRules string,\n\tsubTitle string,\n\tsubSupportUrl string,\n\tsubProfileUrl string,\n\tsubAnnounce string,\n\tsubEnableRouting bool,\n\tsubRoutingRules string,\n) *SUBController {\n\tsub := NewSubService(showInfo, rModel)\n\ta := &SUBController{\n\t\tsubTitle:         subTitle,\n\t\tsubSupportUrl:    subSupportUrl,\n\t\tsubProfileUrl:    subProfileUrl,\n\t\tsubAnnounce:      subAnnounce,\n\t\tsubEnableRouting: subEnableRouting,\n\t\tsubRoutingRules:  subRoutingRules,\n\t\tsubPath:          subPath,\n\t\tsubJsonPath:      jsonPath,\n\t\tjsonEnabled:      jsonEnabled,\n\t\tsubEncrypt:       encrypt,\n\t\tupdateInterval:   update,\n\n\t\tsubService:     sub,\n\t\tsubJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),\n\t}\n\ta.initRouter(g)\n\treturn a\n}\n\n// initRouter registers HTTP routes for subscription links and JSON endpoints\n// on the provided router group.\nfunc (a *SUBController) initRouter(g *gin.RouterGroup) {\n\tgLink := g.Group(a.subPath)\n\tgLink.GET(\":subid\", a.subs)\n\tif a.jsonEnabled {\n\t\tgJson := g.Group(a.subJsonPath)\n\t\tgJson.GET(\":subid\", a.subJsons)\n\t}\n}\n\n// subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data.\nfunc (a *SUBController) subs(c *gin.Context) {\n\tsubId := c.Param(\"subid\")\n\tscheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)\n\tsubs, lastOnline, traffic, err := a.subService.GetSubs(subId, host)\n\tif err != nil || len(subs) == 0 {\n\t\tc.String(400, \"Error!\")\n\t} else {\n\t\tresult := \"\"\n\t\tfor _, sub := range subs {\n\t\t\tresult += sub + \"\\n\"\n\t\t}\n\n\t\t// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here\n\t\taccept := c.GetHeader(\"Accept\")\n\t\tif strings.Contains(strings.ToLower(accept), \"text/html\") || c.Query(\"html\") == \"1\" || strings.EqualFold(c.Query(\"view\"), \"html\") {\n\t\t\t// Build page data in service\n\t\t\tsubURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)\n\t\t\tif !a.jsonEnabled {\n\t\t\t\tsubJsonURL = \"\"\n\t\t\t}\n\t\t\t// Get base_path from context (set by middleware)\n\t\t\tbasePath, exists := c.Get(\"base_path\")\n\t\t\tif !exists {\n\t\t\t\tbasePath = \"/\"\n\t\t\t}\n\t\t\t// Add subId to base_path for asset URLs\n\t\t\tbasePathStr := basePath.(string)\n\t\t\tif basePathStr == \"/\" {\n\t\t\t\tbasePathStr = \"/\" + subId + \"/\"\n\t\t\t} else {\n\t\t\t\t// Remove trailing slash if exists, add subId, then add trailing slash\n\t\t\t\tbasePathStr = strings.TrimRight(basePathStr, \"/\") + \"/\" + subId + \"/\"\n\t\t\t}\n\t\t\tpage := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)\n\t\t\tc.HTML(200, \"subpage.html\", gin.H{\n\t\t\t\t\"title\":        \"subscription.title\",\n\t\t\t\t\"cur_ver\":      config.GetVersion(),\n\t\t\t\t\"host\":         page.Host,\n\t\t\t\t\"base_path\":    page.BasePath,\n\t\t\t\t\"sId\":          page.SId,\n\t\t\t\t\"download\":     page.Download,\n\t\t\t\t\"upload\":       page.Upload,\n\t\t\t\t\"total\":        page.Total,\n\t\t\t\t\"used\":         page.Used,\n\t\t\t\t\"remained\":     page.Remained,\n\t\t\t\t\"expire\":       page.Expire,\n\t\t\t\t\"lastOnline\":   page.LastOnline,\n\t\t\t\t\"datepicker\":   page.Datepicker,\n\t\t\t\t\"downloadByte\": page.DownloadByte,\n\t\t\t\t\"uploadByte\":   page.UploadByte,\n\t\t\t\t\"totalByte\":    page.TotalByte,\n\t\t\t\t\"subUrl\":       page.SubUrl,\n\t\t\t\t\"subJsonUrl\":   page.SubJsonUrl,\n\t\t\t\t\"result\":       page.Result,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\t// Add headers\n\t\theader := fmt.Sprintf(\"upload=%d; download=%d; total=%d; expire=%d\", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)\n\t\tprofileUrl := a.subProfileUrl\n\t\tif profileUrl == \"\" {\n\t\t\tprofileUrl = fmt.Sprintf(\"%s://%s%s\", scheme, hostWithPort, c.Request.RequestURI)\n\t\t}\n\t\ta.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)\n\n\t\tif a.subEncrypt {\n\t\t\tc.String(200, base64.StdEncoding.EncodeToString([]byte(result)))\n\t\t} else {\n\t\t\tc.String(200, result)\n\t\t}\n\t}\n}\n\n// subJsons handles HTTP requests for JSON subscription configurations.\nfunc (a *SUBController) subJsons(c *gin.Context) {\n\tsubId := c.Param(\"subid\")\n\tscheme, host, hostWithPort, _ := a.subService.ResolveRequest(c)\n\tjsonSub, header, err := a.subJsonService.GetJson(subId, host)\n\tif err != nil || len(jsonSub) == 0 {\n\t\tc.String(400, \"Error!\")\n\t} else {\n\t\t// Add headers\n\t\tprofileUrl := a.subProfileUrl\n\t\tif profileUrl == \"\" {\n\t\t\tprofileUrl = fmt.Sprintf(\"%s://%s%s\", scheme, hostWithPort, c.Request.RequestURI)\n\t\t}\n\t\ta.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)\n\n\t\tc.String(200, jsonSub)\n\t}\n}\n\n// ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.\nfunc (a *SUBController) ApplyCommonHeaders(\n\tc *gin.Context,\n\theader,\n\tupdateInterval,\n\tprofileTitle string,\n\tprofileSupportUrl string,\n\tprofileUrl string,\n\tprofileAnnounce string,\n\tprofileEnableRouting bool,\n\tprofileRoutingRules string,\n) {\n\tc.Writer.Header().Set(\"Subscription-Userinfo\", header)\n\tc.Writer.Header().Set(\"Profile-Update-Interval\", updateInterval)\n\n\t//Basics\n\tif profileTitle != \"\" {\n\t\tc.Writer.Header().Set(\"Profile-Title\", \"base64:\"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))\n\t}\n\tif profileSupportUrl != \"\" {\n\t\tc.Writer.Header().Set(\"Support-Url\", profileSupportUrl)\n\t}\n\tif profileUrl != \"\" {\n\t\tc.Writer.Header().Set(\"Profile-Web-Page-Url\", profileUrl)\n\t}\n\tif profileAnnounce != \"\" {\n\t\tc.Writer.Header().Set(\"Announce\", \"base64:\"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce)))\n\t}\n\n\t//Advanced (Happ)\n\tc.Writer.Header().Set(\"Routing-Enable\", strconv.FormatBool(profileEnableRouting))\n\tif profileRoutingRules != \"\" {\n\t\tc.Writer.Header().Set(\"Routing\", profileRoutingRules)\n\t}\n}\n"
  },
  {
    "path": "sub/subJsonService.go",
    "content": "package sub\n\nimport (\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"maps\"\n\t\"strings\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/json_util\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/random\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n)\n\n//go:embed default.json\nvar defaultJson string\n\n// SubJsonService handles JSON subscription configuration generation and management.\ntype SubJsonService struct {\n\tconfigJson       map[string]any\n\tdefaultOutbounds []json_util.RawMessage\n\tfragment         string\n\tnoises           string\n\tmux              string\n\n\tinboundService service.InboundService\n\tSubService     *SubService\n}\n\n// NewSubJsonService creates a new JSON subscription service with the given configuration.\nfunc NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {\n\tvar configJson map[string]any\n\tvar defaultOutbounds []json_util.RawMessage\n\tjson.Unmarshal([]byte(defaultJson), &configJson)\n\tif outboundSlices, ok := configJson[\"outbounds\"].([]any); ok {\n\t\tfor _, defaultOutbound := range outboundSlices {\n\t\t\tjsonBytes, _ := json.Marshal(defaultOutbound)\n\t\t\tdefaultOutbounds = append(defaultOutbounds, jsonBytes)\n\t\t}\n\t}\n\n\tif rules != \"\" {\n\t\tvar newRules []any\n\t\trouting, _ := configJson[\"routing\"].(map[string]any)\n\t\tdefaultRules, _ := routing[\"rules\"].([]any)\n\t\tjson.Unmarshal([]byte(rules), &newRules)\n\t\tdefaultRules = append(newRules, defaultRules...)\n\t\trouting[\"rules\"] = defaultRules\n\t\tconfigJson[\"routing\"] = routing\n\t}\n\n\tif fragment != \"\" {\n\t\tdefaultOutbounds = append(defaultOutbounds, json_util.RawMessage(fragment))\n\t}\n\n\tif noises != \"\" {\n\t\tdefaultOutbounds = append(defaultOutbounds, json_util.RawMessage(noises))\n\t}\n\n\treturn &SubJsonService{\n\t\tconfigJson:       configJson,\n\t\tdefaultOutbounds: defaultOutbounds,\n\t\tfragment:         fragment,\n\t\tnoises:           noises,\n\t\tmux:              mux,\n\t\tSubService:       subService,\n\t}\n}\n\n// GetJson generates a JSON subscription configuration for the given subscription ID and host.\nfunc (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {\n\tinbounds, err := s.SubService.getInboundsBySubId(subId)\n\tif err != nil || len(inbounds) == 0 {\n\t\treturn \"\", \"\", err\n\t}\n\n\tvar header string\n\tvar traffic xray.ClientTraffic\n\tvar clientTraffics []xray.ClientTraffic\n\tvar configArray []json_util.RawMessage\n\n\t// Prepare Inbounds\n\tfor _, inbound := range inbounds {\n\t\tclients, err := s.inboundService.GetClients(inbound)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"SubJsonService - GetClients: Unable to get clients from inbound\")\n\t\t}\n\t\tif clients == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {\n\t\t\tlisten, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings)\n\t\t\tif err == nil {\n\t\t\t\tinbound.Listen = listen\n\t\t\t\tinbound.Port = port\n\t\t\t\tinbound.StreamSettings = streamSettings\n\t\t\t}\n\t\t}\n\n\t\tfor _, client := range clients {\n\t\t\tif client.Enable && client.SubID == subId {\n\t\t\t\tclientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))\n\t\t\t\tnewConfigs := s.getConfig(inbound, client, host)\n\t\t\t\tconfigArray = append(configArray, newConfigs...)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(configArray) == 0 {\n\t\treturn \"\", \"\", nil\n\t}\n\n\t// Prepare statistics\n\tfor index, clientTraffic := range clientTraffics {\n\t\tif index == 0 {\n\t\t\ttraffic.Up = clientTraffic.Up\n\t\t\ttraffic.Down = clientTraffic.Down\n\t\t\ttraffic.Total = clientTraffic.Total\n\t\t\tif clientTraffic.ExpiryTime > 0 {\n\t\t\t\ttraffic.ExpiryTime = clientTraffic.ExpiryTime\n\t\t\t}\n\t\t} else {\n\t\t\ttraffic.Up += clientTraffic.Up\n\t\t\ttraffic.Down += clientTraffic.Down\n\t\t\tif traffic.Total == 0 || clientTraffic.Total == 0 {\n\t\t\t\ttraffic.Total = 0\n\t\t\t} else {\n\t\t\t\ttraffic.Total += clientTraffic.Total\n\t\t\t}\n\t\t\tif clientTraffic.ExpiryTime != traffic.ExpiryTime {\n\t\t\t\ttraffic.ExpiryTime = 0\n\t\t\t}\n\t\t}\n\t}\n\n\t// Combile outbounds\n\tvar finalJson []byte\n\tif len(configArray) == 1 {\n\t\tfinalJson, _ = json.MarshalIndent(configArray[0], \"\", \"  \")\n\t} else {\n\t\tfinalJson, _ = json.MarshalIndent(configArray, \"\", \"  \")\n\t}\n\n\theader = fmt.Sprintf(\"upload=%d; download=%d; total=%d; expire=%d\", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)\n\treturn string(finalJson), header, nil\n}\n\nfunc (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client, host string) []json_util.RawMessage {\n\tvar newJsonArray []json_util.RawMessage\n\tstream := s.streamData(inbound.StreamSettings)\n\n\texternalProxies, ok := stream[\"externalProxy\"].([]any)\n\tif !ok || len(externalProxies) == 0 {\n\t\texternalProxies = []any{\n\t\t\tmap[string]any{\n\t\t\t\t\"forceTls\": \"same\",\n\t\t\t\t\"dest\":     host,\n\t\t\t\t\"port\":     float64(inbound.Port),\n\t\t\t\t\"remark\":   \"\",\n\t\t\t},\n\t\t}\n\t}\n\n\tdelete(stream, \"externalProxy\")\n\n\tfor _, ep := range externalProxies {\n\t\textPrxy := ep.(map[string]any)\n\t\tinbound.Listen = extPrxy[\"dest\"].(string)\n\t\tinbound.Port = int(extPrxy[\"port\"].(float64))\n\t\tnewStream := stream\n\t\tswitch extPrxy[\"forceTls\"].(string) {\n\t\tcase \"tls\":\n\t\t\tif newStream[\"security\"] != \"tls\" {\n\t\t\t\tnewStream[\"security\"] = \"tls\"\n\t\t\t\tnewStream[\"tlsSettings\"] = map[string]any{}\n\t\t\t}\n\t\tcase \"none\":\n\t\t\tif newStream[\"security\"] != \"none\" {\n\t\t\t\tnewStream[\"security\"] = \"none\"\n\t\t\t\tdelete(newStream, \"tlsSettings\")\n\t\t\t}\n\t\t}\n\t\tstreamSettings, _ := json.MarshalIndent(newStream, \"\", \"  \")\n\n\t\tvar newOutbounds []json_util.RawMessage\n\n\t\tswitch inbound.Protocol {\n\t\tcase \"vmess\":\n\t\t\tnewOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))\n\t\tcase \"vless\":\n\t\t\tnewOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))\n\t\tcase \"trojan\", \"shadowsocks\":\n\t\t\tnewOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))\n\t\t}\n\n\t\tnewOutbounds = append(newOutbounds, s.defaultOutbounds...)\n\t\tnewConfigJson := make(map[string]any)\n\t\tmaps.Copy(newConfigJson, s.configJson)\n\n\t\tnewConfigJson[\"outbounds\"] = newOutbounds\n\t\tnewConfigJson[\"remarks\"] = s.SubService.genRemark(inbound, client.Email, extPrxy[\"remark\"].(string))\n\n\t\tnewConfig, _ := json.MarshalIndent(newConfigJson, \"\", \"  \")\n\t\tnewJsonArray = append(newJsonArray, newConfig)\n\t}\n\n\treturn newJsonArray\n}\n\nfunc (s *SubJsonService) streamData(stream string) map[string]any {\n\tvar streamSettings map[string]any\n\tjson.Unmarshal([]byte(stream), &streamSettings)\n\tsecurity, _ := streamSettings[\"security\"].(string)\n\tswitch security {\n\tcase \"tls\":\n\t\tstreamSettings[\"tlsSettings\"] = s.tlsData(streamSettings[\"tlsSettings\"].(map[string]any))\n\tcase \"reality\":\n\t\tstreamSettings[\"realitySettings\"] = s.realityData(streamSettings[\"realitySettings\"].(map[string]any))\n\t}\n\tdelete(streamSettings, \"sockopt\")\n\n\tif s.fragment != \"\" {\n\t\tstreamSettings[\"sockopt\"] = json_util.RawMessage(`{\"dialerProxy\": \"fragment\", \"tcpKeepAliveIdle\": 100, \"tcpMptcp\": true, \"penetrate\": true}`)\n\t}\n\n\t// remove proxy protocol\n\tnetwork, _ := streamSettings[\"network\"].(string)\n\tswitch network {\n\tcase \"tcp\":\n\t\tstreamSettings[\"tcpSettings\"] = s.removeAcceptProxy(streamSettings[\"tcpSettings\"])\n\tcase \"ws\":\n\t\tstreamSettings[\"wsSettings\"] = s.removeAcceptProxy(streamSettings[\"wsSettings\"])\n\tcase \"httpupgrade\":\n\t\tstreamSettings[\"httpupgradeSettings\"] = s.removeAcceptProxy(streamSettings[\"httpupgradeSettings\"])\n\t}\n\treturn streamSettings\n}\n\nfunc (s *SubJsonService) removeAcceptProxy(setting any) map[string]any {\n\tnetSettings, ok := setting.(map[string]any)\n\tif ok {\n\t\tdelete(netSettings, \"acceptProxyProtocol\")\n\t}\n\treturn netSettings\n}\n\nfunc (s *SubJsonService) tlsData(tData map[string]any) map[string]any {\n\ttlsData := make(map[string]any, 1)\n\ttlsClientSettings, _ := tData[\"settings\"].(map[string]any)\n\n\ttlsData[\"serverName\"] = tData[\"serverName\"]\n\ttlsData[\"alpn\"] = tData[\"alpn\"]\n\tif fingerprint, ok := tlsClientSettings[\"fingerprint\"].(string); ok {\n\t\ttlsData[\"fingerprint\"] = fingerprint\n\t}\n\treturn tlsData\n}\n\nfunc (s *SubJsonService) realityData(rData map[string]any) map[string]any {\n\trltyData := make(map[string]any, 1)\n\trltyClientSettings, _ := rData[\"settings\"].(map[string]any)\n\n\trltyData[\"show\"] = false\n\trltyData[\"publicKey\"] = rltyClientSettings[\"publicKey\"]\n\trltyData[\"fingerprint\"] = rltyClientSettings[\"fingerprint\"]\n\trltyData[\"mldsa65Verify\"] = rltyClientSettings[\"mldsa65Verify\"]\n\n\t// Set random data\n\trltyData[\"spiderX\"] = \"/\" + random.Seq(15)\n\tshortIds, ok := rData[\"shortIds\"].([]any)\n\tif ok && len(shortIds) > 0 {\n\t\trltyData[\"shortId\"] = shortIds[random.Num(len(shortIds))].(string)\n\t} else {\n\t\trltyData[\"shortId\"] = \"\"\n\t}\n\tserverNames, ok := rData[\"serverNames\"].([]any)\n\tif ok && len(serverNames) > 0 {\n\t\trltyData[\"serverName\"] = serverNames[random.Num(len(serverNames))].(string)\n\t} else {\n\t\trltyData[\"serverName\"] = \"\"\n\t}\n\n\treturn rltyData\n}\n\nfunc (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {\n\toutbound := Outbound{}\n\tusersData := make([]UserVnext, 1)\n\n\tusersData[0].ID = client.ID\n\tusersData[0].Email = client.Email\n\tusersData[0].Security = client.Security\n\tvnextData := make([]VnextSetting, 1)\n\tvnextData[0] = VnextSetting{\n\t\tAddress: inbound.Listen,\n\t\tPort:    inbound.Port,\n\t\tUsers:   usersData,\n\t}\n\n\toutbound.Protocol = string(inbound.Protocol)\n\toutbound.Tag = \"proxy\"\n\tif s.mux != \"\" {\n\t\toutbound.Mux = json_util.RawMessage(s.mux)\n\t}\n\toutbound.StreamSettings = streamSettings\n\toutbound.Settings = map[string]any{\n\t\t\"vnext\": vnextData,\n\t}\n\n\tresult, _ := json.MarshalIndent(outbound, \"\", \"  \")\n\treturn result\n}\n\nfunc (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {\n\toutbound := Outbound{}\n\toutbound.Protocol = string(inbound.Protocol)\n\toutbound.Tag = \"proxy\"\n\tif s.mux != \"\" {\n\t\toutbound.Mux = json_util.RawMessage(s.mux)\n\t}\n\toutbound.StreamSettings = streamSettings\n\tsettings := make(map[string]any)\n\tsettings[\"address\"] = inbound.Listen\n\tsettings[\"port\"] = inbound.Port\n\tsettings[\"id\"] = client.ID\n\tif client.Flow != \"\" {\n\t\tsettings[\"flow\"] = client.Flow\n\t}\n\n\t// Add encryption for VLESS outbound from inbound settings\n\tvar inboundSettings map[string]any\n\tjson.Unmarshal([]byte(inbound.Settings), &inboundSettings)\n\tif encryption, ok := inboundSettings[\"encryption\"].(string); ok {\n\t\tsettings[\"encryption\"] = encryption\n\t}\n\n\toutbound.Settings = settings\n\tresult, _ := json.MarshalIndent(outbound, \"\", \"  \")\n\treturn result\n}\n\nfunc (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {\n\toutbound := Outbound{}\n\n\tserverData := make([]ServerSetting, 1)\n\tserverData[0] = ServerSetting{\n\t\tAddress:  inbound.Listen,\n\t\tPort:     inbound.Port,\n\t\tLevel:    8,\n\t\tPassword: client.Password,\n\t}\n\n\tif inbound.Protocol == model.Shadowsocks {\n\t\tvar inboundSettings map[string]any\n\t\tjson.Unmarshal([]byte(inbound.Settings), &inboundSettings)\n\t\tmethod, _ := inboundSettings[\"method\"].(string)\n\t\tserverData[0].Method = method\n\n\t\t// server password in multi-user 2022 protocols\n\t\tif strings.HasPrefix(method, \"2022\") {\n\t\t\tif serverPassword, ok := inboundSettings[\"password\"].(string); ok {\n\t\t\t\tserverData[0].Password = fmt.Sprintf(\"%s:%s\", serverPassword, client.Password)\n\t\t\t}\n\t\t}\n\t}\n\n\toutbound.Protocol = string(inbound.Protocol)\n\toutbound.Tag = \"proxy\"\n\tif s.mux != \"\" {\n\t\toutbound.Mux = json_util.RawMessage(s.mux)\n\t}\n\toutbound.StreamSettings = streamSettings\n\toutbound.Settings = map[string]any{\n\t\t\"servers\": serverData,\n\t}\n\n\tresult, _ := json.MarshalIndent(outbound, \"\", \"  \")\n\treturn result\n}\n\ntype Outbound struct {\n\tProtocol       string               `json:\"protocol\"`\n\tTag            string               `json:\"tag\"`\n\tStreamSettings json_util.RawMessage `json:\"streamSettings\"`\n\tMux            json_util.RawMessage `json:\"mux,omitempty\"`\n\tSettings       map[string]any       `json:\"settings,omitempty\"`\n}\n\ntype VnextSetting struct {\n\tAddress string      `json:\"address\"`\n\tPort    int         `json:\"port\"`\n\tUsers   []UserVnext `json:\"users\"`\n}\n\ntype UserVnext struct {\n\tID       string `json:\"id\"`\n\tEmail    string `json:\"email,omitempty\"`\n\tSecurity string `json:\"security,omitempty\"`\n}\n\ntype ServerSetting struct {\n\tPassword string `json:\"password\"`\n\tLevel    int    `json:\"level\"`\n\tAddress  string `json:\"address\"`\n\tPort     int    `json:\"port\"`\n\tFlow     string `json:\"flow,omitempty\"`\n\tMethod   string `json:\"method,omitempty\"`\n}\n"
  },
  {
    "path": "sub/subService.go",
    "content": "package sub\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/goccy/go-json\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/random\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n)\n\n// SubService provides business logic for generating subscription links and managing subscription data.\ntype SubService struct {\n\taddress        string\n\tshowInfo       bool\n\tremarkModel    string\n\tdatepicker     string\n\tinboundService service.InboundService\n\tsettingService service.SettingService\n}\n\n// NewSubService creates a new subscription service with the given configuration.\nfunc NewSubService(showInfo bool, remarkModel string) *SubService {\n\treturn &SubService{\n\t\tshowInfo:    showInfo,\n\t\tremarkModel: remarkModel,\n\t}\n}\n\n// GetSubs retrieves subscription links for a given subscription ID and host.\nfunc (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) {\n\ts.address = host\n\tvar result []string\n\tvar traffic xray.ClientTraffic\n\tvar lastOnline int64\n\tvar clientTraffics []xray.ClientTraffic\n\tinbounds, err := s.getInboundsBySubId(subId)\n\tif err != nil {\n\t\treturn nil, 0, traffic, err\n\t}\n\n\tif len(inbounds) == 0 {\n\t\treturn nil, 0, traffic, common.NewError(\"No inbounds found with \", subId)\n\t}\n\n\ts.datepicker, err = s.settingService.GetDatepicker()\n\tif err != nil {\n\t\ts.datepicker = \"gregorian\"\n\t}\n\tfor _, inbound := range inbounds {\n\t\tclients, err := s.inboundService.GetClients(inbound)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"SubService - GetClients: Unable to get clients from inbound\")\n\t\t}\n\t\tif clients == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {\n\t\t\tlisten, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)\n\t\t\tif err == nil {\n\t\t\t\tinbound.Listen = listen\n\t\t\t\tinbound.Port = port\n\t\t\t\tinbound.StreamSettings = streamSettings\n\t\t\t}\n\t\t}\n\t\tfor _, client := range clients {\n\t\t\tif client.Enable && client.SubID == subId {\n\t\t\t\tlink := s.getLink(inbound, client.Email)\n\t\t\t\tresult = append(result, link)\n\t\t\t\tct := s.getClientTraffics(inbound.ClientStats, client.Email)\n\t\t\t\tclientTraffics = append(clientTraffics, ct)\n\t\t\t\tif ct.LastOnline > lastOnline {\n\t\t\t\t\tlastOnline = ct.LastOnline\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Prepare statistics\n\tfor index, clientTraffic := range clientTraffics {\n\t\tif index == 0 {\n\t\t\ttraffic.Up = clientTraffic.Up\n\t\t\ttraffic.Down = clientTraffic.Down\n\t\t\ttraffic.Total = clientTraffic.Total\n\t\t\tif clientTraffic.ExpiryTime > 0 {\n\t\t\t\ttraffic.ExpiryTime = clientTraffic.ExpiryTime\n\t\t\t}\n\t\t} else {\n\t\t\ttraffic.Up += clientTraffic.Up\n\t\t\ttraffic.Down += clientTraffic.Down\n\t\t\tif traffic.Total == 0 || clientTraffic.Total == 0 {\n\t\t\t\ttraffic.Total = 0\n\t\t\t} else {\n\t\t\t\ttraffic.Total += clientTraffic.Total\n\t\t\t}\n\t\t\tif clientTraffic.ExpiryTime != traffic.ExpiryTime {\n\t\t\t\ttraffic.ExpiryTime = 0\n\t\t\t}\n\t\t}\n\t}\n\treturn result, lastOnline, traffic, nil\n}\n\nfunc (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {\n\tdb := database.GetDB()\n\tvar inbounds []*model.Inbound\n\terr := db.Model(model.Inbound{}).Preload(\"ClientStats\").Where(`id in (\n\t\tSELECT DISTINCT inbounds.id\n\t\tFROM inbounds,\n\t\t\tJSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client \n\t\tWHERE\n\t\t\tprotocol in ('vmess','vless','trojan','shadowsocks')\n\t\t\tAND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?\n\t)`, subId, true).Find(&inbounds).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn inbounds, nil\n}\n\nfunc (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {\n\tfor _, traffic := range traffics {\n\t\tif traffic.Email == email {\n\t\t\treturn traffic\n\t\t}\n\t}\n\treturn xray.ClientTraffic{}\n}\n\nfunc (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {\n\tdb := database.GetDB()\n\tvar inbound *model.Inbound\n\terr := db.Model(model.Inbound{}).\n\t\tWhere(\"JSON_TYPE(settings, '$.fallbacks') = 'array'\").\n\t\tWhere(\"EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)\", dest).\n\t\tFind(&inbound).Error\n\tif err != nil {\n\t\treturn \"\", 0, \"\", err\n\t}\n\n\tvar stream map[string]any\n\tjson.Unmarshal([]byte(streamSettings), &stream)\n\tvar masterStream map[string]any\n\tjson.Unmarshal([]byte(inbound.StreamSettings), &masterStream)\n\tstream[\"security\"] = masterStream[\"security\"]\n\tstream[\"tlsSettings\"] = masterStream[\"tlsSettings\"]\n\tstream[\"externalProxy\"] = masterStream[\"externalProxy\"]\n\tmodifiedStream, _ := json.MarshalIndent(stream, \"\", \"  \")\n\n\treturn inbound.Listen, inbound.Port, string(modifiedStream), nil\n}\n\nfunc (s *SubService) getLink(inbound *model.Inbound, email string) string {\n\tswitch inbound.Protocol {\n\tcase \"vmess\":\n\t\treturn s.genVmessLink(inbound, email)\n\tcase \"vless\":\n\t\treturn s.genVlessLink(inbound, email)\n\tcase \"trojan\":\n\t\treturn s.genTrojanLink(inbound, email)\n\tcase \"shadowsocks\":\n\t\treturn s.genShadowsocksLink(inbound, email)\n\t}\n\treturn \"\"\n}\n\nfunc (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {\n\tif inbound.Protocol != model.VMESS {\n\t\treturn \"\"\n\t}\n\tvar address string\n\tif inbound.Listen == \"\" || inbound.Listen == \"0.0.0.0\" || inbound.Listen == \"::\" || inbound.Listen == \"::0\" {\n\t\taddress = s.address\n\t} else {\n\t\taddress = inbound.Listen\n\t}\n\tobj := map[string]any{\n\t\t\"v\":    \"2\",\n\t\t\"add\":  address,\n\t\t\"port\": inbound.Port,\n\t\t\"type\": \"none\",\n\t}\n\tvar stream map[string]any\n\tjson.Unmarshal([]byte(inbound.StreamSettings), &stream)\n\tnetwork, _ := stream[\"network\"].(string)\n\tobj[\"net\"] = network\n\tswitch network {\n\tcase \"tcp\":\n\t\ttcp, _ := stream[\"tcpSettings\"].(map[string]any)\n\t\theader, _ := tcp[\"header\"].(map[string]any)\n\t\ttypeStr, _ := header[\"type\"].(string)\n\t\tobj[\"type\"] = typeStr\n\t\tif typeStr == \"http\" {\n\t\t\trequest := header[\"request\"].(map[string]any)\n\t\t\trequestPath, _ := request[\"path\"].([]any)\n\t\t\tobj[\"path\"] = requestPath[0].(string)\n\t\t\theaders, _ := request[\"headers\"].(map[string]any)\n\t\t\tobj[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"kcp\":\n\t\tkcp, _ := stream[\"kcpSettings\"].(map[string]any)\n\t\theader, _ := kcp[\"header\"].(map[string]any)\n\t\tobj[\"type\"], _ = header[\"type\"].(string)\n\t\tobj[\"path\"], _ = kcp[\"seed\"].(string)\n\tcase \"ws\":\n\t\tws, _ := stream[\"wsSettings\"].(map[string]any)\n\t\tobj[\"path\"] = ws[\"path\"].(string)\n\t\tif host, ok := ws[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tobj[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := ws[\"headers\"].(map[string]any)\n\t\t\tobj[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"grpc\":\n\t\tgrpc, _ := stream[\"grpcSettings\"].(map[string]any)\n\t\tobj[\"path\"] = grpc[\"serviceName\"].(string)\n\t\tobj[\"authority\"] = grpc[\"authority\"].(string)\n\t\tif grpc[\"multiMode\"].(bool) {\n\t\t\tobj[\"type\"] = \"multi\"\n\t\t}\n\tcase \"httpupgrade\":\n\t\thttpupgrade, _ := stream[\"httpupgradeSettings\"].(map[string]any)\n\t\tobj[\"path\"] = httpupgrade[\"path\"].(string)\n\t\tif host, ok := httpupgrade[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tobj[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := httpupgrade[\"headers\"].(map[string]any)\n\t\t\tobj[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"xhttp\":\n\t\txhttp, _ := stream[\"xhttpSettings\"].(map[string]any)\n\t\tobj[\"path\"] = xhttp[\"path\"].(string)\n\t\tif host, ok := xhttp[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tobj[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := xhttp[\"headers\"].(map[string]any)\n\t\t\tobj[\"host\"] = searchHost(headers)\n\t\t}\n\t\tobj[\"mode\"] = xhttp[\"mode\"].(string)\n\t}\n\tsecurity, _ := stream[\"security\"].(string)\n\tobj[\"tls\"] = security\n\tif security == \"tls\" {\n\t\ttlsSetting, _ := stream[\"tlsSettings\"].(map[string]any)\n\t\talpns, _ := tlsSetting[\"alpn\"].([]any)\n\t\tif len(alpns) > 0 {\n\t\t\tvar alpn []string\n\t\t\tfor _, a := range alpns {\n\t\t\t\talpn = append(alpn, a.(string))\n\t\t\t}\n\t\t\tobj[\"alpn\"] = strings.Join(alpn, \",\")\n\t\t}\n\t\tif sniValue, ok := searchKey(tlsSetting, \"serverName\"); ok {\n\t\t\tobj[\"sni\"], _ = sniValue.(string)\n\t\t}\n\n\t\ttlsSettings, _ := searchKey(tlsSetting, \"settings\")\n\t\tif tlsSetting != nil {\n\t\t\tif fpValue, ok := searchKey(tlsSettings, \"fingerprint\"); ok {\n\t\t\t\tobj[\"fp\"], _ = fpValue.(string)\n\t\t\t}\n\t\t}\n\t}\n\n\tclients, _ := s.inboundService.GetClients(inbound)\n\tclientIndex := -1\n\tfor i, client := range clients {\n\t\tif client.Email == email {\n\t\t\tclientIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\tobj[\"id\"] = clients[clientIndex].ID\n\tobj[\"scy\"] = clients[clientIndex].Security\n\n\texternalProxies, _ := stream[\"externalProxy\"].([]any)\n\n\tif len(externalProxies) > 0 {\n\t\tlinks := \"\"\n\t\tfor index, externalProxy := range externalProxies {\n\t\t\tep, _ := externalProxy.(map[string]any)\n\t\t\tnewSecurity, _ := ep[\"forceTls\"].(string)\n\t\t\tnewObj := map[string]any{}\n\t\t\tfor key, value := range obj {\n\t\t\t\tif !(newSecurity == \"none\" && (key == \"alpn\" || key == \"sni\" || key == \"fp\")) {\n\t\t\t\t\tnewObj[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\tnewObj[\"ps\"] = s.genRemark(inbound, email, ep[\"remark\"].(string))\n\t\t\tnewObj[\"add\"] = ep[\"dest\"].(string)\n\t\t\tnewObj[\"port\"] = int(ep[\"port\"].(float64))\n\n\t\t\tif newSecurity != \"same\" {\n\t\t\t\tnewObj[\"tls\"] = newSecurity\n\t\t\t}\n\t\t\tif index > 0 {\n\t\t\t\tlinks += \"\\n\"\n\t\t\t}\n\t\t\tjsonStr, _ := json.MarshalIndent(newObj, \"\", \"  \")\n\t\t\tlinks += \"vmess://\" + base64.StdEncoding.EncodeToString(jsonStr)\n\t\t}\n\t\treturn links\n\t}\n\n\tobj[\"ps\"] = s.genRemark(inbound, email, \"\")\n\n\tjsonStr, _ := json.MarshalIndent(obj, \"\", \"  \")\n\treturn \"vmess://\" + base64.StdEncoding.EncodeToString(jsonStr)\n}\n\nfunc (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {\n\tvar address string\n\tif inbound.Listen == \"\" || inbound.Listen == \"0.0.0.0\" || inbound.Listen == \"::\" || inbound.Listen == \"::0\" {\n\t\taddress = s.address\n\t} else {\n\t\taddress = inbound.Listen\n\t}\n\n\tif inbound.Protocol != model.VLESS {\n\t\treturn \"\"\n\t}\n\tvar stream map[string]any\n\tjson.Unmarshal([]byte(inbound.StreamSettings), &stream)\n\tclients, _ := s.inboundService.GetClients(inbound)\n\tclientIndex := -1\n\tfor i, client := range clients {\n\t\tif client.Email == email {\n\t\t\tclientIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\tuuid := clients[clientIndex].ID\n\tport := inbound.Port\n\tstreamNetwork := stream[\"network\"].(string)\n\tparams := make(map[string]string)\n\tparams[\"type\"] = streamNetwork\n\n\t// Add encryption parameter for VLESS from inbound settings\n\tvar settings map[string]any\n\tjson.Unmarshal([]byte(inbound.Settings), &settings)\n\tif encryption, ok := settings[\"encryption\"].(string); ok {\n\t\tparams[\"encryption\"] = encryption\n\t}\n\n\tswitch streamNetwork {\n\tcase \"tcp\":\n\t\ttcp, _ := stream[\"tcpSettings\"].(map[string]any)\n\t\theader, _ := tcp[\"header\"].(map[string]any)\n\t\ttypeStr, _ := header[\"type\"].(string)\n\t\tif typeStr == \"http\" {\n\t\t\trequest := header[\"request\"].(map[string]any)\n\t\t\trequestPath, _ := request[\"path\"].([]any)\n\t\t\tparams[\"path\"] = requestPath[0].(string)\n\t\t\theaders, _ := request[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t\tparams[\"headerType\"] = \"http\"\n\t\t}\n\tcase \"kcp\":\n\t\tkcp, _ := stream[\"kcpSettings\"].(map[string]any)\n\t\theader, _ := kcp[\"header\"].(map[string]any)\n\t\tparams[\"headerType\"] = header[\"type\"].(string)\n\t\tparams[\"seed\"] = kcp[\"seed\"].(string)\n\tcase \"ws\":\n\t\tws, _ := stream[\"wsSettings\"].(map[string]any)\n\t\tparams[\"path\"] = ws[\"path\"].(string)\n\t\tif host, ok := ws[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := ws[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"grpc\":\n\t\tgrpc, _ := stream[\"grpcSettings\"].(map[string]any)\n\t\tparams[\"serviceName\"] = grpc[\"serviceName\"].(string)\n\t\tparams[\"authority\"], _ = grpc[\"authority\"].(string)\n\t\tif grpc[\"multiMode\"].(bool) {\n\t\t\tparams[\"mode\"] = \"multi\"\n\t\t}\n\tcase \"httpupgrade\":\n\t\thttpupgrade, _ := stream[\"httpupgradeSettings\"].(map[string]any)\n\t\tparams[\"path\"] = httpupgrade[\"path\"].(string)\n\t\tif host, ok := httpupgrade[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := httpupgrade[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"xhttp\":\n\t\txhttp, _ := stream[\"xhttpSettings\"].(map[string]any)\n\t\tparams[\"path\"] = xhttp[\"path\"].(string)\n\t\tif host, ok := xhttp[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := xhttp[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\t\tparams[\"mode\"] = xhttp[\"mode\"].(string)\n\t}\n\tsecurity, _ := stream[\"security\"].(string)\n\tif security == \"tls\" {\n\t\tparams[\"security\"] = \"tls\"\n\t\ttlsSetting, _ := stream[\"tlsSettings\"].(map[string]any)\n\t\talpns, _ := tlsSetting[\"alpn\"].([]any)\n\t\tvar alpn []string\n\t\tfor _, a := range alpns {\n\t\t\talpn = append(alpn, a.(string))\n\t\t}\n\t\tif len(alpn) > 0 {\n\t\t\tparams[\"alpn\"] = strings.Join(alpn, \",\")\n\t\t}\n\t\tif sniValue, ok := searchKey(tlsSetting, \"serverName\"); ok {\n\t\t\tparams[\"sni\"], _ = sniValue.(string)\n\t\t}\n\n\t\ttlsSettings, _ := searchKey(tlsSetting, \"settings\")\n\t\tif tlsSetting != nil {\n\t\t\tif fpValue, ok := searchKey(tlsSettings, \"fingerprint\"); ok {\n\t\t\t\tparams[\"fp\"], _ = fpValue.(string)\n\t\t\t}\n\t\t}\n\n\t\tif streamNetwork == \"tcp\" && len(clients[clientIndex].Flow) > 0 {\n\t\t\tparams[\"flow\"] = clients[clientIndex].Flow\n\t\t}\n\t}\n\n\tif security == \"reality\" {\n\t\tparams[\"security\"] = \"reality\"\n\t\trealitySetting, _ := stream[\"realitySettings\"].(map[string]any)\n\t\trealitySettings, _ := searchKey(realitySetting, \"settings\")\n\t\tif realitySetting != nil {\n\t\t\tif sniValue, ok := searchKey(realitySetting, \"serverNames\"); ok {\n\t\t\t\tsNames, _ := sniValue.([]any)\n\t\t\t\tparams[\"sni\"] = sNames[random.Num(len(sNames))].(string)\n\t\t\t}\n\t\t\tif pbkValue, ok := searchKey(realitySettings, \"publicKey\"); ok {\n\t\t\t\tparams[\"pbk\"], _ = pbkValue.(string)\n\t\t\t}\n\t\t\tif sidValue, ok := searchKey(realitySetting, \"shortIds\"); ok {\n\t\t\t\tshortIds, _ := sidValue.([]any)\n\t\t\t\tparams[\"sid\"] = shortIds[random.Num(len(shortIds))].(string)\n\t\t\t}\n\t\t\tif fpValue, ok := searchKey(realitySettings, \"fingerprint\"); ok {\n\t\t\t\tif fp, ok := fpValue.(string); ok && len(fp) > 0 {\n\t\t\t\t\tparams[\"fp\"] = fp\n\t\t\t\t}\n\t\t\t}\n\t\t\tif pqvValue, ok := searchKey(realitySettings, \"mldsa65Verify\"); ok {\n\t\t\t\tif pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {\n\t\t\t\t\tparams[\"pqv\"] = pqv\n\t\t\t\t}\n\t\t\t}\n\t\t\tparams[\"spx\"] = \"/\" + random.Seq(15)\n\t\t}\n\n\t\tif streamNetwork == \"tcp\" && len(clients[clientIndex].Flow) > 0 {\n\t\t\tparams[\"flow\"] = clients[clientIndex].Flow\n\t\t}\n\t}\n\n\tif security != \"tls\" && security != \"reality\" {\n\t\tparams[\"security\"] = \"none\"\n\t}\n\n\texternalProxies, _ := stream[\"externalProxy\"].([]any)\n\n\tif len(externalProxies) > 0 {\n\t\tlinks := make([]string, 0, len(externalProxies))\n\t\tfor _, externalProxy := range externalProxies {\n\t\t\tep, _ := externalProxy.(map[string]any)\n\t\t\tnewSecurity, _ := ep[\"forceTls\"].(string)\n\t\t\tdest, _ := ep[\"dest\"].(string)\n\t\t\tport := int(ep[\"port\"].(float64))\n\t\t\tlink := fmt.Sprintf(\"vless://%s@%s:%d\", uuid, dest, port)\n\n\t\t\tif newSecurity != \"same\" {\n\t\t\t\tparams[\"security\"] = newSecurity\n\t\t\t} else {\n\t\t\t\tparams[\"security\"] = security\n\t\t\t}\n\t\t\turl, _ := url.Parse(link)\n\t\t\tq := url.Query()\n\n\t\t\tfor k, v := range params {\n\t\t\t\tif !(newSecurity == \"none\" && (k == \"alpn\" || k == \"sni\" || k == \"fp\")) {\n\t\t\t\t\tq.Add(k, v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set the new query values on the URL\n\t\t\turl.RawQuery = q.Encode()\n\n\t\t\turl.Fragment = s.genRemark(inbound, email, ep[\"remark\"].(string))\n\n\t\t\tlinks = append(links, url.String())\n\t\t}\n\t\treturn strings.Join(links, \"\\n\")\n\t}\n\n\tlink := fmt.Sprintf(\"vless://%s@%s:%d\", uuid, address, port)\n\turl, _ := url.Parse(link)\n\tq := url.Query()\n\n\tfor k, v := range params {\n\t\tq.Add(k, v)\n\t}\n\n\t// Set the new query values on the URL\n\turl.RawQuery = q.Encode()\n\n\turl.Fragment = s.genRemark(inbound, email, \"\")\n\treturn url.String()\n}\n\nfunc (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {\n\tvar address string\n\tif inbound.Listen == \"\" || inbound.Listen == \"0.0.0.0\" || inbound.Listen == \"::\" || inbound.Listen == \"::0\" {\n\t\taddress = s.address\n\t} else {\n\t\taddress = inbound.Listen\n\t}\n\tif inbound.Protocol != model.Trojan {\n\t\treturn \"\"\n\t}\n\tvar stream map[string]any\n\tjson.Unmarshal([]byte(inbound.StreamSettings), &stream)\n\tclients, _ := s.inboundService.GetClients(inbound)\n\tclientIndex := -1\n\tfor i, client := range clients {\n\t\tif client.Email == email {\n\t\t\tclientIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\tpassword := clients[clientIndex].Password\n\tport := inbound.Port\n\tstreamNetwork := stream[\"network\"].(string)\n\tparams := make(map[string]string)\n\tparams[\"type\"] = streamNetwork\n\n\tswitch streamNetwork {\n\tcase \"tcp\":\n\t\ttcp, _ := stream[\"tcpSettings\"].(map[string]any)\n\t\theader, _ := tcp[\"header\"].(map[string]any)\n\t\ttypeStr, _ := header[\"type\"].(string)\n\t\tif typeStr == \"http\" {\n\t\t\trequest := header[\"request\"].(map[string]any)\n\t\t\trequestPath, _ := request[\"path\"].([]any)\n\t\t\tparams[\"path\"] = requestPath[0].(string)\n\t\t\theaders, _ := request[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t\tparams[\"headerType\"] = \"http\"\n\t\t}\n\tcase \"kcp\":\n\t\tkcp, _ := stream[\"kcpSettings\"].(map[string]any)\n\t\theader, _ := kcp[\"header\"].(map[string]any)\n\t\tparams[\"headerType\"] = header[\"type\"].(string)\n\t\tparams[\"seed\"] = kcp[\"seed\"].(string)\n\tcase \"ws\":\n\t\tws, _ := stream[\"wsSettings\"].(map[string]any)\n\t\tparams[\"path\"] = ws[\"path\"].(string)\n\t\tif host, ok := ws[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := ws[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"grpc\":\n\t\tgrpc, _ := stream[\"grpcSettings\"].(map[string]any)\n\t\tparams[\"serviceName\"] = grpc[\"serviceName\"].(string)\n\t\tparams[\"authority\"], _ = grpc[\"authority\"].(string)\n\t\tif grpc[\"multiMode\"].(bool) {\n\t\t\tparams[\"mode\"] = \"multi\"\n\t\t}\n\tcase \"httpupgrade\":\n\t\thttpupgrade, _ := stream[\"httpupgradeSettings\"].(map[string]any)\n\t\tparams[\"path\"] = httpupgrade[\"path\"].(string)\n\t\tif host, ok := httpupgrade[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := httpupgrade[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"xhttp\":\n\t\txhttp, _ := stream[\"xhttpSettings\"].(map[string]any)\n\t\tparams[\"path\"] = xhttp[\"path\"].(string)\n\t\tif host, ok := xhttp[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := xhttp[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\t\tparams[\"mode\"] = xhttp[\"mode\"].(string)\n\t}\n\tsecurity, _ := stream[\"security\"].(string)\n\tif security == \"tls\" {\n\t\tparams[\"security\"] = \"tls\"\n\t\ttlsSetting, _ := stream[\"tlsSettings\"].(map[string]any)\n\t\talpns, _ := tlsSetting[\"alpn\"].([]any)\n\t\tvar alpn []string\n\t\tfor _, a := range alpns {\n\t\t\talpn = append(alpn, a.(string))\n\t\t}\n\t\tif len(alpn) > 0 {\n\t\t\tparams[\"alpn\"] = strings.Join(alpn, \",\")\n\t\t}\n\t\tif sniValue, ok := searchKey(tlsSetting, \"serverName\"); ok {\n\t\t\tparams[\"sni\"], _ = sniValue.(string)\n\t\t}\n\n\t\ttlsSettings, _ := searchKey(tlsSetting, \"settings\")\n\t\tif tlsSetting != nil {\n\t\t\tif fpValue, ok := searchKey(tlsSettings, \"fingerprint\"); ok {\n\t\t\t\tparams[\"fp\"], _ = fpValue.(string)\n\t\t\t}\n\t\t}\n\t}\n\n\tif security == \"reality\" {\n\t\tparams[\"security\"] = \"reality\"\n\t\trealitySetting, _ := stream[\"realitySettings\"].(map[string]any)\n\t\trealitySettings, _ := searchKey(realitySetting, \"settings\")\n\t\tif realitySetting != nil {\n\t\t\tif sniValue, ok := searchKey(realitySetting, \"serverNames\"); ok {\n\t\t\t\tsNames, _ := sniValue.([]any)\n\t\t\t\tparams[\"sni\"] = sNames[random.Num(len(sNames))].(string)\n\t\t\t}\n\t\t\tif pbkValue, ok := searchKey(realitySettings, \"publicKey\"); ok {\n\t\t\t\tparams[\"pbk\"], _ = pbkValue.(string)\n\t\t\t}\n\t\t\tif sidValue, ok := searchKey(realitySetting, \"shortIds\"); ok {\n\t\t\t\tshortIds, _ := sidValue.([]any)\n\t\t\t\tparams[\"sid\"] = shortIds[random.Num(len(shortIds))].(string)\n\t\t\t}\n\t\t\tif fpValue, ok := searchKey(realitySettings, \"fingerprint\"); ok {\n\t\t\t\tif fp, ok := fpValue.(string); ok && len(fp) > 0 {\n\t\t\t\t\tparams[\"fp\"] = fp\n\t\t\t\t}\n\t\t\t}\n\t\t\tif pqvValue, ok := searchKey(realitySettings, \"mldsa65Verify\"); ok {\n\t\t\t\tif pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {\n\t\t\t\t\tparams[\"pqv\"] = pqv\n\t\t\t\t}\n\t\t\t}\n\t\t\tparams[\"spx\"] = \"/\" + random.Seq(15)\n\t\t}\n\n\t\tif streamNetwork == \"tcp\" && len(clients[clientIndex].Flow) > 0 {\n\t\t\tparams[\"flow\"] = clients[clientIndex].Flow\n\t\t}\n\t}\n\n\tif security != \"tls\" && security != \"reality\" {\n\t\tparams[\"security\"] = \"none\"\n\t}\n\n\texternalProxies, _ := stream[\"externalProxy\"].([]any)\n\n\tif len(externalProxies) > 0 {\n\t\tlinks := \"\"\n\t\tfor index, externalProxy := range externalProxies {\n\t\t\tep, _ := externalProxy.(map[string]any)\n\t\t\tnewSecurity, _ := ep[\"forceTls\"].(string)\n\t\t\tdest, _ := ep[\"dest\"].(string)\n\t\t\tport := int(ep[\"port\"].(float64))\n\t\t\tlink := fmt.Sprintf(\"trojan://%s@%s:%d\", password, dest, port)\n\n\t\t\tif newSecurity != \"same\" {\n\t\t\t\tparams[\"security\"] = newSecurity\n\t\t\t} else {\n\t\t\t\tparams[\"security\"] = security\n\t\t\t}\n\t\t\turl, _ := url.Parse(link)\n\t\t\tq := url.Query()\n\n\t\t\tfor k, v := range params {\n\t\t\t\tif !(newSecurity == \"none\" && (k == \"alpn\" || k == \"sni\" || k == \"fp\")) {\n\t\t\t\t\tq.Add(k, v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set the new query values on the URL\n\t\t\turl.RawQuery = q.Encode()\n\n\t\t\turl.Fragment = s.genRemark(inbound, email, ep[\"remark\"].(string))\n\n\t\t\tif index > 0 {\n\t\t\t\tlinks += \"\\n\"\n\t\t\t}\n\t\t\tlinks += url.String()\n\t\t}\n\t\treturn links\n\t}\n\n\tlink := fmt.Sprintf(\"trojan://%s@%s:%d\", password, address, port)\n\n\turl, _ := url.Parse(link)\n\tq := url.Query()\n\n\tfor k, v := range params {\n\t\tq.Add(k, v)\n\t}\n\n\t// Set the new query values on the URL\n\turl.RawQuery = q.Encode()\n\n\turl.Fragment = s.genRemark(inbound, email, \"\")\n\treturn url.String()\n}\n\nfunc (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {\n\tvar address string\n\tif inbound.Listen == \"\" || inbound.Listen == \"0.0.0.0\" || inbound.Listen == \"::\" || inbound.Listen == \"::0\" {\n\t\taddress = s.address\n\t} else {\n\t\taddress = inbound.Listen\n\t}\n\tif inbound.Protocol != model.Shadowsocks {\n\t\treturn \"\"\n\t}\n\tvar stream map[string]any\n\tjson.Unmarshal([]byte(inbound.StreamSettings), &stream)\n\tclients, _ := s.inboundService.GetClients(inbound)\n\n\tvar settings map[string]any\n\tjson.Unmarshal([]byte(inbound.Settings), &settings)\n\tinboundPassword := settings[\"password\"].(string)\n\tmethod := settings[\"method\"].(string)\n\tclientIndex := -1\n\tfor i, client := range clients {\n\t\tif client.Email == email {\n\t\t\tclientIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\tstreamNetwork := stream[\"network\"].(string)\n\tparams := make(map[string]string)\n\tparams[\"type\"] = streamNetwork\n\n\tswitch streamNetwork {\n\tcase \"tcp\":\n\t\ttcp, _ := stream[\"tcpSettings\"].(map[string]any)\n\t\theader, _ := tcp[\"header\"].(map[string]any)\n\t\ttypeStr, _ := header[\"type\"].(string)\n\t\tif typeStr == \"http\" {\n\t\t\trequest := header[\"request\"].(map[string]any)\n\t\t\trequestPath, _ := request[\"path\"].([]any)\n\t\t\tparams[\"path\"] = requestPath[0].(string)\n\t\t\theaders, _ := request[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t\tparams[\"headerType\"] = \"http\"\n\t\t}\n\tcase \"kcp\":\n\t\tkcp, _ := stream[\"kcpSettings\"].(map[string]any)\n\t\theader, _ := kcp[\"header\"].(map[string]any)\n\t\tparams[\"headerType\"] = header[\"type\"].(string)\n\t\tparams[\"seed\"] = kcp[\"seed\"].(string)\n\tcase \"ws\":\n\t\tws, _ := stream[\"wsSettings\"].(map[string]any)\n\t\tparams[\"path\"] = ws[\"path\"].(string)\n\t\tif host, ok := ws[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := ws[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"grpc\":\n\t\tgrpc, _ := stream[\"grpcSettings\"].(map[string]any)\n\t\tparams[\"serviceName\"] = grpc[\"serviceName\"].(string)\n\t\tparams[\"authority\"], _ = grpc[\"authority\"].(string)\n\t\tif grpc[\"multiMode\"].(bool) {\n\t\t\tparams[\"mode\"] = \"multi\"\n\t\t}\n\tcase \"httpupgrade\":\n\t\thttpupgrade, _ := stream[\"httpupgradeSettings\"].(map[string]any)\n\t\tparams[\"path\"] = httpupgrade[\"path\"].(string)\n\t\tif host, ok := httpupgrade[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := httpupgrade[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\tcase \"xhttp\":\n\t\txhttp, _ := stream[\"xhttpSettings\"].(map[string]any)\n\t\tparams[\"path\"] = xhttp[\"path\"].(string)\n\t\tif host, ok := xhttp[\"host\"].(string); ok && len(host) > 0 {\n\t\t\tparams[\"host\"] = host\n\t\t} else {\n\t\t\theaders, _ := xhttp[\"headers\"].(map[string]any)\n\t\t\tparams[\"host\"] = searchHost(headers)\n\t\t}\n\t\tparams[\"mode\"] = xhttp[\"mode\"].(string)\n\t}\n\n\tsecurity, _ := stream[\"security\"].(string)\n\tif security == \"tls\" {\n\t\tparams[\"security\"] = \"tls\"\n\t\ttlsSetting, _ := stream[\"tlsSettings\"].(map[string]any)\n\t\talpns, _ := tlsSetting[\"alpn\"].([]any)\n\t\tvar alpn []string\n\t\tfor _, a := range alpns {\n\t\t\talpn = append(alpn, a.(string))\n\t\t}\n\t\tif len(alpn) > 0 {\n\t\t\tparams[\"alpn\"] = strings.Join(alpn, \",\")\n\t\t}\n\t\tif sniValue, ok := searchKey(tlsSetting, \"serverName\"); ok {\n\t\t\tparams[\"sni\"], _ = sniValue.(string)\n\t\t}\n\n\t\ttlsSettings, _ := searchKey(tlsSetting, \"settings\")\n\t\tif tlsSetting != nil {\n\t\t\tif fpValue, ok := searchKey(tlsSettings, \"fingerprint\"); ok {\n\t\t\t\tparams[\"fp\"], _ = fpValue.(string)\n\t\t\t}\n\t\t}\n\t}\n\n\tencPart := fmt.Sprintf(\"%s:%s\", method, clients[clientIndex].Password)\n\tif method[0] == '2' {\n\t\tencPart = fmt.Sprintf(\"%s:%s:%s\", method, inboundPassword, clients[clientIndex].Password)\n\t}\n\n\texternalProxies, _ := stream[\"externalProxy\"].([]any)\n\n\tif len(externalProxies) > 0 {\n\t\tlinks := \"\"\n\t\tfor index, externalProxy := range externalProxies {\n\t\t\tep, _ := externalProxy.(map[string]any)\n\t\t\tnewSecurity, _ := ep[\"forceTls\"].(string)\n\t\t\tdest, _ := ep[\"dest\"].(string)\n\t\t\tport := int(ep[\"port\"].(float64))\n\t\t\tlink := fmt.Sprintf(\"ss://%s@%s:%d\", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port)\n\n\t\t\tif newSecurity != \"same\" {\n\t\t\t\tparams[\"security\"] = newSecurity\n\t\t\t} else {\n\t\t\t\tparams[\"security\"] = security\n\t\t\t}\n\t\t\turl, _ := url.Parse(link)\n\t\t\tq := url.Query()\n\n\t\t\tfor k, v := range params {\n\t\t\t\tif !(newSecurity == \"none\" && (k == \"alpn\" || k == \"sni\" || k == \"fp\")) {\n\t\t\t\t\tq.Add(k, v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set the new query values on the URL\n\t\t\turl.RawQuery = q.Encode()\n\n\t\t\turl.Fragment = s.genRemark(inbound, email, ep[\"remark\"].(string))\n\n\t\t\tif index > 0 {\n\t\t\t\tlinks += \"\\n\"\n\t\t\t}\n\t\t\tlinks += url.String()\n\t\t}\n\t\treturn links\n\t}\n\n\tlink := fmt.Sprintf(\"ss://%s@%s:%d\", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)\n\turl, _ := url.Parse(link)\n\tq := url.Query()\n\n\tfor k, v := range params {\n\t\tq.Add(k, v)\n\t}\n\n\t// Set the new query values on the URL\n\turl.RawQuery = q.Encode()\n\n\turl.Fragment = s.genRemark(inbound, email, \"\")\n\treturn url.String()\n}\n\nfunc (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {\n\tseparationChar := string(s.remarkModel[0])\n\torderChars := s.remarkModel[1:]\n\torders := map[byte]string{\n\t\t'i': \"\",\n\t\t'e': \"\",\n\t\t'o': \"\",\n\t}\n\tif len(email) > 0 {\n\t\torders['e'] = email\n\t}\n\tif len(inbound.Remark) > 0 {\n\t\torders['i'] = inbound.Remark\n\t}\n\tif len(extra) > 0 {\n\t\torders['o'] = extra\n\t}\n\n\tvar remark []string\n\tfor i := 0; i < len(orderChars); i++ {\n\t\tchar := orderChars[i]\n\t\torder, exists := orders[char]\n\t\tif exists && order != \"\" {\n\t\t\tremark = append(remark, order)\n\t\t}\n\t}\n\n\tif s.showInfo {\n\t\tstatsExist := false\n\t\tvar stats xray.ClientTraffic\n\t\tfor _, clientStat := range inbound.ClientStats {\n\t\t\tif clientStat.Email == email {\n\t\t\t\tstats = clientStat\n\t\t\t\tstatsExist = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Get remained days\n\t\tif statsExist {\n\t\t\tif !stats.Enable {\n\t\t\t\treturn fmt.Sprintf(\"⛔️N/A%s%s\", separationChar, strings.Join(remark, separationChar))\n\t\t\t}\n\t\t\tif vol := stats.Total - (stats.Up + stats.Down); vol > 0 {\n\t\t\t\tremark = append(remark, fmt.Sprintf(\"%s%s\", common.FormatTraffic(vol), \"📊\"))\n\t\t\t}\n\t\t\tnow := time.Now().Unix()\n\t\t\tswitch exp := stats.ExpiryTime / 1000; {\n\t\t\tcase exp > 0:\n\t\t\t\tremainingSeconds := exp - now\n\t\t\t\tdays := remainingSeconds / 86400\n\t\t\t\thours := (remainingSeconds % 86400) / 3600\n\t\t\t\tminutes := (remainingSeconds % 3600) / 60\n\t\t\t\tif days > 0 {\n\t\t\t\t\tif hours > 0 {\n\t\t\t\t\t\tremark = append(remark, fmt.Sprintf(\"%dD,%dH⏳\", days, hours))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tremark = append(remark, fmt.Sprintf(\"%dD⏳\", days))\n\t\t\t\t\t}\n\t\t\t\t} else if hours > 0 {\n\t\t\t\t\tremark = append(remark, fmt.Sprintf(\"%dH⏳\", hours))\n\t\t\t\t} else {\n\t\t\t\t\tremark = append(remark, fmt.Sprintf(\"%dM⏳\", minutes))\n\t\t\t\t}\n\t\t\tcase exp < 0:\n\t\t\t\tdays := exp / -86400\n\t\t\t\thours := (exp % -86400) / 3600\n\t\t\t\tminutes := (exp % -3600) / 60\n\t\t\t\tif days > 0 {\n\t\t\t\t\tif hours > 0 {\n\t\t\t\t\t\tremark = append(remark, fmt.Sprintf(\"%dD,%dH⏳\", days, hours))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tremark = append(remark, fmt.Sprintf(\"%dD⏳\", days))\n\t\t\t\t\t}\n\t\t\t\t} else if hours > 0 {\n\t\t\t\t\tremark = append(remark, fmt.Sprintf(\"%dH⏳\", hours))\n\t\t\t\t} else {\n\t\t\t\t\tremark = append(remark, fmt.Sprintf(\"%dM⏳\", minutes))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn strings.Join(remark, separationChar)\n}\n\nfunc searchKey(data any, key string) (any, bool) {\n\tswitch val := data.(type) {\n\tcase map[string]any:\n\t\tfor k, v := range val {\n\t\t\tif k == key {\n\t\t\t\treturn v, true\n\t\t\t}\n\t\t\tif result, ok := searchKey(v, key); ok {\n\t\t\t\treturn result, true\n\t\t\t}\n\t\t}\n\tcase []any:\n\t\tfor _, v := range val {\n\t\t\tif result, ok := searchKey(v, key); ok {\n\t\t\t\treturn result, true\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, false\n}\n\nfunc searchHost(headers any) string {\n\tdata, _ := headers.(map[string]any)\n\tfor k, v := range data {\n\t\tif strings.EqualFold(k, \"host\") {\n\t\t\tswitch v.(type) {\n\t\t\tcase []any:\n\t\t\t\thosts, _ := v.([]any)\n\t\t\t\tif len(hosts) > 0 {\n\t\t\t\t\treturn hosts[0].(string)\n\t\t\t\t} else {\n\t\t\t\t\treturn \"\"\n\t\t\t\t}\n\t\t\tcase any:\n\t\t\t\treturn v.(string)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// PageData is a view model for subpage.html\n// PageData contains data for rendering the subscription information page.\ntype PageData struct {\n\tHost         string\n\tBasePath     string\n\tSId          string\n\tDownload     string\n\tUpload       string\n\tTotal        string\n\tUsed         string\n\tRemained     string\n\tExpire       int64\n\tLastOnline   int64\n\tDatepicker   string\n\tDownloadByte int64\n\tUploadByte   int64\n\tTotalByte    int64\n\tSubUrl       string\n\tSubJsonUrl   string\n\tResult       []string\n}\n\n// ResolveRequest extracts scheme and host info from request/headers consistently.\n// ResolveRequest extracts scheme, host, and header information from an HTTP request.\nfunc (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {\n\t// scheme\n\tscheme = \"http\"\n\tif c.Request.TLS != nil || strings.EqualFold(c.GetHeader(\"X-Forwarded-Proto\"), \"https\") {\n\t\tscheme = \"https\"\n\t}\n\n\t// base host (no port)\n\tif h, err := getHostFromXFH(c.GetHeader(\"X-Forwarded-Host\")); err == nil && h != \"\" {\n\t\thost = h\n\t}\n\tif host == \"\" {\n\t\thost = c.GetHeader(\"X-Real-IP\")\n\t}\n\tif host == \"\" {\n\t\tvar err error\n\t\thost, _, err = net.SplitHostPort(c.Request.Host)\n\t\tif err != nil {\n\t\t\thost = c.Request.Host\n\t\t}\n\t}\n\n\t// host:port for URLs\n\thostWithPort = c.GetHeader(\"X-Forwarded-Host\")\n\tif hostWithPort == \"\" {\n\t\thostWithPort = c.Request.Host\n\t}\n\tif hostWithPort == \"\" {\n\t\thostWithPort = host\n\t}\n\n\t// header display host\n\thostHeader = c.GetHeader(\"X-Forwarded-Host\")\n\tif hostHeader == \"\" {\n\t\thostHeader = c.GetHeader(\"X-Real-IP\")\n\t}\n\tif hostHeader == \"\" {\n\t\thostHeader = host\n\t}\n\treturn\n}\n\n// BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID.\n// It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components.\nfunc (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {\n\t// Input validation\n\tif subId == \"\" {\n\t\treturn \"\", \"\"\n\t}\n\n\t// Get configured URIs first (highest priority)\n\tconfiguredSubURI, _ := s.settingService.GetSubURI()\n\tconfiguredSubJsonURI, _ := s.settingService.GetSubJsonURI()\n\n\t// Determine base scheme and host (cached to avoid duplicate calls)\n\tvar baseScheme, baseHostWithPort string\n\tif configuredSubURI == \"\" || configuredSubJsonURI == \"\" {\n\t\tbaseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)\n\t}\n\n\t// Build subscription URL\n\tsubURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)\n\n\t// Build JSON subscription URL\n\tsubJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)\n\n\treturn subURL, subJsonURL\n}\n\n// getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values\nfunc (s *SubService) getBaseSchemeAndHost(requestScheme, requestHostWithPort string) (string, string) {\n\tsubDomain, err := s.settingService.GetSubDomain()\n\tif err != nil || subDomain == \"\" {\n\t\treturn requestScheme, requestHostWithPort\n\t}\n\n\t// Get port and TLS settings\n\tsubPort, _ := s.settingService.GetSubPort()\n\tsubKeyFile, _ := s.settingService.GetSubKeyFile()\n\tsubCertFile, _ := s.settingService.GetSubCertFile()\n\n\t// Determine scheme from TLS configuration\n\tscheme := \"http\"\n\tif subKeyFile != \"\" && subCertFile != \"\" {\n\t\tscheme = \"https\"\n\t}\n\n\t// Build host:port, always include port for clarity\n\thostWithPort := fmt.Sprintf(\"%s:%d\", subDomain, subPort)\n\n\treturn scheme, hostWithPort\n}\n\n// buildSingleURL constructs a single URL using configured URI or base components\nfunc (s *SubService) buildSingleURL(configuredURI, baseScheme, baseHostWithPort, basePath, subId string) string {\n\tif configuredURI != \"\" {\n\t\treturn s.joinPathWithID(configuredURI, subId)\n\t}\n\n\tbaseURL := fmt.Sprintf(\"%s://%s\", baseScheme, baseHostWithPort)\n\treturn s.joinPathWithID(baseURL+basePath, subId)\n}\n\n// joinPathWithID safely joins a base path with a subscription ID\nfunc (s *SubService) joinPathWithID(basePath, subId string) string {\n\tif strings.HasSuffix(basePath, \"/\") {\n\t\treturn basePath + subId\n\t}\n\treturn basePath + \"/\" + subId\n}\n\n// BuildPageData parses header and prepares the template view model.\n// BuildPageData constructs page data for rendering the subscription information page.\nfunc (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {\n\tdownload := common.FormatTraffic(traffic.Down)\n\tupload := common.FormatTraffic(traffic.Up)\n\ttotal := \"∞\"\n\tused := common.FormatTraffic(traffic.Up + traffic.Down)\n\tremained := \"\"\n\tif traffic.Total > 0 {\n\t\ttotal = common.FormatTraffic(traffic.Total)\n\t\tleft := max(traffic.Total-(traffic.Up+traffic.Down), 0)\n\t\tremained = common.FormatTraffic(left)\n\t}\n\n\tdatepicker := s.datepicker\n\tif datepicker == \"\" {\n\t\tdatepicker = \"gregorian\"\n\t}\n\n\treturn PageData{\n\t\tHost:         hostHeader,\n\t\tBasePath:     basePath,\n\t\tSId:          subId,\n\t\tDownload:     download,\n\t\tUpload:       upload,\n\t\tTotal:        total,\n\t\tUsed:         used,\n\t\tRemained:     remained,\n\t\tExpire:       traffic.ExpiryTime / 1000,\n\t\tLastOnline:   lastOnline,\n\t\tDatepicker:   datepicker,\n\t\tDownloadByte: traffic.Down,\n\t\tUploadByte:   traffic.Up,\n\t\tTotalByte:    traffic.Total,\n\t\tSubUrl:       subURL,\n\t\tSubJsonUrl:   subJsonURL,\n\t\tResult:       subs,\n\t}\n}\n\nfunc getHostFromXFH(s string) (string, error) {\n\tif strings.Contains(s, \":\") {\n\t\trealHost, _, err := net.SplitHostPort(s)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn realHost, nil\n\t}\n\treturn s, nil\n}\n"
  },
  {
    "path": "update.sh",
    "content": "#!/bin/bash\n\nred='\\033[0;31m'\ngreen='\\033[0;32m'\nblue='\\033[0;34m'\nyellow='\\033[0;33m'\nplain='\\033[0m'\n\nxui_folder=\"${XUI_MAIN_FOLDER:=/usr/local/x-ui}\"\nxui_service=\"${XUI_SERVICE:=/etc/systemd/system}\"\n\n# Don't edit this config\nb_source=\"${BASH_SOURCE[0]}\"\nwhile [ -h \"$b_source\" ]; do\n    b_dir=\"$(cd -P \"$(dirname \"$b_source\")\" >/dev/null 2>&1 && pwd || pwd -P)\"\n    b_source=\"$(readlink \"$b_source\")\"\n    [[ $b_source != /* ]] && b_source=\"$b_dir/$b_source\"\ndone\ncur_dir=\"$(cd -P \"$(dirname \"$b_source\")\" >/dev/null 2>&1 && pwd || pwd -P)\"\nscript_name=$(basename \"$0\")\n\n# Check command exist function\n_command_exists() {\n    type \"$1\" &>/dev/null\n}\n\n# Fail, log and exit script function\n_fail() {\n    local msg=${1}\n    echo -e \"${red}${msg}${plain}\"\n    exit 2\n}\n\n# check root\n[[ $EUID -ne 0 ]] && _fail \"FATAL ERROR: Please run this script with root privilege.\"\n\nif _command_exists curl; then\n    curl_bin=$(which curl)\nelse\n    _fail \"ERROR: Command 'curl' not found.\"\nfi\n\n# Check OS and set release variable\nif [[ -f /etc/os-release ]]; then\n    source /etc/os-release\n    release=$ID\n    elif [[ -f /usr/lib/os-release ]]; then\n    source /usr/lib/os-release\n    release=$ID\nelse\n    _fail \"Failed to check the system OS, please contact the author!\"\nfi\necho \"The OS release is: $release\"\n\narch() {\n    case \"$(uname -m)\" in\n        x86_64 | x64 | amd64) echo 'amd64' ;;\n        i*86 | x86) echo '386' ;;\n        armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;\n        armv7* | armv7 | arm) echo 'armv7' ;;\n        armv6* | armv6) echo 'armv6' ;;\n        armv5* | armv5) echo 'armv5' ;;\n        s390x) echo 's390x' ;;\n        *) echo -e \"${red}Unsupported CPU architecture!${plain}\" && rm -f \"${cur_dir}/${script_name}\" >/dev/null 2>&1 && exit 2;;\n    esac\n}\n\necho \"Arch: $(arch)\"\n\n# Simple helpers\nis_ipv4() {\n    [[ \"$1\" =~ ^([0-9]{1,3}\\.){3}[0-9]{1,3}$ ]] && return 0 || return 1\n}\nis_ipv6() {\n    [[ \"$1\" =~ : ]] && return 0 || return 1\n}\nis_ip() {\n    is_ipv4 \"$1\" || is_ipv6 \"$1\"\n}\nis_domain() {\n    [[ \"$1\" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1\n}\n\n# Port helpers\nis_port_in_use() {\n    local port=\"$1\"\n    if command -v ss >/dev/null 2>&1; then\n        ss -ltn 2>/dev/null | awk -v p=\":${port}$\" '$4 ~ p {exit 0} END {exit 1}'\n        return\n    fi\n    if command -v netstat >/dev/null 2>&1; then\n        netstat -lnt 2>/dev/null | awk -v p=\":${port} \" '$4 ~ p {exit 0} END {exit 1}'\n        return\n    fi\n    if command -v lsof >/dev/null 2>&1; then\n        lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0\n    fi\n    return 1\n}\n\ngen_random_string() {\n    local length=\"$1\"\n    local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w \"$length\" | head -n 1)\n    echo \"$random_string\"\n}\n\ninstall_base() {\n    echo -e \"${green}Updating and install dependency packages...${plain}\"\n    case \"${release}\" in\n        ubuntu | debian | armbian)\n            apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat >/dev/null 2>&1\n        ;;\n        fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)\n            dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1\n        ;;\n        centos)\n            if [[ \"${VERSION_ID}\" =~ ^7 ]]; then\n                yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat >/dev/null 2>&1\n            else\n                dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1\n            fi\n        ;;\n        arch | manjaro | parch)\n            pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat >/dev/null 2>&1\n        ;;\n        opensuse-tumbleweed | opensuse-leap)\n            zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat >/dev/null 2>&1\n        ;;\n        alpine)\n            apk update >/dev/null 2>&1 && apk add curl tar tzdata socat >/dev/null 2>&1\n        ;;\n        *)\n            apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat >/dev/null 2>&1\n        ;;\n    esac\n}\n\ninstall_acme() {\n    echo -e \"${green}Installing acme.sh for SSL certificate management...${plain}\"\n    cd ~ || return 1\n    curl -s https://get.acme.sh | sh >/dev/null 2>&1\n    if [ $? -ne 0 ]; then\n        echo -e \"${red}Failed to install acme.sh${plain}\"\n        return 1\n    else\n        echo -e \"${green}acme.sh installed successfully${plain}\"\n    fi\n    return 0\n}\n\nsetup_ssl_certificate() {\n    local domain=\"$1\"\n    local server_ip=\"$2\"\n    local existing_port=\"$3\"\n    local existing_webBasePath=\"$4\"\n    \n    echo -e \"${green}Setting up SSL certificate...${plain}\"\n    \n    # Check if acme.sh is installed\n    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        install_acme\n        if [ $? -ne 0 ]; then\n            echo -e \"${yellow}Failed to install acme.sh, skipping SSL setup${plain}\"\n            return 1\n        fi\n    fi\n    \n    # Create certificate directory\n    local certPath=\"/root/cert/${domain}\"\n    mkdir -p \"$certPath\"\n    \n    # Issue certificate\n    echo -e \"${green}Issuing SSL certificate for ${domain}...${plain}\"\n    echo -e \"${yellow}Note: Port 80 must be open and accessible from the internet${plain}\"\n    \n    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1\n    ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force\n    \n    if [ $? -ne 0 ]; then\n        echo -e \"${yellow}Failed to issue certificate for ${domain}${plain}\"\n        echo -e \"${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}\"\n        rm -rf ~/.acme.sh/${domain} 2>/dev/null\n        rm -rf \"$certPath\" 2>/dev/null\n        return 1\n    fi\n    \n    # Install certificate\n    ~/.acme.sh/acme.sh --installcert -d ${domain} \\\n        --key-file /root/cert/${domain}/privkey.pem \\\n        --fullchain-file /root/cert/${domain}/fullchain.pem \\\n        --reloadcmd \"systemctl restart x-ui\" >/dev/null 2>&1\n    \n    if [ $? -ne 0 ]; then\n        echo -e \"${yellow}Failed to install certificate${plain}\"\n        return 1\n    fi\n    \n    # Enable auto-renew\n    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1\n    chmod 600 $certPath/privkey.pem 2>/dev/null\n    chmod 644 $certPath/fullchain.pem 2>/dev/null\n    \n    # Set certificate for panel\n    local webCertFile=\"/root/cert/${domain}/fullchain.pem\"\n    local webKeyFile=\"/root/cert/${domain}/privkey.pem\"\n    \n    if [[ -f \"$webCertFile\" && -f \"$webKeyFile\" ]]; then\n        ${xui_folder}/x-ui cert -webCert \"$webCertFile\" -webCertKey \"$webKeyFile\" >/dev/null 2>&1\n        echo -e \"${green}SSL certificate installed and configured successfully!${plain}\"\n        return 0\n    else\n        echo -e \"${yellow}Certificate files not found${plain}\"\n        return 1\n    fi\n}\n\n# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity)\n# Requires acme.sh and port 80 open for HTTP-01 challenge\nsetup_ip_certificate() {\n    local ipv4=\"$1\"\n    local ipv6=\"$2\"  # optional\n\n    echo -e \"${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}\"\n    echo -e \"${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}\"\n    echo -e \"${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}\"\n\n    # Check for acme.sh\n    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        install_acme\n        if [ $? -ne 0 ]; then\n            echo -e \"${red}Failed to install acme.sh${plain}\"\n            return 1\n        fi\n    fi\n\n    # Validate IP address\n    if [[ -z \"$ipv4\" ]]; then\n        echo -e \"${red}IPv4 address is required${plain}\"\n        return 1\n    fi\n\n    if ! is_ipv4 \"$ipv4\"; then\n        echo -e \"${red}Invalid IPv4 address: $ipv4${plain}\"\n        return 1\n    fi\n\n    # Create certificate directory\n    local certDir=\"/root/cert/ip\"\n    mkdir -p \"$certDir\"\n\n    # Build domain arguments\n    local domain_args=\"-d ${ipv4}\"\n    if [[ -n \"$ipv6\" ]] && is_ipv6 \"$ipv6\"; then\n        domain_args=\"${domain_args} -d ${ipv6}\"\n        echo -e \"${green}Including IPv6 address: ${ipv6}${plain}\"\n    fi\n\n    # Set reload command for auto-renewal (add || true so it doesn't fail if service stopped)\n    local reloadCmd=\"systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true\"\n\n    # Choose port for HTTP-01 listener (default 80, prompt override)\n    local WebPort=\"\"\n    read -rp \"Port to use for ACME HTTP-01 listener (default 80): \" WebPort\n    WebPort=\"${WebPort:-80}\"\n    if ! [[ \"${WebPort}\" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then\n        echo -e \"${red}Invalid port provided. Falling back to 80.${plain}\"\n        WebPort=80\n    fi\n    echo -e \"${green}Using port ${WebPort} for standalone validation.${plain}\"\n    if [[ \"${WebPort}\" -ne 80 ]]; then\n        echo -e \"${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}\"\n    fi\n\n    # Ensure chosen port is available\n    while true; do\n        if is_port_in_use \"${WebPort}\"; then\n            echo -e \"${yellow}Port ${WebPort} is currently in use.${plain}\"\n\n            local alt_port=\"\"\n            read -rp \"Enter another port for acme.sh standalone listener (leave empty to abort): \" alt_port\n            alt_port=\"${alt_port// /}\"\n            if [[ -z \"${alt_port}\" ]]; then\n                echo -e \"${red}Port ${WebPort} is busy; cannot proceed.${plain}\"\n                return 1\n            fi\n            if ! [[ \"${alt_port}\" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then\n                echo -e \"${red}Invalid port provided.${plain}\"\n                return 1\n            fi\n            WebPort=\"${alt_port}\"\n            continue\n        else\n            echo -e \"${green}Port ${WebPort} is free and ready for standalone validation.${plain}\"\n            break\n        fi\n    done\n\n    # Issue certificate with shortlived profile\n    echo -e \"${green}Issuing IP certificate for ${ipv4}...${plain}\"\n    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1\n    \n    ~/.acme.sh/acme.sh --issue \\\n        ${domain_args} \\\n        --standalone \\\n        --server letsencrypt \\\n        --certificate-profile shortlived \\\n        --days 6 \\\n        --httpport ${WebPort} \\\n        --force\n\n    if [ $? -ne 0 ]; then\n        echo -e \"${red}Failed to issue IP certificate${plain}\"\n        echo -e \"${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}\"\n        # Cleanup acme.sh data for both IPv4 and IPv6 if specified\n        rm -rf ~/.acme.sh/${ipv4} 2>/dev/null\n        [[ -n \"$ipv6\" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null\n        rm -rf ${certDir} 2>/dev/null\n        return 1\n    fi\n\n    echo -e \"${green}Certificate issued successfully, installing...${plain}\"\n\n    # Install certificate\n    # Note: acme.sh may report \"Reload error\" and exit non-zero if reloadcmd fails,\n    # but the cert files are still installed. We check for files instead of exit code.\n    ~/.acme.sh/acme.sh --installcert -d ${ipv4} \\\n        --key-file \"${certDir}/privkey.pem\" \\\n        --fullchain-file \"${certDir}/fullchain.pem\" \\\n        --reloadcmd \"${reloadCmd}\" 2>&1 || true\n\n    # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)\n    if [[ ! -f \"${certDir}/fullchain.pem\" || ! -f \"${certDir}/privkey.pem\" ]]; then\n        echo -e \"${red}Certificate files not found after installation${plain}\"\n        # Cleanup acme.sh data for both IPv4 and IPv6 if specified\n        rm -rf ~/.acme.sh/${ipv4} 2>/dev/null\n        [[ -n \"$ipv6\" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null\n        rm -rf ${certDir} 2>/dev/null\n        return 1\n    fi\n    \n    echo -e \"${green}Certificate files installed successfully${plain}\"\n\n    # Enable auto-upgrade for acme.sh (ensures cron job runs)\n    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1\n\n    chmod 600 ${certDir}/privkey.pem 2>/dev/null\n    chmod 644 ${certDir}/fullchain.pem 2>/dev/null\n\n    # Configure panel to use the certificate\n    echo -e \"${green}Setting certificate paths for the panel...${plain}\"\n    ${xui_folder}/x-ui cert -webCert \"${certDir}/fullchain.pem\" -webCertKey \"${certDir}/privkey.pem\"\n    if [ $? -ne 0 ]; then\n        echo -e \"${yellow}Warning: Could not set certificate paths automatically.${plain}\"\n        echo -e \"${yellow}You may need to set them manually in the panel settings.${plain}\"\n        echo -e \"${yellow}Cert path: ${certDir}/fullchain.pem${plain}\"\n        echo -e \"${yellow}Key path: ${certDir}/privkey.pem${plain}\"\n    else\n        echo -e \"${green}Certificate paths set successfully!${plain}\"\n    fi\n\n    echo -e \"${green}IP certificate installed and configured successfully!${plain}\"\n    echo -e \"${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}\"\n    echo -e \"${yellow}Panel will automatically restart after each renewal.${plain}\"\n    return 0\n}\n\n# Comprehensive manual SSL certificate issuance via acme.sh\nssl_cert_issue() {\n    local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')\n    local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')\n    \n    # check for acme.sh first\n    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        echo \"acme.sh could not be found. Installing now...\"\n        cd ~ || return 1\n        curl -s https://get.acme.sh | sh\n        if [ $? -ne 0 ]; then\n            echo -e \"${red}Failed to install acme.sh${plain}\"\n            return 1\n        else\n            echo -e \"${green}acme.sh installed successfully${plain}\"\n        fi\n    fi\n\n    # get the domain here, and we need to verify it\n    local domain=\"\"\n    while true; do\n        read -rp \"Please enter your domain name: \" domain\n        domain=\"${domain// /}\"  # Trim whitespace\n        \n        if [[ -z \"$domain\" ]]; then\n            echo -e \"${red}Domain name cannot be empty. Please try again.${plain}\"\n            continue\n        fi\n        \n        if ! is_domain \"$domain\"; then\n            echo -e \"${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}\"\n            continue\n        fi\n        \n        break\n    done\n    echo -e \"${green}Your domain is: ${domain}, checking it...${plain}\"\n\n    # check if there already exists a certificate\n    local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')\n    if [ \"${currentCert}\" == \"${domain}\" ]; then\n        local certInfo=$(~/.acme.sh/acme.sh --list)\n        echo -e \"${red}System already has certificates for this domain. Cannot issue again.${plain}\"\n        echo -e \"${yellow}Current certificate details:${plain}\"\n        echo \"$certInfo\"\n        return 1\n    else\n        echo -e \"${green}Your domain is ready for issuing certificates now...${plain}\"\n    fi\n\n    # create a directory for the certificate\n    certPath=\"/root/cert/${domain}\"\n    if [ ! -d \"$certPath\" ]; then\n        mkdir -p \"$certPath\"\n    else\n        rm -rf \"$certPath\"\n        mkdir -p \"$certPath\"\n    fi\n\n    # get the port number for the standalone server\n    local WebPort=80\n    read -rp \"Please choose which port to use (default is 80): \" WebPort\n    if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then\n        echo -e \"${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}\"\n        WebPort=80\n    fi\n    echo -e \"${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}\"\n\n    # Stop panel temporarily\n    echo -e \"${yellow}Stopping panel temporarily...${plain}\"\n    systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null\n\n    # issue the certificate\n    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force\n    ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force\n    if [ $? -ne 0 ]; then\n        echo -e \"${red}Issuing certificate failed, please check logs.${plain}\"\n        rm -rf ~/.acme.sh/${domain}\n        systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null\n        return 1\n    else\n        echo -e \"${green}Issuing certificate succeeded, installing certificates...${plain}\"\n    fi\n\n    # Setup reload command\n    reloadCmd=\"systemctl restart x-ui || rc-service x-ui restart\"\n    echo -e \"${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}\"\n    echo -e \"${green}This command will run on every certificate issue and renew.${plain}\"\n    read -rp \"Would you like to modify --reloadcmd for ACME? (y/n): \" setReloadcmd\n    if [[ \"$setReloadcmd\" == \"y\" || \"$setReloadcmd\" == \"Y\" ]]; then\n        echo -e \"\\n${green}\\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui\"\n        echo -e \"${green}\\t2.${plain} Input your own command\"\n        echo -e \"${green}\\t0.${plain} Keep default reloadcmd\"\n        read -rp \"Choose an option: \" choice\n        case \"$choice\" in\n        1)\n            echo -e \"${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}\"\n            reloadCmd=\"systemctl reload nginx ; systemctl restart x-ui\"\n            ;;\n        2)\n            echo -e \"${yellow}It's recommended to put x-ui restart at the end${plain}\"\n            read -rp \"Please enter your custom reloadcmd: \" reloadCmd\n            echo -e \"${green}Reloadcmd is: ${reloadCmd}${plain}\"\n            ;;\n        *)\n            echo -e \"${green}Keeping default reloadcmd${plain}\"\n            ;;\n        esac\n    fi\n\n    # install the certificate\n    ~/.acme.sh/acme.sh --installcert -d ${domain} \\\n        --key-file /root/cert/${domain}/privkey.pem \\\n        --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd \"${reloadCmd}\"\n\n    if [ $? -ne 0 ]; then\n        echo -e \"${red}Installing certificate failed, exiting.${plain}\"\n        rm -rf ~/.acme.sh/${domain}\n        systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null\n        return 1\n    else\n        echo -e \"${green}Installing certificate succeeded, enabling auto renew...${plain}\"\n    fi\n\n    # enable auto-renew\n    ~/.acme.sh/acme.sh --upgrade --auto-upgrade\n    if [ $? -ne 0 ]; then\n        echo -e \"${yellow}Auto renew setup had issues, certificate details:${plain}\"\n        ls -lah /root/cert/${domain}/\n        chmod 600 $certPath/privkey.pem\n        chmod 644 $certPath/fullchain.pem\n    else\n        echo -e \"${green}Auto renew succeeded, certificate details:${plain}\"\n        ls -lah /root/cert/${domain}/\n        chmod 600 $certPath/privkey.pem\n        chmod 644 $certPath/fullchain.pem\n    fi\n\n    # Restart panel\n    systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null\n\n    # Prompt user to set panel paths after successful certificate installation\n    read -rp \"Would you like to set this certificate for the panel? (y/n): \" setPanel\n    if [[ \"$setPanel\" == \"y\" || \"$setPanel\" == \"Y\" ]]; then\n        local webCertFile=\"/root/cert/${domain}/fullchain.pem\"\n        local webKeyFile=\"/root/cert/${domain}/privkey.pem\"\n\n        if [[ -f \"$webCertFile\" && -f \"$webKeyFile\" ]]; then\n            ${xui_folder}/x-ui cert -webCert \"$webCertFile\" -webCertKey \"$webKeyFile\"\n            echo -e \"${green}Certificate paths set for the panel${plain}\"\n            echo -e \"${green}Certificate File: $webCertFile${plain}\"\n            echo -e \"${green}Private Key File: $webKeyFile${plain}\"\n            echo \"\"\n            echo -e \"${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}\"\n            echo -e \"${yellow}Panel will restart to apply SSL certificate...${plain}\"\n            systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null\n        else\n            echo -e \"${red}Error: Certificate or private key file not found for domain: $domain.${plain}\"\n        fi\n    else\n        echo -e \"${yellow}Skipping panel path setting.${plain}\"\n    fi\n    \n    return 0\n}\n# Unified interactive SSL setup (domain or IP)\n# Sets global `SSL_HOST` to the chosen domain/IP\nprompt_and_setup_ssl() {\n    local panel_port=\"$1\"\n    local web_base_path=\"$2\"   # expected without leading slash\n    local server_ip=\"$3\"\n\n    local ssl_choice=\"\"\n\n    echo -e \"${yellow}Choose SSL certificate setup method:${plain}\"\n    echo -e \"${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)\"\n    echo -e \"${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)\"\n    echo -e \"${green}3.${plain} Custom SSL Certificate (Path to existing files)\"\n    echo -e \"${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths.\"\n    read -rp \"Choose an option (default 2 for IP): \" ssl_choice\n    ssl_choice=\"${ssl_choice// /}\"  # Trim whitespace\n    \n    # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)\n    if [[ \"$ssl_choice\" != \"1\" && \"$ssl_choice\" != \"3\" ]]; then\n        ssl_choice=\"2\"\n    fi\n\n    case \"$ssl_choice\" in\n    1)\n        # User chose Let's Encrypt domain option\n        echo -e \"${green}Using Let's Encrypt for domain certificate...${plain}\"\n        ssl_cert_issue\n        # Extract the domain that was used from the certificate\n        local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')\n        if [[ -n \"${cert_domain}\" ]]; then\n            SSL_HOST=\"${cert_domain}\"\n            echo -e \"${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}\"\n        else\n            echo -e \"${yellow}SSL setup may have completed, but domain extraction failed${plain}\"\n            SSL_HOST=\"${server_ip}\"\n        fi\n        ;;\n    2)\n        # User chose Let's Encrypt IP certificate option\n        echo -e \"${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}\"\n        \n        # Ask for optional IPv6\n        local ipv6_addr=\"\"\n        read -rp \"Do you have an IPv6 address to include? (leave empty to skip): \" ipv6_addr\n        ipv6_addr=\"${ipv6_addr// /}\"  # Trim whitespace\n        \n        # Stop panel if running (port 80 needed)\n        if [[ $release == \"alpine\" ]]; then\n            rc-service x-ui stop >/dev/null 2>&1\n        else\n            systemctl stop x-ui >/dev/null 2>&1\n        fi\n        \n        setup_ip_certificate \"${server_ip}\" \"${ipv6_addr}\"\n        if [ $? -eq 0 ]; then\n            SSL_HOST=\"${server_ip}\"\n            echo -e \"${green}✓ Let's Encrypt IP certificate configured successfully${plain}\"\n        else\n            echo -e \"${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}\"\n            SSL_HOST=\"${server_ip}\"\n        fi\n        \n        # Restart panel after SSL is configured (restart applies new cert settings)\n        if [[ $release == \"alpine\" ]]; then\n            rc-service x-ui restart >/dev/null 2>&1\n        else\n            systemctl restart x-ui >/dev/null 2>&1\n        fi\n\n        ;;\n    3)\n        # User chose Custom Paths (User Provided) option\n        echo -e \"${green}Using custom existing certificate...${plain}\"\n        local custom_cert=\"\"\n        local custom_key=\"\"\n        local custom_domain=\"\"\n\n        # 3.1 Request Domain to compose Panel URL later\n        read -rp \"Please enter domain name certificate issued for: \" custom_domain\n        custom_domain=\"${custom_domain// /}\" # Убираем пробелы\n\n        # 3.2 Loop for Certificate Path\n        while true; do\n            read -rp \"Input certificate path (keywords: .crt / fullchain): \" custom_cert\n            # Strip quotes if present\n            custom_cert=$(echo \"$custom_cert\" | tr -d '\"' | tr -d \"'\")\n\n            if [[ -f \"$custom_cert\" && -r \"$custom_cert\" && -s \"$custom_cert\" ]]; then\n                break\n            elif [[ ! -f \"$custom_cert\" ]]; then\n                echo -e \"${red}Error: File does not exist! Try again.${plain}\"\n            elif [[ ! -r \"$custom_cert\" ]]; then\n                echo -e \"${red}Error: File exists but is not readable (check permissions)!${plain}\"\n            else\n                echo -e \"${red}Error: File is empty!${plain}\"\n            fi\n        done\n\n        # 3.3 Loop for Private Key Path\n        while true; do\n            read -rp \"Input private key path (keywords: .key / privatekey): \" custom_key\n            # Strip quotes if present\n            custom_key=$(echo \"$custom_key\" | tr -d '\"' | tr -d \"'\")\n\n            if [[ -f \"$custom_key\" && -r \"$custom_key\" && -s \"$custom_key\" ]]; then\n                break\n            elif [[ ! -f \"$custom_key\" ]]; then\n                echo -e \"${red}Error: File does not exist! Try again.${plain}\"\n            elif [[ ! -r \"$custom_key\" ]]; then\n                echo -e \"${red}Error: File exists but is not readable (check permissions)!${plain}\"\n            else\n                echo -e \"${red}Error: File is empty!${plain}\"\n            fi\n        done\n\n        # 3.4 Apply Settings via x-ui binary\n        ${xui_folder}/x-ui cert -webCert \"$custom_cert\" -webCertKey \"$custom_key\" >/dev/null 2>&1\n\n        # Set SSL_HOST for composing Panel URL\n        if [[ -n \"$custom_domain\" ]]; then\n            SSL_HOST=\"$custom_domain\"\n        else\n            SSL_HOST=\"${server_ip}\"\n        fi\n\n        echo -e \"${green}✓ Custom certificate paths applied.${plain}\"\n        echo -e \"${yellow}Note: You are responsible for renewing these files externally.${plain}\"\n\n        systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1\n        ;;\n    *)\n        echo -e \"${red}Invalid option. Skipping SSL setup.${plain}\"\n        SSL_HOST=\"${server_ip}\"\n        ;;\n    esac\n}\n\nconfig_after_update() {\n    echo -e \"${yellow}x-ui settings:${plain}\"\n    ${xui_folder}/x-ui setting -show true\n    ${xui_folder}/x-ui migrate\n    \n    # Properly detect empty cert by checking if cert: line exists and has content after it\n    local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')\n    local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')\n    local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')\n    \n    # Get server IP\n    local URL_lists=(\n        \"https://api4.ipify.org\"\n        \"https://ipv4.icanhazip.com\"\n        \"https://v4.api.ipinfo.io/ip\"\n        \"https://ipv4.myexternalip.com/raw\"\n        \"https://4.ident.me\"\n        \"https://check-host.net/ip\"\n    )\n    local server_ip=\"\"\n    for ip_address in \"${URL_lists[@]}\"; do\n        local response=$(curl -s -w \"\\n%{http_code}\" --max-time 3 \"${ip_address}\" 2>/dev/null)\n        local http_code=$(echo \"$response\" | tail -n1)\n        local ip_result=$(echo \"$response\" | head -n-1 | tr -d '[:space:]')\n        if [[ \"${http_code}\" == \"200\" && -n \"${ip_result}\" ]]; then\n            server_ip=\"${ip_result}\"\n            break\n        fi\n    done\n    \n    # Handle missing/short webBasePath\n    if [[ ${#existing_webBasePath} -lt 4 ]]; then\n        echo -e \"${yellow}WebBasePath is missing or too short. Generating a new one...${plain}\"\n        local config_webBasePath=$(gen_random_string 18)\n        ${xui_folder}/x-ui setting -webBasePath \"${config_webBasePath}\"\n        existing_webBasePath=\"${config_webBasePath}\"\n        echo -e \"${green}New WebBasePath: ${config_webBasePath}${plain}\"\n    fi\n    \n    # Check and prompt for SSL if missing\n    if [[ -z \"$existing_cert\" ]]; then\n        echo \"\"\n        echo -e \"${red}═══════════════════════════════════════════${plain}\"\n        echo -e \"${red}      ⚠ NO SSL CERTIFICATE DETECTED ⚠     ${plain}\"\n        echo -e \"${red}═══════════════════════════════════════════${plain}\"\n        echo -e \"${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}\"\n        echo -e \"${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}\"\n        echo \"\"\n        \n        if [[ -z \"${server_ip}\" ]]; then\n            echo -e \"${red}Failed to detect server IP${plain}\"\n            echo -e \"${yellow}Please configure SSL manually using: x-ui${plain}\"\n            return\n        fi\n        \n        # Prompt and setup SSL (domain or IP)\n        prompt_and_setup_ssl \"${existing_port}\" \"${existing_webBasePath}\" \"${server_ip}\"\n        \n        echo \"\"\n        echo -e \"${green}═══════════════════════════════════════════${plain}\"\n        echo -e \"${green}     Panel Access Information              ${plain}\"\n        echo -e \"${green}═══════════════════════════════════════════${plain}\"\n        echo -e \"${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}\"\n        echo -e \"${green}═══════════════════════════════════════════${plain}\"\n        echo -e \"${yellow}⚠ SSL Certificate: Enabled and configured${plain}\"\n    else\n        echo -e \"${green}SSL certificate is already configured${plain}\"\n        # Show access URL with existing certificate\n        local cert_domain=$(basename \"$(dirname \"$existing_cert\")\")\n        echo \"\"\n        echo -e \"${green}═══════════════════════════════════════════${plain}\"\n        echo -e \"${green}     Panel Access Information              ${plain}\"\n        echo -e \"${green}═══════════════════════════════════════════${plain}\"\n        echo -e \"${green}Access URL: https://${cert_domain}:${existing_port}/${existing_webBasePath}${plain}\"\n        echo -e \"${green}═══════════════════════════════════════════${plain}\"\n    fi\n}\n\nupdate_x-ui() {\n    cd ${xui_folder%/x-ui}/\n    \n    if [ -f \"${xui_folder}/x-ui\" ]; then\n        current_xui_version=$(${xui_folder}/x-ui -v)\n        echo -e \"${green}Current x-ui version: ${current_xui_version}${plain}\"\n    else\n        _fail \"ERROR: Current x-ui version: unknown\"\n    fi\n    \n    echo -e \"${green}Downloading new x-ui version...${plain}\"\n    \n    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/')\n    if [[ ! -n \"$tag_version\" ]]; then\n        echo -e \"${yellow}Trying to fetch version with IPv4...${plain}\"\n        tag_version=$(${curl_bin} -4 -Ls \"https://api.github.com/repos/MHSanaei/3x-ui/releases/latest\" | grep '\"tag_name\":' | sed -E 's/.*\"([^\"]+)\".*/\\1/')\n        if [[ ! -n \"$tag_version\" ]]; then\n            _fail \"ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later\"\n        fi\n    fi\n    echo -e \"Got x-ui latest version: ${tag_version}, beginning the installation...\"\n    ${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\n    if [[ $? -ne 0 ]]; then\n        echo -e \"${yellow}Trying to fetch version with IPv4...${plain}\"\n        ${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\n        if [[ $? -ne 0 ]]; then\n            _fail \"ERROR: Failed to download x-ui, please be sure that your server can access GitHub\"\n        fi\n    fi\n    \n    if [[ -e ${xui_folder}/ ]]; then\n        echo -e \"${green}Stopping x-ui...${plain}\"\n        if [[ $release == \"alpine\" ]]; then\n            if [ -f \"/etc/init.d/x-ui\" ]; then\n                rc-service x-ui stop >/dev/null 2>&1\n                rc-update del x-ui >/dev/null 2>&1\n                echo -e \"${green}Removing old service unit version...${plain}\"\n                rm -f /etc/init.d/x-ui >/dev/null 2>&1\n            else\n                rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1\n                _fail \"ERROR: x-ui service unit not installed.\"\n            fi\n        else\n            if [ -f \"${xui_service}/x-ui.service\" ]; then\n                systemctl stop x-ui >/dev/null 2>&1\n                systemctl disable x-ui >/dev/null 2>&1\n                echo -e \"${green}Removing old systemd unit version...${plain}\"\n                rm ${xui_service}/x-ui.service -f >/dev/null 2>&1\n                systemctl daemon-reload >/dev/null 2>&1\n            else\n                rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1\n                _fail \"ERROR: x-ui systemd unit not installed.\"\n            fi\n        fi\n        echo -e \"${green}Removing old x-ui version...${plain}\"\n        rm ${xui_folder} -f >/dev/null 2>&1\n        rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1\n        rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1\n        rm ${xui_folder}/x-ui.service.arch -f >/dev/null 2>&1\n        rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1\n        rm ${xui_folder}/x-ui -f >/dev/null 2>&1\n        rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1\n        echo -e \"${green}Removing old xray version...${plain}\"\n        rm ${xui_folder}/bin/xray-linux-amd64 -f >/dev/null 2>&1\n        echo -e \"${green}Removing old README and LICENSE file...${plain}\"\n        rm ${xui_folder}/bin/README.md -f >/dev/null 2>&1\n        rm ${xui_folder}/bin/LICENSE -f >/dev/null 2>&1\n    else\n        rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1\n        _fail \"ERROR: x-ui not installed.\"\n    fi\n    \n    echo -e \"${green}Installing new x-ui version...${plain}\"\n    tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1\n    rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1\n    cd x-ui >/dev/null 2>&1\n    chmod +x x-ui >/dev/null 2>&1\n    \n    # Check the system's architecture and rename the file accordingly\n    if [[ $(arch) == \"armv5\" || $(arch) == \"armv6\" || $(arch) == \"armv7\" ]]; then\n        mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1\n        chmod +x bin/xray-linux-arm >/dev/null 2>&1\n    fi\n    \n    chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1\n    \n    echo -e \"${green}Downloading and installing x-ui.sh script...${plain}\"\n    ${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1\n    if [[ $? -ne 0 ]]; then\n        echo -e \"${yellow}Trying to fetch x-ui with IPv4...${plain}\"\n        ${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1\n        if [[ $? -ne 0 ]]; then\n            _fail \"ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub\"\n        fi\n    fi\n    \n    chmod +x ${xui_folder}/x-ui.sh >/dev/null 2>&1\n    chmod +x /usr/bin/x-ui >/dev/null 2>&1\n    mkdir -p /var/log/x-ui >/dev/null 2>&1\n    \n    echo -e \"${green}Changing owner...${plain}\"\n    chown -R root:root ${xui_folder} >/dev/null 2>&1\n    \n    if [ -f \"${xui_folder}/bin/config.json\" ]; then\n        echo -e \"${green}Changing on config file permissions...${plain}\"\n        chmod 640 ${xui_folder}/bin/config.json >/dev/null 2>&1\n    fi\n    \n    if [[ $release == \"alpine\" ]]; then\n        echo -e \"${green}Downloading and installing startup unit x-ui.rc...${plain}\"\n        ${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1\n        if [[ $? -ne 0 ]]; then\n            ${curl_bin} -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1\n            if [[ $? -ne 0 ]]; then\n                _fail \"ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub\"\n            fi\n        fi\n        chmod +x /etc/init.d/x-ui >/dev/null 2>&1\n        chown root:root /etc/init.d/x-ui >/dev/null 2>&1\n        rc-update add x-ui >/dev/null 2>&1\n        rc-service x-ui start >/dev/null 2>&1\n    else\n        if [ -f \"x-ui.service\" ]; then\n            echo -e \"${green}Installing systemd unit...${plain}\"\n            cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1\n            if [[ $? -ne 0 ]]; then\n                echo -e \"${red}Failed to copy x-ui.service${plain}\"\n                exit 1\n            fi\n        else\n            service_installed=false\n            case \"${release}\" in\n                ubuntu | debian | armbian)\n                    if [ -f \"x-ui.service.debian\" ]; then\n                        echo -e \"${green}Installing debian-like systemd unit...${plain}\"\n                        cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1\n                        if [[ $? -eq 0 ]]; then\n                            service_installed=true\n                        fi\n                    fi\n                ;;\n                arch | manjaro | parch)\n                    if [ -f \"x-ui.service.arch\" ]; then\n                        echo -e \"${green}Installing arch-like systemd unit...${plain}\"\n                        cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1\n                        if [[ $? -eq 0 ]]; then\n                            service_installed=true\n                        fi\n                    fi\n                ;;\n                *)\n                    if [ -f \"x-ui.service.rhel\" ]; then\n                        echo -e \"${green}Installing rhel-like systemd unit...${plain}\"\n                        cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1\n                        if [[ $? -eq 0 ]]; then\n                            service_installed=true\n                        fi\n                    fi\n                ;;\n            esac\n            \n            # If service file not found in tar.gz, download from GitHub\n            if [ \"$service_installed\" = false ]; then\n                echo -e \"${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}\"\n                case \"${release}\" in\n                    ubuntu | debian | armbian)\n                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1\n                    ;;\n                    arch | manjaro | parch)\n                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1\n                    ;;\n                    *)\n                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1\n                    ;;\n                esac\n                \n                if [[ $? -ne 0 ]]; then\n                    echo -e \"${red}Failed to install x-ui.service from GitHub${plain}\"\n                    exit 1\n                fi\n            fi\n        fi\n        chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1\n        chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1\n        systemctl daemon-reload >/dev/null 2>&1\n        systemctl enable x-ui >/dev/null 2>&1\n        systemctl start x-ui >/dev/null 2>&1\n    fi\n    \n    config_after_update\n    \n    echo -e \"${green}x-ui ${tag_version}${plain} updating finished, it is running now...\"\n    echo -e \"\"\n    echo -e \"┌───────────────────────────────────────────────────────┐\n│  ${blue}x-ui control menu usages (subcommands):${plain}              │\n│                                                       │\n│  ${blue}x-ui${plain}              - Admin Management Script          │\n│  ${blue}x-ui start${plain}        - Start                            │\n│  ${blue}x-ui stop${plain}         - Stop                             │\n│  ${blue}x-ui restart${plain}      - Restart                          │\n│  ${blue}x-ui status${plain}       - Current Status                   │\n│  ${blue}x-ui settings${plain}     - Current Settings                 │\n│  ${blue}x-ui enable${plain}       - Enable Autostart on OS Startup   │\n│  ${blue}x-ui disable${plain}      - Disable Autostart on OS Startup  │\n│  ${blue}x-ui log${plain}          - Check logs                       │\n│  ${blue}x-ui banlog${plain}       - Check Fail2ban ban logs          │\n│  ${blue}x-ui update${plain}       - Update                           │\n│  ${blue}x-ui legacy${plain}       - Legacy version                   │\n│  ${blue}x-ui install${plain}      - Install                          │\n│  ${blue}x-ui uninstall${plain}    - Uninstall                        │\n└───────────────────────────────────────────────────────┘\"\n}\n\necho -e \"${green}Running...${plain}\"\ninstall_base\nupdate_x-ui $1\n"
  },
  {
    "path": "util/common/err.go",
    "content": "// Package common provides common utility functions for error handling, formatting, and multi-error management.\npackage common\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n)\n\n// NewErrorf creates a new error with formatted message.\nfunc NewErrorf(format string, a ...any) error {\n\tmsg := fmt.Sprintf(format, a...)\n\treturn errors.New(msg)\n}\n\n// NewError creates a new error from the given arguments.\nfunc NewError(a ...any) error {\n\tmsg := fmt.Sprintln(a...)\n\treturn errors.New(msg)\n}\n\n// Recover handles panic recovery and logs the panic error if a message is provided.\nfunc Recover(msg string) any {\n\tpanicErr := recover()\n\tif panicErr != nil {\n\t\tif msg != \"\" {\n\t\t\tlogger.Error(msg, \"panic:\", panicErr)\n\t\t}\n\t}\n\treturn panicErr\n}\n"
  },
  {
    "path": "util/common/format.go",
    "content": "package common\n\nimport (\n\t\"fmt\"\n)\n\n// FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB).\nfunc FormatTraffic(trafficBytes int64) string {\n\tunits := []string{\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"}\n\tunitIndex := 0\n\tsize := float64(trafficBytes)\n\n\tfor size >= 1024 && unitIndex < len(units)-1 {\n\t\tsize /= 1024\n\t\tunitIndex++\n\t}\n\treturn fmt.Sprintf(\"%.2f%s\", size, units[unitIndex])\n}\n"
  },
  {
    "path": "util/common/multi_error.go",
    "content": "package common\n\nimport (\n\t\"strings\"\n)\n\n// multiError represents a collection of errors.\ntype multiError []error\n\n// Error returns a string representation of all errors joined with \" | \".\nfunc (e multiError) Error() string {\n\tvar r strings.Builder\n\tr.WriteString(\"multierr: \")\n\tfor _, err := range e {\n\t\tr.WriteString(err.Error())\n\t\tr.WriteString(\" | \")\n\t}\n\treturn r.String()\n}\n\n// Combine combines multiple errors into a single error, filtering out nil errors.\nfunc Combine(maybeError ...error) error {\n\tvar errs multiError\n\tfor _, err := range maybeError {\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\tif len(errs) == 0 {\n\t\treturn nil\n\t}\n\treturn errs\n}\n"
  },
  {
    "path": "util/crypto/crypto.go",
    "content": "// Package crypto provides cryptographic utilities for password hashing and verification.\npackage crypto\n\nimport (\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\n// HashPasswordAsBcrypt generates a bcrypt hash of the given password.\nfunc HashPasswordAsBcrypt(password string) (string, error) {\n\thash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)\n\treturn string(hash), err\n}\n\n// CheckPasswordHash verifies if the given password matches the bcrypt hash.\nfunc CheckPasswordHash(hash, password string) bool {\n\treturn bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil\n}\n"
  },
  {
    "path": "util/json_util/json.go",
    "content": "// Package json_util provides JSON utilities including a custom RawMessage type.\npackage json_util\n\nimport (\n\t\"errors\"\n)\n\n// RawMessage is a custom JSON raw message type that marshals empty slices as \"null\".\ntype RawMessage []byte\n\n// MarshalJSON customizes the JSON marshaling behavior for RawMessage.\n// Empty RawMessage values are marshaled as \"null\" instead of \"[]\".\nfunc (m RawMessage) MarshalJSON() ([]byte, error) {\n\tif len(m) == 0 {\n\t\treturn []byte(\"null\"), nil\n\t}\n\treturn m, nil\n}\n\n// UnmarshalJSON sets *m to a copy of the JSON data.\nfunc (m *RawMessage) UnmarshalJSON(data []byte) error {\n\tif m == nil {\n\t\treturn errors.New(\"json.RawMessage: UnmarshalJSON on nil pointer\")\n\t}\n\t*m = append((*m)[0:0], data...)\n\treturn nil\n}\n"
  },
  {
    "path": "util/ldap/ldap.go",
    "content": "package ldaputil\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\n\t\"github.com/go-ldap/ldap/v3\"\n)\n\ntype Config struct {\n\tHost       string\n\tPort       int\n\tUseTLS     bool\n\tBindDN     string\n\tPassword   string\n\tBaseDN     string\n\tUserFilter string\n\tUserAttr   string\n\tFlagField  string\n\tTruthyVals []string\n\tInvert     bool\n}\n\n// FetchVlessFlags returns map[email]enabled\nfunc FetchVlessFlags(cfg Config) (map[string]bool, error) {\n\taddr := fmt.Sprintf(\"%s:%d\", cfg.Host, cfg.Port)\n\n\tscheme := \"ldap\"\n\tif cfg.UseTLS {\n\t\tscheme = \"ldaps\"\n\t}\n\n\tldapURL := fmt.Sprintf(\"%s://%s\", scheme, addr)\n\n\tvar opts []ldap.DialOpt\n\tif cfg.UseTLS {\n\t\topts = append(opts, ldap.DialWithTLSConfig(&tls.Config{\n\t\t\tInsecureSkipVerify: false,\n\t\t}))\n\t}\n\n\tconn, err := ldap.DialURL(ldapURL, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer conn.Close()\n\n\tif cfg.BindDN != \"\" {\n\t\tif err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif cfg.UserFilter == \"\" {\n\t\tcfg.UserFilter = \"(objectClass=person)\"\n\t}\n\tif cfg.UserAttr == \"\" {\n\t\tcfg.UserAttr = \"mail\"\n\t}\n\t// if field not set we fallback to legacy vless_enabled\n\tif cfg.FlagField == \"\" {\n\t\tcfg.FlagField = \"vless_enabled\"\n\t}\n\n\treq := ldap.NewSearchRequest(\n\t\tcfg.BaseDN,\n\t\tldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,\n\t\tcfg.UserFilter,\n\t\t[]string{cfg.UserAttr, cfg.FlagField},\n\t\tnil,\n\t)\n\n\tres, err := conn.Search(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := make(map[string]bool, len(res.Entries))\n\tfor _, e := range res.Entries {\n\t\tuser := e.GetAttributeValue(cfg.UserAttr)\n\t\tif user == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tval := e.GetAttributeValue(cfg.FlagField)\n\t\tenabled := false\n\t\tfor _, t := range cfg.TruthyVals {\n\t\t\tif val == t {\n\t\t\t\tenabled = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif cfg.Invert {\n\t\t\tenabled = !enabled\n\t\t}\n\t\tresult[user] = enabled\n\t}\n\treturn result, nil\n}\n\n// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.\nfunc AuthenticateUser(cfg Config, username, password string) (bool, error) {\n\taddr := fmt.Sprintf(\"%s:%d\", cfg.Host, cfg.Port)\n\n\tscheme := \"ldap\"\n\tif cfg.UseTLS {\n\t\tscheme = \"ldaps\"\n\t}\n\n\tldapURL := fmt.Sprintf(\"%s://%s\", scheme, addr)\n\n\tvar opts []ldap.DialOpt\n\tif cfg.UseTLS {\n\t\topts = append(opts, ldap.DialWithTLSConfig(&tls.Config{\n\t\t\tInsecureSkipVerify: false,\n\t\t}))\n\t}\n\n\tconn, err := ldap.DialURL(ldapURL, opts...)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer conn.Close()\n\n\t// Optional initial bind for search\n\tif cfg.BindDN != \"\" {\n\t\tif err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tif cfg.UserFilter == \"\" {\n\t\tcfg.UserFilter = \"(objectClass=person)\"\n\t}\n\tif cfg.UserAttr == \"\" {\n\t\tcfg.UserAttr = \"uid\"\n\t}\n\n\t// Build filter to find specific user\n\tfilter := fmt.Sprintf(\"(&%s(%s=%s))\", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))\n\treq := ldap.NewSearchRequest(\n\t\tcfg.BaseDN,\n\t\tldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,\n\t\tfilter,\n\t\t[]string{\"dn\"},\n\t\tnil,\n\t)\n\tres, err := conn.Search(req)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif len(res.Entries) == 0 {\n\t\treturn false, nil\n\t}\n\tuserDN := res.Entries[0].DN\n\t// Try to bind as the user\n\tif err := conn.Bind(userDN, password); err != nil {\n\t\treturn false, nil\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "util/random/random.go",
    "content": "// Package random provides utilities for generating random strings and numbers.\npackage random\n\nimport (\n\t\"crypto/rand\"\n\t\"math/big\"\n)\n\nvar (\n\tnumSeq      [10]rune\n\tlowerSeq    [26]rune\n\tupperSeq    [26]rune\n\tnumLowerSeq [36]rune\n\tnumUpperSeq [36]rune\n\tallSeq      [62]rune\n)\n\n// init initializes the character sequences used for random string generation.\n// It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations.\nfunc init() {\n\tfor i := range 10 {\n\t\tnumSeq[i] = rune('0' + i)\n\t}\n\tfor i := range 26 {\n\t\tlowerSeq[i] = rune('a' + i)\n\t\tupperSeq[i] = rune('A' + i)\n\t}\n\n\tcopy(numLowerSeq[:], numSeq[:])\n\tcopy(numLowerSeq[len(numSeq):], lowerSeq[:])\n\n\tcopy(numUpperSeq[:], numSeq[:])\n\tcopy(numUpperSeq[len(numSeq):], upperSeq[:])\n\n\tcopy(allSeq[:], numSeq[:])\n\tcopy(allSeq[len(numSeq):], lowerSeq[:])\n\tcopy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:])\n}\n\n// Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters).\nfunc Seq(n int) string {\n\trunes := make([]rune, n)\n\tfor i := range n {\n\t\tidx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq))))\n\t\tif err != nil {\n\t\t\tpanic(\"crypto/rand failed: \" + err.Error())\n\t\t}\n\t\trunes[i] = allSeq[idx.Int64()]\n\t}\n\treturn string(runes)\n}\n\n// Num generates a random integer between 0 and n-1.\nfunc Num(n int) int {\n\tbn := big.NewInt(int64(n))\n\tr, err := rand.Int(rand.Reader, bn)\n\tif err != nil {\n\t\tpanic(\"crypto/rand failed: \" + err.Error())\n\t}\n\treturn int(r.Int64())\n}\n"
  },
  {
    "path": "util/reflect_util/reflect.go",
    "content": "// Package reflect_util provides reflection utilities for working with struct fields and values.\npackage reflect_util\n\nimport \"reflect\"\n\n// GetFields returns all struct fields of the given reflect.Type.\nfunc GetFields(t reflect.Type) []reflect.StructField {\n\tnum := t.NumField()\n\tfields := make([]reflect.StructField, 0, num)\n\tfor i := range num {\n\t\tfields = append(fields, t.Field(i))\n\t}\n\treturn fields\n}\n\n// GetFieldValues returns all field values of the given reflect.Value.\nfunc GetFieldValues(v reflect.Value) []reflect.Value {\n\tnum := v.NumField()\n\tfields := make([]reflect.Value, 0, num)\n\tfor i := range num {\n\t\tfields = append(fields, v.Field(i))\n\t}\n\treturn fields\n}\n"
  },
  {
    "path": "util/sys/psutil.go",
    "content": "// Package sys provides system utilities for monitoring network connections and CPU usage.\n// Platform-specific implementations are provided for Windows, Linux, and macOS.\npackage sys\n\nimport (\n\t_ \"unsafe\"\n)\n\n//go:linkname HostProc github.com/shirou/gopsutil/v4/internal/common.HostProc\nfunc HostProc(combineWith ...string) string\n"
  },
  {
    "path": "util/sys/sys_darwin.go",
    "content": "//go:build darwin\n// +build darwin\n\npackage sys\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/shirou/gopsutil/v4/net\"\n\t\"golang.org/x/sys/unix\"\n)\n\nvar SIGUSR1 = syscall.SIGUSR1\n\nfunc GetTCPCount() (int, error) {\n\tstats, err := net.Connections(\"tcp\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(stats), nil\n}\n\nfunc GetUDPCount() (int, error) {\n\tstats, err := net.Connections(\"udp\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(stats), nil\n}\n\n// --- CPU Utilization (macOS native) ---\n\n// sysctl kern.cp_time returns an array of 5 longs: user, nice, sys, idle, intr.\n// We compute utilization deltas without cgo.\nvar (\n\tcpuMu       sync.Mutex\n\tlastTotals  [5]uint64\n\thasLastCPUT bool\n)\n\nfunc CPUPercentRaw() (float64, error) {\n\traw, err := unix.SysctlRaw(\"kern.cp_time\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\t// Expect either 5*8 bytes (uint64) or 5*4 bytes (uint32)\n\tvar out [5]uint64\n\tswitch len(raw) {\n\tcase 5 * 8:\n\t\tfor i := range 5 {\n\t\t\tout[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8])\n\t\t}\n\tcase 5 * 4:\n\t\tfor i := range 5 {\n\t\t\tout[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4]))\n\t\t}\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"unexpected kern.cp_time size: %d\", len(raw))\n\t}\n\n\t// user, nice, sys, idle, intr\n\tuser := out[0]\n\tnice := out[1]\n\tsysv := out[2]\n\tidle := out[3]\n\tintr := out[4]\n\n\tcpuMu.Lock()\n\tdefer cpuMu.Unlock()\n\n\tif !hasLastCPUT {\n\t\tlastTotals = out\n\t\thasLastCPUT = true\n\t\treturn 0, nil\n\t}\n\n\tdUser := user - lastTotals[0]\n\tdNice := nice - lastTotals[1]\n\tdSys := sysv - lastTotals[2]\n\tdIdle := idle - lastTotals[3]\n\tdIntr := intr - lastTotals[4]\n\n\tlastTotals = out\n\n\ttotald := dUser + dNice + dSys + dIdle + dIntr\n\tif totald == 0 {\n\t\treturn 0, nil\n\t}\n\tbusy := totald - dIdle\n\tpct := float64(busy) / float64(totald) * 100.0\n\tif pct > 100 {\n\t\tpct = 100\n\t}\n\treturn pct, nil\n}\n"
  },
  {
    "path": "util/sys/sys_linux.go",
    "content": "//go:build linux\n// +build linux\n\npackage sys\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n)\n\nvar SIGUSR1 = syscall.SIGUSR1\n\nfunc getLinesNum(filename string) (int, error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer file.Close()\n\n\tsum := 0\n\tbuf := make([]byte, 8192)\n\tfor {\n\t\tn, err := file.Read(buf)\n\n\t\tvar buffPosition int\n\t\tfor {\n\t\t\ti := bytes.IndexByte(buf[buffPosition:n], '\\n')\n\t\t\tif i < 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tbuffPosition += i + 1\n\t\t\tsum++\n\t\t}\n\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t} else if err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\treturn sum, nil\n}\n\n// GetTCPCount returns the number of active TCP connections by reading\n// /proc/net/tcp and /proc/net/tcp6 when available.\nfunc GetTCPCount() (int, error) {\n\troot := HostProc()\n\n\ttcp4, err := safeGetLinesNum(fmt.Sprintf(\"%v/net/tcp\", root))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\ttcp6, err := safeGetLinesNum(fmt.Sprintf(\"%v/net/tcp6\", root))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn tcp4 + tcp6, nil\n}\n\nfunc GetUDPCount() (int, error) {\n\troot := HostProc()\n\n\tudp4, err := safeGetLinesNum(fmt.Sprintf(\"%v/net/udp\", root))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tudp6, err := safeGetLinesNum(fmt.Sprintf(\"%v/net/udp6\", root))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn udp4 + udp6, nil\n}\n\n// safeGetLinesNum returns 0 if the file does not exist, otherwise forwards\n// to getLinesNum to count the number of lines.\nfunc safeGetLinesNum(path string) (int, error) {\n\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\treturn 0, nil\n\t} else if err != nil {\n\t\treturn 0, err\n\t}\n\treturn getLinesNum(path)\n}\n\n// --- CPU Utilization (Linux native) ---\n\nvar (\n\tcpuMu       sync.Mutex\n\tlastTotal   uint64\n\tlastIdleAll uint64\n\thasLast     bool\n)\n\n// CPUPercentRaw returns instantaneous total CPU utilization by reading /proc/stat.\n// First call initializes and returns 0; subsequent calls return busy/total * 100.\nfunc CPUPercentRaw() (float64, error) {\n\tf, err := os.Open(\"/proc/stat\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer f.Close()\n\n\trd := bufio.NewReader(f)\n\tline, err := rd.ReadString('\\n')\n\tif err != nil && err != io.EOF {\n\t\treturn 0, err\n\t}\n\t// Expect line like: cpu  user nice system idle iowait irq softirq steal guest guest_nice\n\tfields := strings.Fields(line)\n\tif len(fields) < 5 || fields[0] != \"cpu\" {\n\t\treturn 0, fmt.Errorf(\"unexpected /proc/stat format\")\n\t}\n\n\tvar nums []uint64\n\tfor i := 1; i < len(fields); i++ {\n\t\tv, err := strconv.ParseUint(fields[i], 10, 64)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tnums = append(nums, v)\n\t}\n\tif len(nums) < 4 { // need at least user,nice,system,idle\n\t\treturn 0, fmt.Errorf(\"insufficient cpu fields\")\n\t}\n\n\t// Conform with standard Linux CPU accounting\n\tvar user, nice, system, idle, iowait, irq, softirq, steal uint64\n\tuser = nums[0]\n\tif len(nums) > 1 {\n\t\tnice = nums[1]\n\t}\n\tif len(nums) > 2 {\n\t\tsystem = nums[2]\n\t}\n\tif len(nums) > 3 {\n\t\tidle = nums[3]\n\t}\n\tif len(nums) > 4 {\n\t\tiowait = nums[4]\n\t}\n\tif len(nums) > 5 {\n\t\tirq = nums[5]\n\t}\n\tif len(nums) > 6 {\n\t\tsoftirq = nums[6]\n\t}\n\tif len(nums) > 7 {\n\t\tsteal = nums[7]\n\t}\n\n\tidleAll := idle + iowait\n\tnonIdle := user + nice + system + irq + softirq + steal\n\ttotal := idleAll + nonIdle\n\n\tcpuMu.Lock()\n\tdefer cpuMu.Unlock()\n\n\tif !hasLast {\n\t\tlastTotal = total\n\t\tlastIdleAll = idleAll\n\t\thasLast = true\n\t\treturn 0, nil\n\t}\n\n\ttotald := total - lastTotal\n\tidled := idleAll - lastIdleAll\n\tlastTotal = total\n\tlastIdleAll = idleAll\n\n\tif totald == 0 {\n\t\treturn 0, nil\n\t}\n\tbusy := totald - idled\n\tpct := float64(busy) / float64(totald) * 100.0\n\tif pct > 100 {\n\t\tpct = 100\n\t}\n\treturn pct, nil\n}\n"
  },
  {
    "path": "util/sys/sys_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage sys\n\nimport (\n\t\"errors\"\n\t\"sync\"\n\t\"syscall\"\n\t\"unsafe\"\n\n\t\"github.com/shirou/gopsutil/v4/net\"\n)\n\nvar SIGUSR1 = syscall.Signal(0)\n\n// GetConnectionCount returns the number of active connections for the specified protocol (\"tcp\" or \"udp\").\nfunc GetConnectionCount(proto string) (int, error) {\n\tif proto != \"tcp\" && proto != \"udp\" {\n\t\treturn 0, errors.New(\"invalid protocol\")\n\t}\n\n\tstats, err := net.Connections(proto)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(stats), nil\n}\n\n// GetTCPCount returns the number of active TCP connections.\nfunc GetTCPCount() (int, error) {\n\treturn GetConnectionCount(\"tcp\")\n}\n\n// GetUDPCount returns the number of active UDP connections.\nfunc GetUDPCount() (int, error) {\n\treturn GetConnectionCount(\"udp\")\n}\n\n// --- CPU Utilization (Windows native) ---\n\nvar (\n\tmodKernel32        = syscall.NewLazyDLL(\"kernel32.dll\")\n\tprocGetSystemTimes = modKernel32.NewProc(\"GetSystemTimes\")\n\n\tcpuMu      sync.Mutex\n\tlastIdle   uint64\n\tlastKernel uint64\n\tlastUser   uint64\n\thasLast    bool\n)\n\ntype filetime struct {\n\tLowDateTime  uint32\n\tHighDateTime uint32\n}\n\n// ftToUint64 converts a Windows FILETIME-like struct to a uint64 for\n// arithmetic and delta calculations used by CPUPercentRaw.\nfunc ftToUint64(ft filetime) uint64 {\n\treturn (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)\n}\n\n// CPUPercentRaw returns the instantaneous total CPU utilization percentage using\n// Windows GetSystemTimes across all logical processors. The first call returns 0\n// as it initializes the baseline. Subsequent calls compute deltas.\nfunc CPUPercentRaw() (float64, error) {\n\tvar idleFT, kernelFT, userFT filetime\n\tr1, _, e1 := procGetSystemTimes.Call(\n\t\tuintptr(unsafe.Pointer(&idleFT)),\n\t\tuintptr(unsafe.Pointer(&kernelFT)),\n\t\tuintptr(unsafe.Pointer(&userFT)),\n\t)\n\tif r1 == 0 { // failure\n\t\tif e1 != nil {\n\t\t\treturn 0, e1\n\t\t}\n\t\treturn 0, syscall.GetLastError()\n\t}\n\n\tidle := ftToUint64(idleFT)\n\tkernel := ftToUint64(kernelFT)\n\tuser := ftToUint64(userFT)\n\n\tcpuMu.Lock()\n\tdefer cpuMu.Unlock()\n\n\tif !hasLast {\n\t\tlastIdle = idle\n\t\tlastKernel = kernel\n\t\tlastUser = user\n\t\thasLast = true\n\t\treturn 0, nil\n\t}\n\n\tidleDelta := idle - lastIdle\n\tkernelDelta := kernel - lastKernel\n\tuserDelta := user - lastUser\n\n\t// Update for next call\n\tlastIdle = idle\n\tlastKernel = kernel\n\tlastUser = user\n\n\ttotal := kernelDelta + userDelta\n\tif total == 0 {\n\t\treturn 0, nil\n\t}\n\t// On Windows, kernel time includes idle time; busy = total - idle\n\tbusy := total - idleDelta\n\n\tpct := float64(busy) / float64(total) * 100.0\n\t// lower bound not needed; ratios of uint64 are non-negative\n\tif pct > 100 {\n\t\tpct = 100\n\t}\n\treturn pct, nil\n}\n"
  },
  {
    "path": "web/assets/codemirror/fold/brace-fold.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: https://codemirror.net/5/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n\"use strict\";\n\nfunction bracketFolding(pairs) {\n  return function(cm, start) {\n    var line = start.line, lineText = cm.getLine(line);\n\n    function findOpening(pair) {\n      var tokenType;\n      for (var at = start.ch, pass = 0;;) {\n        var found = at <= 0 ? -1 : lineText.lastIndexOf(pair[0], at - 1);\n        if (found == -1) {\n          if (pass == 1) break;\n          pass = 1;\n          at = lineText.length;\n          continue;\n        }\n        if (pass == 1 && found < start.ch) break;\n        tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));\n        if (!/^(comment|string)/.test(tokenType)) return {ch: found + 1, tokenType: tokenType, pair: pair};\n        at = found - 1;\n      }\n    }\n\n    function findRange(found) {\n      var count = 1, lastLine = cm.lastLine(), end, startCh = found.ch, endCh\n      outer: for (var i = line; i <= lastLine; ++i) {\n        var text = cm.getLine(i), pos = i == line ? startCh : 0;\n        for (;;) {\n          var nextOpen = text.indexOf(found.pair[0], pos), nextClose = text.indexOf(found.pair[1], pos);\n          if (nextOpen < 0) nextOpen = text.length;\n          if (nextClose < 0) nextClose = text.length;\n          pos = Math.min(nextOpen, nextClose);\n          if (pos == text.length) break;\n          if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == found.tokenType) {\n            if (pos == nextOpen) ++count;\n            else if (!--count) { end = i; endCh = pos; break outer; }\n          }\n          ++pos;\n        }\n      }\n\n      if (end == null || line == end) return null\n      return {from: CodeMirror.Pos(line, startCh),\n              to: CodeMirror.Pos(end, endCh)};\n    }\n\n    var found = []\n    for (var i = 0; i < pairs.length; i++) {\n      var open = findOpening(pairs[i])\n      if (open) found.push(open)\n    }\n    found.sort(function(a, b) { return a.ch - b.ch })\n    for (var i = 0; i < found.length; i++) {\n      var range = findRange(found[i])\n      if (range) return range\n    }\n    return null\n  }\n}\n\nCodeMirror.registerHelper(\"fold\", \"brace\", bracketFolding([[\"{\", \"}\"], [\"[\", \"]\"]]));\n\nCodeMirror.registerHelper(\"fold\", \"brace-paren\", bracketFolding([[\"{\", \"}\"], [\"[\", \"]\"], [\"(\", \")\"]]));\n\nCodeMirror.registerHelper(\"fold\", \"import\", function(cm, start) {\n  function hasImport(line) {\n    if (line < cm.firstLine() || line > cm.lastLine()) return null;\n    var start = cm.getTokenAt(CodeMirror.Pos(line, 1));\n    if (!/\\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));\n    if (start.type != \"keyword\" || start.string != \"import\") return null;\n    // Now find closing semicolon, return its position\n    for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {\n      var text = cm.getLine(i), semi = text.indexOf(\";\");\n      if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};\n    }\n  }\n\n  var startLine = start.line, has = hasImport(startLine), prev;\n  if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))\n    return null;\n  for (var end = has.end;;) {\n    var next = hasImport(end.line + 1);\n    if (next == null) break;\n    end = next.end;\n  }\n  return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};\n});\n\nCodeMirror.registerHelper(\"fold\", \"include\", function(cm, start) {\n  function hasInclude(line) {\n    if (line < cm.firstLine() || line > cm.lastLine()) return null;\n    var start = cm.getTokenAt(CodeMirror.Pos(line, 1));\n    if (!/\\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));\n    if (start.type == \"meta\" && start.string.slice(0, 8) == \"#include\") return start.start + 8;\n  }\n\n  var startLine = start.line, has = hasInclude(startLine);\n  if (has == null || hasInclude(startLine - 1) != null) return null;\n  for (var end = startLine;;) {\n    var next = hasInclude(end + 1);\n    if (next == null) break;\n    ++end;\n  }\n  return {from: CodeMirror.Pos(startLine, has + 1),\n          to: cm.clipPos(CodeMirror.Pos(end))};\n});\n\n});\n"
  },
  {
    "path": "web/assets/codemirror/fold/foldcode.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: https://codemirror.net/5/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  function doFold(cm, pos, options, force) {\n    if (options && options.call) {\n      var finder = options;\n      options = null;\n    } else {\n      var finder = getOption(cm, options, \"rangeFinder\");\n    }\n    if (typeof pos == \"number\") pos = CodeMirror.Pos(pos, 0);\n    var minSize = getOption(cm, options, \"minFoldSize\");\n\n    function getRange(allowFolded) {\n      var range = finder(cm, pos);\n      if (!range || range.to.line - range.from.line < minSize) return null;\n      if (force === \"fold\") return range;\n\n      var marks = cm.findMarksAt(range.from);\n      for (var i = 0; i < marks.length; ++i) {\n        if (marks[i].__isFold) {\n          if (!allowFolded) return null;\n          range.cleared = true;\n          marks[i].clear();\n        }\n      }\n      return range;\n    }\n\n    var range = getRange(true);\n    if (getOption(cm, options, \"scanUp\")) while (!range && pos.line > cm.firstLine()) {\n      pos = CodeMirror.Pos(pos.line - 1, 0);\n      range = getRange(false);\n    }\n    if (!range || range.cleared || force === \"unfold\") return;\n\n    var myWidget = makeWidget(cm, options, range);\n    CodeMirror.on(myWidget, \"mousedown\", function(e) {\n      myRange.clear();\n      CodeMirror.e_preventDefault(e);\n    });\n    var myRange = cm.markText(range.from, range.to, {\n      replacedWith: myWidget,\n      clearOnEnter: getOption(cm, options, \"clearOnEnter\"),\n      __isFold: true\n    });\n    myRange.on(\"clear\", function(from, to) {\n      CodeMirror.signal(cm, \"unfold\", cm, from, to);\n    });\n    CodeMirror.signal(cm, \"fold\", cm, range.from, range.to);\n  }\n\n  function makeWidget(cm, options, range) {\n    var widget = getOption(cm, options, \"widget\");\n\n    if (typeof widget == \"function\") {\n      widget = widget(range.from, range.to);\n    }\n\n    if (typeof widget == \"string\") {\n      var text = document.createTextNode(widget);\n      widget = document.createElement(\"span\");\n      widget.appendChild(text);\n      widget.className = \"CodeMirror-foldmarker\";\n    } else if (widget) {\n      widget = widget.cloneNode(true)\n    }\n    return widget;\n  }\n\n  // Clumsy backwards-compatible interface\n  CodeMirror.newFoldFunction = function(rangeFinder, widget) {\n    return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };\n  };\n\n  // New-style interface\n  CodeMirror.defineExtension(\"foldCode\", function(pos, options, force) {\n    doFold(this, pos, options, force);\n  });\n\n  CodeMirror.defineExtension(\"isFolded\", function(pos) {\n    var marks = this.findMarksAt(pos);\n    for (var i = 0; i < marks.length; ++i)\n      if (marks[i].__isFold) return true;\n  });\n\n  CodeMirror.commands.toggleFold = function(cm) {\n    cm.foldCode(cm.getCursor());\n  };\n  CodeMirror.commands.fold = function(cm) {\n    cm.foldCode(cm.getCursor(), null, \"fold\");\n  };\n  CodeMirror.commands.unfold = function(cm) {\n    cm.foldCode(cm.getCursor(), { scanUp: false }, \"unfold\");\n  };\n  CodeMirror.commands.foldAll = function(cm) {\n    cm.operation(function() {\n      for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)\n        cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, \"fold\");\n    });\n  };\n  CodeMirror.commands.unfoldAll = function(cm) {\n    cm.operation(function() {\n      for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)\n        cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, \"unfold\");\n    });\n  };\n\n  CodeMirror.registerHelper(\"fold\", \"combine\", function() {\n    var funcs = Array.prototype.slice.call(arguments, 0);\n    return function(cm, start) {\n      for (var i = 0; i < funcs.length; ++i) {\n        var found = funcs[i](cm, start);\n        if (found) return found;\n      }\n    };\n  });\n\n  CodeMirror.registerHelper(\"fold\", \"auto\", function(cm, start) {\n    var helpers = cm.getHelpers(start, \"fold\");\n    for (var i = 0; i < helpers.length; i++) {\n      var cur = helpers[i](cm, start);\n      if (cur) return cur;\n    }\n  });\n\n  var defaultOptions = {\n    rangeFinder: CodeMirror.fold.auto,\n    widget: \"\\u2194\",\n    minFoldSize: 0,\n    scanUp: false,\n    clearOnEnter: true\n  };\n\n  CodeMirror.defineOption(\"foldOptions\", null);\n\n  function getOption(cm, options, name) {\n    if (options && options[name] !== undefined)\n      return options[name];\n    var editorOptions = cm.options.foldOptions;\n    if (editorOptions && editorOptions[name] !== undefined)\n      return editorOptions[name];\n    return defaultOptions[name];\n  }\n\n  CodeMirror.defineExtension(\"foldOption\", function(options, name) {\n    return getOption(this, options, name);\n  });\n});\n"
  },
  {
    "path": "web/assets/codemirror/fold/foldgutter.css",
    "content": ".CodeMirror-foldmarker {\n  color: blue;\n  text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;\n  font-family: arial;\n  line-height: .3;\n  cursor: pointer;\n}\n.CodeMirror-foldgutter {\n  width: .7em;\n}\n.CodeMirror-foldgutter-open,\n.CodeMirror-foldgutter-folded {\n  cursor: pointer;\n}\n.CodeMirror-foldgutter-open:after {\n  content: \"\\25BE\";\n}\n.CodeMirror-foldgutter-folded:after {\n  content: \"\\25B8\";\n}\n"
  },
  {
    "path": "web/assets/codemirror/fold/foldgutter.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: https://codemirror.net/5/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"), require(\"./foldcode\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\", \"./foldcode\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  CodeMirror.defineOption(\"foldGutter\", false, function(cm, val, old) {\n    if (old && old != CodeMirror.Init) {\n      cm.clearGutter(cm.state.foldGutter.options.gutter);\n      cm.state.foldGutter = null;\n      cm.off(\"gutterClick\", onGutterClick);\n      cm.off(\"changes\", onChange);\n      cm.off(\"viewportChange\", onViewportChange);\n      cm.off(\"fold\", onFold);\n      cm.off(\"unfold\", onFold);\n      cm.off(\"swapDoc\", onChange);\n      cm.off(\"optionChange\", optionChange);\n    }\n    if (val) {\n      cm.state.foldGutter = new State(parseOptions(val));\n      updateInViewport(cm);\n      cm.on(\"gutterClick\", onGutterClick);\n      cm.on(\"changes\", onChange);\n      cm.on(\"viewportChange\", onViewportChange);\n      cm.on(\"fold\", onFold);\n      cm.on(\"unfold\", onFold);\n      cm.on(\"swapDoc\", onChange);\n      cm.on(\"optionChange\", optionChange);\n    }\n  });\n\n  var Pos = CodeMirror.Pos;\n\n  function State(options) {\n    this.options = options;\n    this.from = this.to = 0;\n  }\n\n  function parseOptions(opts) {\n    if (opts === true) opts = {};\n    if (opts.gutter == null) opts.gutter = \"CodeMirror-foldgutter\";\n    if (opts.indicatorOpen == null) opts.indicatorOpen = \"CodeMirror-foldgutter-open\";\n    if (opts.indicatorFolded == null) opts.indicatorFolded = \"CodeMirror-foldgutter-folded\";\n    return opts;\n  }\n\n  function isFolded(cm, line) {\n    var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));\n    for (var i = 0; i < marks.length; ++i) {\n      if (marks[i].__isFold) {\n        var fromPos = marks[i].find(-1);\n        if (fromPos && fromPos.line === line)\n          return marks[i];\n      }\n    }\n  }\n\n  function marker(spec) {\n    if (typeof spec == \"string\") {\n      var elt = document.createElement(\"div\");\n      elt.className = spec + \" CodeMirror-guttermarker-subtle\";\n      return elt;\n    } else {\n      return spec.cloneNode(true);\n    }\n  }\n\n  function updateFoldInfo(cm, from, to) {\n    var opts = cm.state.foldGutter.options, cur = from - 1;\n    var minSize = cm.foldOption(opts, \"minFoldSize\");\n    var func = cm.foldOption(opts, \"rangeFinder\");\n    // we can reuse the built-in indicator element if its className matches the new state\n    var clsFolded = typeof opts.indicatorFolded == \"string\" && classTest(opts.indicatorFolded);\n    var clsOpen = typeof opts.indicatorOpen == \"string\" && classTest(opts.indicatorOpen);\n    cm.eachLine(from, to, function(line) {\n      ++cur;\n      var mark = null;\n      var old = line.gutterMarkers;\n      if (old) old = old[opts.gutter];\n      if (isFolded(cm, cur)) {\n        if (clsFolded && old && clsFolded.test(old.className)) return;\n        mark = marker(opts.indicatorFolded);\n      } else {\n        var pos = Pos(cur, 0);\n        var range = func && func(cm, pos);\n        if (range && range.to.line - range.from.line >= minSize) {\n          if (clsOpen && old && clsOpen.test(old.className)) return;\n          mark = marker(opts.indicatorOpen);\n        }\n      }\n      if (!mark && !old) return;\n      cm.setGutterMarker(line, opts.gutter, mark);\n    });\n  }\n\n  // copied from CodeMirror/src/util/dom.js\n  function classTest(cls) { return new RegExp(\"(^|\\\\s)\" + cls + \"(?:$|\\\\s)\\\\s*\") }\n\n  function updateInViewport(cm) {\n    var vp = cm.getViewport(), state = cm.state.foldGutter;\n    if (!state) return;\n    cm.operation(function() {\n      updateFoldInfo(cm, vp.from, vp.to);\n    });\n    state.from = vp.from; state.to = vp.to;\n  }\n\n  function onGutterClick(cm, line, gutter) {\n    var state = cm.state.foldGutter;\n    if (!state) return;\n    var opts = state.options;\n    if (gutter != opts.gutter) return;\n    var folded = isFolded(cm, line);\n    if (folded) folded.clear();\n    else cm.foldCode(Pos(line, 0), opts);\n  }\n\n  function optionChange(cm, option) {\n    if (option == \"mode\") onChange(cm)\n  }\n\n  function onChange(cm) {\n    var state = cm.state.foldGutter;\n    if (!state) return;\n    var opts = state.options;\n    state.from = state.to = 0;\n    clearTimeout(state.changeUpdate);\n    state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);\n  }\n\n  function onViewportChange(cm) {\n    var state = cm.state.foldGutter;\n    if (!state) return;\n    var opts = state.options;\n    clearTimeout(state.changeUpdate);\n    state.changeUpdate = setTimeout(function() {\n      var vp = cm.getViewport();\n      if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {\n        updateInViewport(cm);\n      } else {\n        cm.operation(function() {\n          if (vp.from < state.from) {\n            updateFoldInfo(cm, vp.from, state.from);\n            state.from = vp.from;\n          }\n          if (vp.to > state.to) {\n            updateFoldInfo(cm, state.to, vp.to);\n            state.to = vp.to;\n          }\n        });\n      }\n    }, opts.updateViewportTimeSpan || 400);\n  }\n\n  function onFold(cm, from) {\n    var state = cm.state.foldGutter;\n    if (!state) return;\n    var line = from.line;\n    if (line >= state.from && line < state.to)\n      updateFoldInfo(cm, line, line + 1);\n  }\n});\n"
  },
  {
    "path": "web/assets/codemirror/hint/javascript-hint.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: https://codemirror.net/5/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  var Pos = CodeMirror.Pos;\n\n  function forEach(arr, f) {\n    for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);\n  }\n\n  function arrayContains(arr, item) {\n    if (!Array.prototype.indexOf) {\n      var i = arr.length;\n      while (i--) {\n        if (arr[i] === item) {\n          return true;\n        }\n      }\n      return false;\n    }\n    return arr.indexOf(item) != -1;\n  }\n\n  function scriptHint(editor, keywords, getToken, options) {\n    // Find the token at the cursor\n    var cur = editor.getCursor(), token = getToken(editor, cur);\n    if (/\\b(?:string|comment)\\b/.test(token.type)) return;\n    var innerMode = CodeMirror.innerMode(editor.getMode(), token.state);\n    if (innerMode.mode.helperType === \"json\") return;\n    token.state = innerMode.state;\n\n    // If it's not a 'word-style' token, ignore the token.\n    if (!/^[\\w$_]*$/.test(token.string)) {\n      token = {start: cur.ch, end: cur.ch, string: \"\", state: token.state,\n               type: token.string == \".\" ? \"property\" : null};\n    } else if (token.end > cur.ch) {\n      token.end = cur.ch;\n      token.string = token.string.slice(0, cur.ch - token.start);\n    }\n\n    var tprop = token;\n    // If it is a property, find out what it is a property of.\n    while (tprop.type == \"property\") {\n      tprop = getToken(editor, Pos(cur.line, tprop.start));\n      if (tprop.string != \".\") return;\n      tprop = getToken(editor, Pos(cur.line, tprop.start));\n      if (!context) var context = [];\n      context.push(tprop);\n    }\n    return {list: getCompletions(token, context, keywords, options),\n            from: Pos(cur.line, token.start),\n            to: Pos(cur.line, token.end)};\n  }\n\n  function javascriptHint(editor, options) {\n    return scriptHint(editor, javascriptKeywords,\n                      function (e, cur) {return e.getTokenAt(cur);},\n                      options);\n  }\n  CodeMirror.registerHelper(\"hint\", \"javascript\", javascriptHint);\n\n  function getCoffeeScriptToken(editor, cur) {\n  // This getToken, it is for coffeescript, imitates the behavior of\n  // getTokenAt method in javascript.js, that is, returning \"property\"\n  // type and treat \".\" as independent token.\n    var token = editor.getTokenAt(cur);\n    if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') {\n      token.end = token.start;\n      token.string = '.';\n      token.type = \"property\";\n    }\n    else if (/^\\.[\\w$_]*$/.test(token.string)) {\n      token.type = \"property\";\n      token.start++;\n      token.string = token.string.replace(/\\./, '');\n    }\n    return token;\n  }\n\n  function coffeescriptHint(editor, options) {\n    return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options);\n  }\n  CodeMirror.registerHelper(\"hint\", \"coffeescript\", coffeescriptHint);\n\n  var stringProps = (\"charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight \" +\n                     \"toUpperCase toLowerCase split concat match replace search\").split(\" \");\n  var arrayProps = (\"length concat join splice push pop shift unshift slice reverse sort indexOf \" +\n                    \"lastIndexOf every some filter forEach map reduce reduceRight \").split(\" \");\n  var funcProps = \"prototype apply call bind\".split(\" \");\n  var javascriptKeywords = (\"break case catch class const continue debugger default delete do else export extends false finally for function \" +\n                  \"if in import instanceof new null return super switch this throw true try typeof var void while with yield\").split(\" \");\n  var coffeescriptKeywords = (\"and break catch class continue delete do else extends false finally for \" +\n                  \"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(\" \");\n\n  function forAllProps(obj, callback) {\n    if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) {\n      for (var name in obj) callback(name)\n    } else {\n      for (var o = obj; o; o = Object.getPrototypeOf(o))\n        Object.getOwnPropertyNames(o).forEach(callback)\n    }\n  }\n\n  function getCompletions(token, context, keywords, options) {\n    var found = [], start = token.string, global = options && options.globalScope || window;\n    function maybeAdd(str) {\n      if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str);\n    }\n    function gatherCompletions(obj) {\n      if (typeof obj == \"string\") forEach(stringProps, maybeAdd);\n      else if (obj instanceof Array) forEach(arrayProps, maybeAdd);\n      else if (obj instanceof Function) forEach(funcProps, maybeAdd);\n      forAllProps(obj, maybeAdd)\n    }\n\n    if (context && context.length) {\n      // If this is a property, see if it belongs to some object we can\n      // find in the current environment.\n      var obj = context.pop(), base;\n      if (obj.type && obj.type.indexOf(\"variable\") === 0) {\n        if (options && options.additionalContext)\n          base = options.additionalContext[obj.string];\n        if (!options || options.useGlobalScope !== false)\n          base = base || global[obj.string];\n      } else if (obj.type == \"string\") {\n        base = \"\";\n      } else if (obj.type == \"atom\") {\n        base = 1;\n      } else if (obj.type == \"function\") {\n        if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&\n            (typeof global.jQuery == 'function'))\n          base = global.jQuery();\n        else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function'))\n          base = global._();\n      }\n      while (base != null && context.length)\n        base = base[context.pop().string];\n      if (base != null) gatherCompletions(base);\n    } else {\n      // If not, just look in the global object, any local scope, and optional additional-context\n      // (reading into JS mode internals to get at the local and global variables)\n      for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);\n      for (var c = token.state.context; c; c = c.prev)\n        for (var v = c.vars; v; v = v.next) maybeAdd(v.name)\n      for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);\n      if (options && options.additionalContext != null)\n        for (var key in options.additionalContext)\n          maybeAdd(key);\n      if (!options || options.useGlobalScope !== false)\n        gatherCompletions(global);\n      forEach(keywords, maybeAdd);\n    }\n    return found;\n  }\n});\n"
  },
  {
    "path": "web/assets/codemirror/javascript.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: https://codemirror.net/5/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n\"use strict\";\n\nCodeMirror.defineMode(\"javascript\", function(config, parserConfig) {\n  var indentUnit = config.indentUnit;\n  var statementIndent = parserConfig.statementIndent;\n  var jsonldMode = parserConfig.jsonld;\n  var jsonMode = parserConfig.json || jsonldMode;\n  var trackScope = parserConfig.trackScope !== false\n  var isTS = parserConfig.typescript;\n  var wordRE = parserConfig.wordCharacters || /[\\w$\\xa1-\\uffff]/;\n\n  // Tokenizer\n\n  var keywords = function(){\n    function kw(type) {return {type: type, style: \"keyword\"};}\n    var A = kw(\"keyword a\"), B = kw(\"keyword b\"), C = kw(\"keyword c\"), D = kw(\"keyword d\");\n    var operator = kw(\"operator\"), atom = {type: \"atom\", style: \"atom\"};\n\n    return {\n      \"if\": kw(\"if\"), \"while\": A, \"with\": A, \"else\": B, \"do\": B, \"try\": B, \"finally\": B,\n      \"return\": D, \"break\": D, \"continue\": D, \"new\": kw(\"new\"), \"delete\": C, \"void\": C, \"throw\": C,\n      \"debugger\": kw(\"debugger\"), \"var\": kw(\"var\"), \"const\": kw(\"var\"), \"let\": kw(\"var\"),\n      \"function\": kw(\"function\"), \"catch\": kw(\"catch\"),\n      \"for\": kw(\"for\"), \"switch\": kw(\"switch\"), \"case\": kw(\"case\"), \"default\": kw(\"default\"),\n      \"in\": operator, \"typeof\": operator, \"instanceof\": operator,\n      \"true\": atom, \"false\": atom, \"null\": atom, \"undefined\": atom, \"NaN\": atom, \"Infinity\": atom,\n      \"this\": kw(\"this\"), \"class\": kw(\"class\"), \"super\": kw(\"atom\"),\n      \"yield\": C, \"export\": kw(\"export\"), \"import\": kw(\"import\"), \"extends\": C,\n      \"await\": C\n    };\n  }();\n\n  var isOperatorChar = /[+\\-*&%=<>!?|~^@]/;\n  var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)\"/;\n\n  function readRegexp(stream) {\n    var escaped = false, next, inSet = false;\n    while ((next = stream.next()) != null) {\n      if (!escaped) {\n        if (next == \"/\" && !inSet) return;\n        if (next == \"[\") inSet = true;\n        else if (inSet && next == \"]\") inSet = false;\n      }\n      escaped = !escaped && next == \"\\\\\";\n    }\n  }\n\n  // Used as scratch variables to communicate multiple values without\n  // consing up tons of objects.\n  var type, content;\n  function ret(tp, style, cont) {\n    type = tp; content = cont;\n    return style;\n  }\n  function tokenBase(stream, state) {\n    var ch = stream.next();\n    if (ch == '\"' || ch == \"'\") {\n      state.tokenize = tokenString(ch);\n      return state.tokenize(stream, state);\n    } else if (ch == \".\" && stream.match(/^\\d[\\d_]*(?:[eE][+\\-]?[\\d_]+)?/)) {\n      return ret(\"number\", \"number\");\n    } else if (ch == \".\" && stream.match(\"..\")) {\n      return ret(\"spread\", \"meta\");\n    } else if (/[\\[\\]{}\\(\\),;\\:\\.]/.test(ch)) {\n      return ret(ch);\n    } else if (ch == \"=\" && stream.eat(\">\")) {\n      return ret(\"=>\", \"operator\");\n    } else if (ch == \"0\" && stream.match(/^(?:x[\\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) {\n      return ret(\"number\", \"number\");\n    } else if (/\\d/.test(ch)) {\n      stream.match(/^[\\d_]*(?:n|(?:\\.[\\d_]*)?(?:[eE][+\\-]?[\\d_]+)?)?/);\n      return ret(\"number\", \"number\");\n    } else if (ch == \"/\") {\n      if (stream.eat(\"*\")) {\n        state.tokenize = tokenComment;\n        return tokenComment(stream, state);\n      } else if (stream.eat(\"/\")) {\n        stream.skipToEnd();\n        return ret(\"comment\", \"comment\");\n      } else if (expressionAllowed(stream, state, 1)) {\n        readRegexp(stream);\n        stream.match(/^\\b(([gimyus])(?![gimyus]*\\2))+\\b/);\n        return ret(\"regexp\", \"string-2\");\n      } else {\n        stream.eat(\"=\");\n        return ret(\"operator\", \"operator\", stream.current());\n      }\n    } else if (ch == \"`\") {\n      state.tokenize = tokenQuasi;\n      return tokenQuasi(stream, state);\n    } else if (ch == \"#\" && stream.peek() == \"!\") {\n      stream.skipToEnd();\n      return ret(\"meta\", \"meta\");\n    } else if (ch == \"#\" && stream.eatWhile(wordRE)) {\n      return ret(\"variable\", \"property\")\n    } else if (ch == \"<\" && stream.match(\"!--\") ||\n               (ch == \"-\" && stream.match(\"->\") && !/\\S/.test(stream.string.slice(0, stream.start)))) {\n      stream.skipToEnd()\n      return ret(\"comment\", \"comment\")\n    } else if (isOperatorChar.test(ch)) {\n      if (ch != \">\" || !state.lexical || state.lexical.type != \">\") {\n        if (stream.eat(\"=\")) {\n          if (ch == \"!\" || ch == \"=\") stream.eat(\"=\")\n        } else if (/[<>*+\\-|&?]/.test(ch)) {\n          stream.eat(ch)\n          if (ch == \">\") stream.eat(ch)\n        }\n      }\n      if (ch == \"?\" && stream.eat(\".\")) return ret(\".\")\n      return ret(\"operator\", \"operator\", stream.current());\n    } else if (wordRE.test(ch)) {\n      stream.eatWhile(wordRE);\n      var word = stream.current()\n      if (state.lastType != \".\") {\n        if (keywords.propertyIsEnumerable(word)) {\n          var kw = keywords[word]\n          return ret(kw.type, kw.style, word)\n        }\n        if (word == \"async\" && stream.match(/^(\\s|\\/\\*([^*]|\\*(?!\\/))*?\\*\\/)*[\\[\\(\\w]/, false))\n          return ret(\"async\", \"keyword\", word)\n      }\n      return ret(\"variable\", \"variable\", word)\n    }\n  }\n\n  function tokenString(quote) {\n    return function(stream, state) {\n      var escaped = false, next;\n      if (jsonldMode && stream.peek() == \"@\" && stream.match(isJsonldKeyword)){\n        state.tokenize = tokenBase;\n        return ret(\"jsonld-keyword\", \"meta\");\n      }\n      while ((next = stream.next()) != null) {\n        if (next == quote && !escaped) break;\n        escaped = !escaped && next == \"\\\\\";\n      }\n      if (!escaped) state.tokenize = tokenBase;\n      return ret(\"string\", \"string\");\n    };\n  }\n\n  function tokenComment(stream, state) {\n    var maybeEnd = false, ch;\n    while (ch = stream.next()) {\n      if (ch == \"/\" && maybeEnd) {\n        state.tokenize = tokenBase;\n        break;\n      }\n      maybeEnd = (ch == \"*\");\n    }\n    return ret(\"comment\", \"comment\");\n  }\n\n  function tokenQuasi(stream, state) {\n    var escaped = false, next;\n    while ((next = stream.next()) != null) {\n      if (!escaped && (next == \"`\" || next == \"$\" && stream.eat(\"{\"))) {\n        state.tokenize = tokenBase;\n        break;\n      }\n      escaped = !escaped && next == \"\\\\\";\n    }\n    return ret(\"quasi\", \"string-2\", stream.current());\n  }\n\n  var brackets = \"([{}])\";\n  // This is a crude lookahead trick to try and notice that we're\n  // parsing the argument patterns for a fat-arrow function before we\n  // actually hit the arrow token. It only works if the arrow is on\n  // the same line as the arguments and there's no strange noise\n  // (comments) in between. Fallback is to only notice when we hit the\n  // arrow, and not declare the arguments as locals for the arrow\n  // body.\n  function findFatArrow(stream, state) {\n    if (state.fatArrowAt) state.fatArrowAt = null;\n    var arrow = stream.string.indexOf(\"=>\", stream.start);\n    if (arrow < 0) return;\n\n    if (isTS) { // Try to skip TypeScript return type declarations after the arguments\n      var m = /:\\s*(?:\\w+(?:<[^>]*>|\\[\\])?|\\{[^}]*\\})\\s*$/.exec(stream.string.slice(stream.start, arrow))\n      if (m) arrow = m.index\n    }\n\n    var depth = 0, sawSomething = false;\n    for (var pos = arrow - 1; pos >= 0; --pos) {\n      var ch = stream.string.charAt(pos);\n      var bracket = brackets.indexOf(ch);\n      if (bracket >= 0 && bracket < 3) {\n        if (!depth) { ++pos; break; }\n        if (--depth == 0) { if (ch == \"(\") sawSomething = true; break; }\n      } else if (bracket >= 3 && bracket < 6) {\n        ++depth;\n      } else if (wordRE.test(ch)) {\n        sawSomething = true;\n      } else if (/[\"'\\/`]/.test(ch)) {\n        for (;; --pos) {\n          if (pos == 0) return\n          var next = stream.string.charAt(pos - 1)\n          if (next == ch && stream.string.charAt(pos - 2) != \"\\\\\") { pos--; break }\n        }\n      } else if (sawSomething && !depth) {\n        ++pos;\n        break;\n      }\n    }\n    if (sawSomething && !depth) state.fatArrowAt = pos;\n  }\n\n  // Parser\n\n  var atomicTypes = {\"atom\": true, \"number\": true, \"variable\": true, \"string\": true,\n                     \"regexp\": true, \"this\": true, \"import\": true, \"jsonld-keyword\": true};\n\n  function JSLexical(indented, column, type, align, prev, info) {\n    this.indented = indented;\n    this.column = column;\n    this.type = type;\n    this.prev = prev;\n    this.info = info;\n    if (align != null) this.align = align;\n  }\n\n  function inScope(state, varname) {\n    if (!trackScope) return false\n    for (var v = state.localVars; v; v = v.next)\n      if (v.name == varname) return true;\n    for (var cx = state.context; cx; cx = cx.prev) {\n      for (var v = cx.vars; v; v = v.next)\n        if (v.name == varname) return true;\n    }\n  }\n\n  function parseJS(state, style, type, content, stream) {\n    var cc = state.cc;\n    // Communicate our context to the combinators.\n    // (Less wasteful than consing up a hundred closures on every call.)\n    cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;\n\n    if (!state.lexical.hasOwnProperty(\"align\"))\n      state.lexical.align = true;\n\n    while(true) {\n      var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;\n      if (combinator(type, content)) {\n        while(cc.length && cc[cc.length - 1].lex)\n          cc.pop()();\n        if (cx.marked) return cx.marked;\n        if (type == \"variable\" && inScope(state, content)) return \"variable-2\";\n        return style;\n      }\n    }\n  }\n\n  // Combinator utils\n\n  var cx = {state: null, column: null, marked: null, cc: null};\n  function pass() {\n    for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);\n  }\n  function cont() {\n    pass.apply(null, arguments);\n    return true;\n  }\n  function inList(name, list) {\n    for (var v = list; v; v = v.next) if (v.name == name) return true\n    return false;\n  }\n  function register(varname) {\n    var state = cx.state;\n    cx.marked = \"def\";\n    if (!trackScope) return\n    if (state.context) {\n      if (state.lexical.info == \"var\" && state.context && state.context.block) {\n        // FIXME function decls are also not block scoped\n        var newContext = registerVarScoped(varname, state.context)\n        if (newContext != null) {\n          state.context = newContext\n          return\n        }\n      } else if (!inList(varname, state.localVars)) {\n        state.localVars = new Var(varname, state.localVars)\n        return\n      }\n    }\n    // Fall through means this is global\n    if (parserConfig.globalVars && !inList(varname, state.globalVars))\n      state.globalVars = new Var(varname, state.globalVars)\n  }\n  function registerVarScoped(varname, context) {\n    if (!context) {\n      return null\n    } else if (context.block) {\n      var inner = registerVarScoped(varname, context.prev)\n      if (!inner) return null\n      if (inner == context.prev) return context\n      return new Context(inner, context.vars, true)\n    } else if (inList(varname, context.vars)) {\n      return context\n    } else {\n      return new Context(context.prev, new Var(varname, context.vars), false)\n    }\n  }\n\n  function isModifier(name) {\n    return name == \"public\" || name == \"private\" || name == \"protected\" || name == \"abstract\" || name == \"readonly\"\n  }\n\n  // Combinators\n\n  function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }\n  function Var(name, next) { this.name = name; this.next = next }\n\n  var defaultVars = new Var(\"this\", new Var(\"arguments\", null))\n  function pushcontext() {\n    cx.state.context = new Context(cx.state.context, cx.state.localVars, false)\n    cx.state.localVars = defaultVars\n  }\n  function pushblockcontext() {\n    cx.state.context = new Context(cx.state.context, cx.state.localVars, true)\n    cx.state.localVars = null\n  }\n  pushcontext.lex = pushblockcontext.lex = true\n  function popcontext() {\n    cx.state.localVars = cx.state.context.vars\n    cx.state.context = cx.state.context.prev\n  }\n  popcontext.lex = true\n  function pushlex(type, info) {\n    var result = function() {\n      var state = cx.state, indent = state.indented;\n      if (state.lexical.type == \"stat\") indent = state.lexical.indented;\n      else for (var outer = state.lexical; outer && outer.type == \")\" && outer.align; outer = outer.prev)\n        indent = outer.indented;\n      state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);\n    };\n    result.lex = true;\n    return result;\n  }\n  function poplex() {\n    var state = cx.state;\n    if (state.lexical.prev) {\n      if (state.lexical.type == \")\")\n        state.indented = state.lexical.indented;\n      state.lexical = state.lexical.prev;\n    }\n  }\n  poplex.lex = true;\n\n  function expect(wanted) {\n    function exp(type) {\n      if (type == wanted) return cont();\n      else if (wanted == \";\" || type == \"}\" || type == \")\" || type == \"]\") return pass();\n      else return cont(exp);\n    }\n    return exp;\n  }\n\n  function statement(type, value) {\n    if (type == \"var\") return cont(pushlex(\"vardef\", value), vardef, expect(\";\"), poplex);\n    if (type == \"keyword a\") return cont(pushlex(\"form\"), parenExpr, statement, poplex);\n    if (type == \"keyword b\") return cont(pushlex(\"form\"), statement, poplex);\n    if (type == \"keyword d\") return cx.stream.match(/^\\s*$/, false) ? cont() : cont(pushlex(\"stat\"), maybeexpression, expect(\";\"), poplex);\n    if (type == \"debugger\") return cont(expect(\";\"));\n    if (type == \"{\") return cont(pushlex(\"}\"), pushblockcontext, block, poplex, popcontext);\n    if (type == \";\") return cont();\n    if (type == \"if\") {\n      if (cx.state.lexical.info == \"else\" && cx.state.cc[cx.state.cc.length - 1] == poplex)\n        cx.state.cc.pop()();\n      return cont(pushlex(\"form\"), parenExpr, statement, poplex, maybeelse);\n    }\n    if (type == \"function\") return cont(functiondef);\n    if (type == \"for\") return cont(pushlex(\"form\"), pushblockcontext, forspec, statement, popcontext, poplex);\n    if (type == \"class\" || (isTS && value == \"interface\")) {\n      cx.marked = \"keyword\"\n      return cont(pushlex(\"form\", type == \"class\" ? type : value), className, poplex)\n    }\n    if (type == \"variable\") {\n      if (isTS && value == \"declare\") {\n        cx.marked = \"keyword\"\n        return cont(statement)\n      } else if (isTS && (value == \"module\" || value == \"enum\" || value == \"type\") && cx.stream.match(/^\\s*\\w/, false)) {\n        cx.marked = \"keyword\"\n        if (value == \"enum\") return cont(enumdef);\n        else if (value == \"type\") return cont(typename, expect(\"operator\"), typeexpr, expect(\";\"));\n        else return cont(pushlex(\"form\"), pattern, expect(\"{\"), pushlex(\"}\"), block, poplex, poplex)\n      } else if (isTS && value == \"namespace\") {\n        cx.marked = \"keyword\"\n        return cont(pushlex(\"form\"), expression, statement, poplex)\n      } else if (isTS && value == \"abstract\") {\n        cx.marked = \"keyword\"\n        return cont(statement)\n      } else {\n        return cont(pushlex(\"stat\"), maybelabel);\n      }\n    }\n    if (type == \"switch\") return cont(pushlex(\"form\"), parenExpr, expect(\"{\"), pushlex(\"}\", \"switch\"), pushblockcontext,\n                                      block, poplex, poplex, popcontext);\n    if (type == \"case\") return cont(expression, expect(\":\"));\n    if (type == \"default\") return cont(expect(\":\"));\n    if (type == \"catch\") return cont(pushlex(\"form\"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);\n    if (type == \"export\") return cont(pushlex(\"stat\"), afterExport, poplex);\n    if (type == \"import\") return cont(pushlex(\"stat\"), afterImport, poplex);\n    if (type == \"async\") return cont(statement)\n    if (value == \"@\") return cont(expression, statement)\n    return pass(pushlex(\"stat\"), expression, expect(\";\"), poplex);\n  }\n  function maybeCatchBinding(type) {\n    if (type == \"(\") return cont(funarg, expect(\")\"))\n  }\n  function expression(type, value) {\n    return expressionInner(type, value, false);\n  }\n  function expressionNoComma(type, value) {\n    return expressionInner(type, value, true);\n  }\n  function parenExpr(type) {\n    if (type != \"(\") return pass()\n    return cont(pushlex(\")\"), maybeexpression, expect(\")\"), poplex)\n  }\n  function expressionInner(type, value, noComma) {\n    if (cx.state.fatArrowAt == cx.stream.start) {\n      var body = noComma ? arrowBodyNoComma : arrowBody;\n      if (type == \"(\") return cont(pushcontext, pushlex(\")\"), commasep(funarg, \")\"), poplex, expect(\"=>\"), body, popcontext);\n      else if (type == \"variable\") return pass(pushcontext, pattern, expect(\"=>\"), body, popcontext);\n    }\n\n    var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;\n    if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);\n    if (type == \"function\") return cont(functiondef, maybeop);\n    if (type == \"class\" || (isTS && value == \"interface\")) { cx.marked = \"keyword\"; return cont(pushlex(\"form\"), classExpression, poplex); }\n    if (type == \"keyword c\" || type == \"async\") return cont(noComma ? expressionNoComma : expression);\n    if (type == \"(\") return cont(pushlex(\")\"), maybeexpression, expect(\")\"), poplex, maybeop);\n    if (type == \"operator\" || type == \"spread\") return cont(noComma ? expressionNoComma : expression);\n    if (type == \"[\") return cont(pushlex(\"]\"), arrayLiteral, poplex, maybeop);\n    if (type == \"{\") return contCommasep(objprop, \"}\", null, maybeop);\n    if (type == \"quasi\") return pass(quasi, maybeop);\n    if (type == \"new\") return cont(maybeTarget(noComma));\n    return cont();\n  }\n  function maybeexpression(type) {\n    if (type.match(/[;\\}\\)\\],]/)) return pass();\n    return pass(expression);\n  }\n\n  function maybeoperatorComma(type, value) {\n    if (type == \",\") return cont(maybeexpression);\n    return maybeoperatorNoComma(type, value, false);\n  }\n  function maybeoperatorNoComma(type, value, noComma) {\n    var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;\n    var expr = noComma == false ? expression : expressionNoComma;\n    if (type == \"=>\") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);\n    if (type == \"operator\") {\n      if (/\\+\\+|--/.test(value) || isTS && value == \"!\") return cont(me);\n      if (isTS && value == \"<\" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\\s*\\(/, false))\n        return cont(pushlex(\">\"), commasep(typeexpr, \">\"), poplex, me);\n      if (value == \"?\") return cont(expression, expect(\":\"), expr);\n      return cont(expr);\n    }\n    if (type == \"quasi\") { return pass(quasi, me); }\n    if (type == \";\") return;\n    if (type == \"(\") return contCommasep(expressionNoComma, \")\", \"call\", me);\n    if (type == \".\") return cont(property, me);\n    if (type == \"[\") return cont(pushlex(\"]\"), maybeexpression, expect(\"]\"), poplex, me);\n    if (isTS && value == \"as\") { cx.marked = \"keyword\"; return cont(typeexpr, me) }\n    if (type == \"regexp\") {\n      cx.state.lastType = cx.marked = \"operator\"\n      cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)\n      return cont(expr)\n    }\n  }\n  function quasi(type, value) {\n    if (type != \"quasi\") return pass();\n    if (value.slice(value.length - 2) != \"${\") return cont(quasi);\n    return cont(maybeexpression, continueQuasi);\n  }\n  function continueQuasi(type) {\n    if (type == \"}\") {\n      cx.marked = \"string-2\";\n      cx.state.tokenize = tokenQuasi;\n      return cont(quasi);\n    }\n  }\n  function arrowBody(type) {\n    findFatArrow(cx.stream, cx.state);\n    return pass(type == \"{\" ? statement : expression);\n  }\n  function arrowBodyNoComma(type) {\n    findFatArrow(cx.stream, cx.state);\n    return pass(type == \"{\" ? statement : expressionNoComma);\n  }\n  function maybeTarget(noComma) {\n    return function(type) {\n      if (type == \".\") return cont(noComma ? targetNoComma : target);\n      else if (type == \"variable\" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma)\n      else return pass(noComma ? expressionNoComma : expression);\n    };\n  }\n  function target(_, value) {\n    if (value == \"target\") { cx.marked = \"keyword\"; return cont(maybeoperatorComma); }\n  }\n  function targetNoComma(_, value) {\n    if (value == \"target\") { cx.marked = \"keyword\"; return cont(maybeoperatorNoComma); }\n  }\n  function maybelabel(type) {\n    if (type == \":\") return cont(poplex, statement);\n    return pass(maybeoperatorComma, expect(\";\"), poplex);\n  }\n  function property(type) {\n    if (type == \"variable\") {cx.marked = \"property\"; return cont();}\n  }\n  function objprop(type, value) {\n    if (type == \"async\") {\n      cx.marked = \"property\";\n      return cont(objprop);\n    } else if (type == \"variable\" || cx.style == \"keyword\") {\n      cx.marked = \"property\";\n      if (value == \"get\" || value == \"set\") return cont(getterSetter);\n      var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params\n      if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\\s*:\\s*/, false)))\n        cx.state.fatArrowAt = cx.stream.pos + m[0].length\n      return cont(afterprop);\n    } else if (type == \"number\" || type == \"string\") {\n      cx.marked = jsonldMode ? \"property\" : (cx.style + \" property\");\n      return cont(afterprop);\n    } else if (type == \"jsonld-keyword\") {\n      return cont(afterprop);\n    } else if (isTS && isModifier(value)) {\n      cx.marked = \"keyword\"\n      return cont(objprop)\n    } else if (type == \"[\") {\n      return cont(expression, maybetype, expect(\"]\"), afterprop);\n    } else if (type == \"spread\") {\n      return cont(expressionNoComma, afterprop);\n    } else if (value == \"*\") {\n      cx.marked = \"keyword\";\n      return cont(objprop);\n    } else if (type == \":\") {\n      return pass(afterprop)\n    }\n  }\n  function getterSetter(type) {\n    if (type != \"variable\") return pass(afterprop);\n    cx.marked = \"property\";\n    return cont(functiondef);\n  }\n  function afterprop(type) {\n    if (type == \":\") return cont(expressionNoComma);\n    if (type == \"(\") return pass(functiondef);\n  }\n  function commasep(what, end, sep) {\n    function proceed(type, value) {\n      if (sep ? sep.indexOf(type) > -1 : type == \",\") {\n        var lex = cx.state.lexical;\n        if (lex.info == \"call\") lex.pos = (lex.pos || 0) + 1;\n        return cont(function(type, value) {\n          if (type == end || value == end) return pass()\n          return pass(what)\n        }, proceed);\n      }\n      if (type == end || value == end) return cont();\n      if (sep && sep.indexOf(\";\") > -1) return pass(what)\n      return cont(expect(end));\n    }\n    return function(type, value) {\n      if (type == end || value == end) return cont();\n      return pass(what, proceed);\n    };\n  }\n  function contCommasep(what, end, info) {\n    for (var i = 3; i < arguments.length; i++)\n      cx.cc.push(arguments[i]);\n    return cont(pushlex(end, info), commasep(what, end), poplex);\n  }\n  function block(type) {\n    if (type == \"}\") return cont();\n    return pass(statement, block);\n  }\n  function maybetype(type, value) {\n    if (isTS) {\n      if (type == \":\") return cont(typeexpr);\n      if (value == \"?\") return cont(maybetype);\n    }\n  }\n  function maybetypeOrIn(type, value) {\n    if (isTS && (type == \":\" || value == \"in\")) return cont(typeexpr)\n  }\n  function mayberettype(type) {\n    if (isTS && type == \":\") {\n      if (cx.stream.match(/^\\s*\\w+\\s+is\\b/, false)) return cont(expression, isKW, typeexpr)\n      else return cont(typeexpr)\n    }\n  }\n  function isKW(_, value) {\n    if (value == \"is\") {\n      cx.marked = \"keyword\"\n      return cont()\n    }\n  }\n  function typeexpr(type, value) {\n    if (value == \"keyof\" || value == \"typeof\" || value == \"infer\" || value == \"readonly\") {\n      cx.marked = \"keyword\"\n      return cont(value == \"typeof\" ? expressionNoComma : typeexpr)\n    }\n    if (type == \"variable\" || value == \"void\") {\n      cx.marked = \"type\"\n      return cont(afterType)\n    }\n    if (value == \"|\" || value == \"&\") return cont(typeexpr)\n    if (type == \"string\" || type == \"number\" || type == \"atom\") return cont(afterType);\n    if (type == \"[\") return cont(pushlex(\"]\"), commasep(typeexpr, \"]\", \",\"), poplex, afterType)\n    if (type == \"{\") return cont(pushlex(\"}\"), typeprops, poplex, afterType)\n    if (type == \"(\") return cont(commasep(typearg, \")\"), maybeReturnType, afterType)\n    if (type == \"<\") return cont(commasep(typeexpr, \">\"), typeexpr)\n    if (type == \"quasi\") { return pass(quasiType, afterType); }\n  }\n  function maybeReturnType(type) {\n    if (type == \"=>\") return cont(typeexpr)\n  }\n  function typeprops(type) {\n    if (type.match(/[\\}\\)\\]]/)) return cont()\n    if (type == \",\" || type == \";\") return cont(typeprops)\n    return pass(typeprop, typeprops)\n  }\n  function typeprop(type, value) {\n    if (type == \"variable\" || cx.style == \"keyword\") {\n      cx.marked = \"property\"\n      return cont(typeprop)\n    } else if (value == \"?\" || type == \"number\" || type == \"string\") {\n      return cont(typeprop)\n    } else if (type == \":\") {\n      return cont(typeexpr)\n    } else if (type == \"[\") {\n      return cont(expect(\"variable\"), maybetypeOrIn, expect(\"]\"), typeprop)\n    } else if (type == \"(\") {\n      return pass(functiondecl, typeprop)\n    } else if (!type.match(/[;\\}\\)\\],]/)) {\n      return cont()\n    }\n  }\n  function quasiType(type, value) {\n    if (type != \"quasi\") return pass();\n    if (value.slice(value.length - 2) != \"${\") return cont(quasiType);\n    return cont(typeexpr, continueQuasiType);\n  }\n  function continueQuasiType(type) {\n    if (type == \"}\") {\n      cx.marked = \"string-2\";\n      cx.state.tokenize = tokenQuasi;\n      return cont(quasiType);\n    }\n  }\n  function typearg(type, value) {\n    if (type == \"variable\" && cx.stream.match(/^\\s*[?:]/, false) || value == \"?\") return cont(typearg)\n    if (type == \":\") return cont(typeexpr)\n    if (type == \"spread\") return cont(typearg)\n    return pass(typeexpr)\n  }\n  function afterType(type, value) {\n    if (value == \"<\") return cont(pushlex(\">\"), commasep(typeexpr, \">\"), poplex, afterType)\n    if (value == \"|\" || type == \".\" || value == \"&\") return cont(typeexpr)\n    if (type == \"[\") return cont(typeexpr, expect(\"]\"), afterType)\n    if (value == \"extends\" || value == \"implements\") { cx.marked = \"keyword\"; return cont(typeexpr) }\n    if (value == \"?\") return cont(typeexpr, expect(\":\"), typeexpr)\n  }\n  function maybeTypeArgs(_, value) {\n    if (value == \"<\") return cont(pushlex(\">\"), commasep(typeexpr, \">\"), poplex, afterType)\n  }\n  function typeparam() {\n    return pass(typeexpr, maybeTypeDefault)\n  }\n  function maybeTypeDefault(_, value) {\n    if (value == \"=\") return cont(typeexpr)\n  }\n  function vardef(_, value) {\n    if (value == \"enum\") {cx.marked = \"keyword\"; return cont(enumdef)}\n    return pass(pattern, maybetype, maybeAssign, vardefCont);\n  }\n  function pattern(type, value) {\n    if (isTS && isModifier(value)) { cx.marked = \"keyword\"; return cont(pattern) }\n    if (type == \"variable\") { register(value); return cont(); }\n    if (type == \"spread\") return cont(pattern);\n    if (type == \"[\") return contCommasep(eltpattern, \"]\");\n    if (type == \"{\") return contCommasep(proppattern, \"}\");\n  }\n  function proppattern(type, value) {\n    if (type == \"variable\" && !cx.stream.match(/^\\s*:/, false)) {\n      register(value);\n      return cont(maybeAssign);\n    }\n    if (type == \"variable\") cx.marked = \"property\";\n    if (type == \"spread\") return cont(pattern);\n    if (type == \"}\") return pass();\n    if (type == \"[\") return cont(expression, expect(']'), expect(':'), proppattern);\n    return cont(expect(\":\"), pattern, maybeAssign);\n  }\n  function eltpattern() {\n    return pass(pattern, maybeAssign)\n  }\n  function maybeAssign(_type, value) {\n    if (value == \"=\") return cont(expressionNoComma);\n  }\n  function vardefCont(type) {\n    if (type == \",\") return cont(vardef);\n  }\n  function maybeelse(type, value) {\n    if (type == \"keyword b\" && value == \"else\") return cont(pushlex(\"form\", \"else\"), statement, poplex);\n  }\n  function forspec(type, value) {\n    if (value == \"await\") return cont(forspec);\n    if (type == \"(\") return cont(pushlex(\")\"), forspec1, poplex);\n  }\n  function forspec1(type) {\n    if (type == \"var\") return cont(vardef, forspec2);\n    if (type == \"variable\") return cont(forspec2);\n    return pass(forspec2)\n  }\n  function forspec2(type, value) {\n    if (type == \")\") return cont()\n    if (type == \";\") return cont(forspec2)\n    if (value == \"in\" || value == \"of\") { cx.marked = \"keyword\"; return cont(expression, forspec2) }\n    return pass(expression, forspec2)\n  }\n  function functiondef(type, value) {\n    if (value == \"*\") {cx.marked = \"keyword\"; return cont(functiondef);}\n    if (type == \"variable\") {register(value); return cont(functiondef);}\n    if (type == \"(\") return cont(pushcontext, pushlex(\")\"), commasep(funarg, \")\"), poplex, mayberettype, statement, popcontext);\n    if (isTS && value == \"<\") return cont(pushlex(\">\"), commasep(typeparam, \">\"), poplex, functiondef)\n  }\n  function functiondecl(type, value) {\n    if (value == \"*\") {cx.marked = \"keyword\"; return cont(functiondecl);}\n    if (type == \"variable\") {register(value); return cont(functiondecl);}\n    if (type == \"(\") return cont(pushcontext, pushlex(\")\"), commasep(funarg, \")\"), poplex, mayberettype, popcontext);\n    if (isTS && value == \"<\") return cont(pushlex(\">\"), commasep(typeparam, \">\"), poplex, functiondecl)\n  }\n  function typename(type, value) {\n    if (type == \"keyword\" || type == \"variable\") {\n      cx.marked = \"type\"\n      return cont(typename)\n    } else if (value == \"<\") {\n      return cont(pushlex(\">\"), commasep(typeparam, \">\"), poplex)\n    }\n  }\n  function funarg(type, value) {\n    if (value == \"@\") cont(expression, funarg)\n    if (type == \"spread\") return cont(funarg);\n    if (isTS && isModifier(value)) { cx.marked = \"keyword\"; return cont(funarg); }\n    if (isTS && type == \"this\") return cont(maybetype, maybeAssign)\n    return pass(pattern, maybetype, maybeAssign);\n  }\n  function classExpression(type, value) {\n    // Class expressions may have an optional name.\n    if (type == \"variable\") return className(type, value);\n    return classNameAfter(type, value);\n  }\n  function className(type, value) {\n    if (type == \"variable\") {register(value); return cont(classNameAfter);}\n  }\n  function classNameAfter(type, value) {\n    if (value == \"<\") return cont(pushlex(\">\"), commasep(typeparam, \">\"), poplex, classNameAfter)\n    if (value == \"extends\" || value == \"implements\" || (isTS && type == \",\")) {\n      if (value == \"implements\") cx.marked = \"keyword\";\n      return cont(isTS ? typeexpr : expression, classNameAfter);\n    }\n    if (type == \"{\") return cont(pushlex(\"}\"), classBody, poplex);\n  }\n  function classBody(type, value) {\n    if (type == \"async\" ||\n        (type == \"variable\" &&\n         (value == \"static\" || value == \"get\" || value == \"set\" || (isTS && isModifier(value))) &&\n         cx.stream.match(/^\\s+#?[\\w$\\xa1-\\uffff]/, false))) {\n      cx.marked = \"keyword\";\n      return cont(classBody);\n    }\n    if (type == \"variable\" || cx.style == \"keyword\") {\n      cx.marked = \"property\";\n      return cont(classfield, classBody);\n    }\n    if (type == \"number\" || type == \"string\") return cont(classfield, classBody);\n    if (type == \"[\")\n      return cont(expression, maybetype, expect(\"]\"), classfield, classBody)\n    if (value == \"*\") {\n      cx.marked = \"keyword\";\n      return cont(classBody);\n    }\n    if (isTS && type == \"(\") return pass(functiondecl, classBody)\n    if (type == \";\" || type == \",\") return cont(classBody);\n    if (type == \"}\") return cont();\n    if (value == \"@\") return cont(expression, classBody)\n  }\n  function classfield(type, value) {\n    if (value == \"!\") return cont(classfield)\n    if (value == \"?\") return cont(classfield)\n    if (type == \":\") return cont(typeexpr, maybeAssign)\n    if (value == \"=\") return cont(expressionNoComma)\n    var context = cx.state.lexical.prev, isInterface = context && context.info == \"interface\"\n    return pass(isInterface ? functiondecl : functiondef)\n  }\n  function afterExport(type, value) {\n    if (value == \"*\") { cx.marked = \"keyword\"; return cont(maybeFrom, expect(\";\")); }\n    if (value == \"default\") { cx.marked = \"keyword\"; return cont(expression, expect(\";\")); }\n    if (type == \"{\") return cont(commasep(exportField, \"}\"), maybeFrom, expect(\";\"));\n    return pass(statement);\n  }\n  function exportField(type, value) {\n    if (value == \"as\") { cx.marked = \"keyword\"; return cont(expect(\"variable\")); }\n    if (type == \"variable\") return pass(expressionNoComma, exportField);\n  }\n  function afterImport(type) {\n    if (type == \"string\") return cont();\n    if (type == \"(\") return pass(expression);\n    if (type == \".\") return pass(maybeoperatorComma);\n    return pass(importSpec, maybeMoreImports, maybeFrom);\n  }\n  function importSpec(type, value) {\n    if (type == \"{\") return contCommasep(importSpec, \"}\");\n    if (type == \"variable\") register(value);\n    if (value == \"*\") cx.marked = \"keyword\";\n    return cont(maybeAs);\n  }\n  function maybeMoreImports(type) {\n    if (type == \",\") return cont(importSpec, maybeMoreImports)\n  }\n  function maybeAs(_type, value) {\n    if (value == \"as\") { cx.marked = \"keyword\"; return cont(importSpec); }\n  }\n  function maybeFrom(_type, value) {\n    if (value == \"from\") { cx.marked = \"keyword\"; return cont(expression); }\n  }\n  function arrayLiteral(type) {\n    if (type == \"]\") return cont();\n    return pass(commasep(expressionNoComma, \"]\"));\n  }\n  function enumdef() {\n    return pass(pushlex(\"form\"), pattern, expect(\"{\"), pushlex(\"}\"), commasep(enummember, \"}\"), poplex, poplex)\n  }\n  function enummember() {\n    return pass(pattern, maybeAssign);\n  }\n\n  function isContinuedStatement(state, textAfter) {\n    return state.lastType == \"operator\" || state.lastType == \",\" ||\n      isOperatorChar.test(textAfter.charAt(0)) ||\n      /[,.]/.test(textAfter.charAt(0));\n  }\n\n  function expressionAllowed(stream, state, backUp) {\n    return state.tokenize == tokenBase &&\n      /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\\[{}\\(,;:]|=>)$/.test(state.lastType) ||\n      (state.lastType == \"quasi\" && /\\{\\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))\n  }\n\n  // Interface\n\n  return {\n    startState: function(basecolumn) {\n      var state = {\n        tokenize: tokenBase,\n        lastType: \"sof\",\n        cc: [],\n        lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, \"block\", false),\n        localVars: parserConfig.localVars,\n        context: parserConfig.localVars && new Context(null, null, false),\n        indented: basecolumn || 0\n      };\n      if (parserConfig.globalVars && typeof parserConfig.globalVars == \"object\")\n        state.globalVars = parserConfig.globalVars;\n      return state;\n    },\n\n    token: function(stream, state) {\n      if (stream.sol()) {\n        if (!state.lexical.hasOwnProperty(\"align\"))\n          state.lexical.align = false;\n        state.indented = stream.indentation();\n        findFatArrow(stream, state);\n      }\n      if (state.tokenize != tokenComment && stream.eatSpace()) return null;\n      var style = state.tokenize(stream, state);\n      if (type == \"comment\") return style;\n      state.lastType = type == \"operator\" && (content == \"++\" || content == \"--\") ? \"incdec\" : type;\n      return parseJS(state, style, type, content, stream);\n    },\n\n    indent: function(state, textAfter) {\n      if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass;\n      if (state.tokenize != tokenBase) return 0;\n      var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top\n      // Kludge to prevent 'maybelse' from blocking lexical scope pops\n      if (!/^\\s*else\\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {\n        var c = state.cc[i];\n        if (c == poplex) lexical = lexical.prev;\n        else if (c != maybeelse && c != popcontext) break;\n      }\n      while ((lexical.type == \"stat\" || lexical.type == \"form\") &&\n             (firstChar == \"}\" || ((top = state.cc[state.cc.length - 1]) &&\n                                   (top == maybeoperatorComma || top == maybeoperatorNoComma) &&\n                                   !/^[,\\.=+\\-*:?[\\(]/.test(textAfter))))\n        lexical = lexical.prev;\n      if (statementIndent && lexical.type == \")\" && lexical.prev.type == \"stat\")\n        lexical = lexical.prev;\n      var type = lexical.type, closing = firstChar == type;\n\n      if (type == \"vardef\") return lexical.indented + (state.lastType == \"operator\" || state.lastType == \",\" ? lexical.info.length + 1 : 0);\n      else if (type == \"form\" && firstChar == \"{\") return lexical.indented;\n      else if (type == \"form\") return lexical.indented + indentUnit;\n      else if (type == \"stat\")\n        return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);\n      else if (lexical.info == \"switch\" && !closing && parserConfig.doubleIndentSwitch != false)\n        return lexical.indented + (/^(?:case|default)\\b/.test(textAfter) ? indentUnit : 2 * indentUnit);\n      else if (lexical.align) return lexical.column + (closing ? 0 : 1);\n      else return lexical.indented + (closing ? 0 : indentUnit);\n    },\n\n    electricInput: /^\\s*(?:case .*?:|default:|\\{|\\})$/,\n    blockCommentStart: jsonMode ? null : \"/*\",\n    blockCommentEnd: jsonMode ? null : \"*/\",\n    blockCommentContinue: jsonMode ? null : \" * \",\n    lineComment: jsonMode ? null : \"//\",\n    fold: \"brace\",\n    closeBrackets: \"()[]{}''\\\"\\\"``\",\n\n    helperType: jsonMode ? \"json\" : \"javascript\",\n    jsonldMode: jsonldMode,\n    jsonMode: jsonMode,\n\n    expressionAllowed: expressionAllowed,\n\n    skipExpression: function(state) {\n      parseJS(state, \"atom\", \"atom\", \"true\", new CodeMirror.StringStream(\"\", 2, null))\n    }\n  };\n});\n\nCodeMirror.registerHelper(\"wordChars\", \"javascript\", /[\\w$]/);\n\nCodeMirror.defineMIME(\"text/javascript\", \"javascript\");\nCodeMirror.defineMIME(\"text/ecmascript\", \"javascript\");\nCodeMirror.defineMIME(\"application/javascript\", \"javascript\");\nCodeMirror.defineMIME(\"application/x-javascript\", \"javascript\");\nCodeMirror.defineMIME(\"application/ecmascript\", \"javascript\");\nCodeMirror.defineMIME(\"application/json\", { name: \"javascript\", json: true });\nCodeMirror.defineMIME(\"application/x-json\", { name: \"javascript\", json: true });\nCodeMirror.defineMIME(\"application/manifest+json\", { name: \"javascript\", json: true })\nCodeMirror.defineMIME(\"application/ld+json\", { name: \"javascript\", jsonld: true });\nCodeMirror.defineMIME(\"text/typescript\", { name: \"javascript\", typescript: true });\nCodeMirror.defineMIME(\"application/typescript\", { name: \"javascript\", typescript: true });\n\n});\n"
  },
  {
    "path": "web/assets/codemirror/jshint.js",
    "content": "/*! 2.13.2 */\nvar JSHINT;\nif (typeof window === 'undefined') window = {};\n(function () {\nvar require;\nrequire=(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<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){\nvar identifierStartTable = [];\n\nfor (var i = 0; i < 128; i++) {\n  identifierStartTable[i] =\n    i === 36 ||           // $\n    i >= 65 && i <= 90 || // A-Z\n    i === 95 ||           // _\n    i >= 97 && i <= 122;  // a-z\n}\n\nvar identifierPartTable = [];\n\nfor (var i = 0; i < 128; i++) {\n  identifierPartTable[i] =\n    identifierStartTable[i] || // $, _, A-Z, a-z\n    i >= 48 && i <= 57;        // 0-9\n}\n\nmodule.exports = {\n  asciiIdentifierStartTable: identifierStartTable,\n  asciiIdentifierPartTable: identifierPartTable\n};\n\n},{}],2:[function(require,module,exports){\nmodule.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])*$/;\n},{}],3:[function(require,module,exports){\nvar 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';\nvar arr = str.split(',').map(function(code) {\n  return parseInt(code, 10);\n});\nmodule.exports = arr;\n},{}],4:[function(require,module,exports){\nvar 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';\nvar arr = str.split(',').map(function(code) {\n  return parseInt(code, 10);\n});\nmodule.exports = arr;\n},{}],5:[function(require,module,exports){\n// http://wiki.commonjs.org/wiki/Unit_Testing/1.0\n//\n// THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8!\n//\n// Originally from narwhal.js (http://narwhaljs.org)\n// Copyright (c) 2009 Thomas Robinson <280north.com>\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the 'Software'), to\n// deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n// sell copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n// when used in node, this will actually load the util module we depend on\n// versus loading the builtin util module as happens otherwise\n// this is a bug in node module loading as far as I am concerned\nvar util = require('util/');\n\nvar pSlice = Array.prototype.slice;\nvar hasOwn = Object.prototype.hasOwnProperty;\n\n// 1. The assert module provides functions that throw\n// AssertionError's when particular conditions are not met. The\n// assert module must conform to the following interface.\n\nvar assert = module.exports = ok;\n\n// 2. The AssertionError is defined in assert.\n// new assert.AssertionError({ message: message,\n//                             actual: actual,\n//                             expected: expected })\n\nassert.AssertionError = function AssertionError(options) {\n  this.name = 'AssertionError';\n  this.actual = options.actual;\n  this.expected = options.expected;\n  this.operator = options.operator;\n  if (options.message) {\n    this.message = options.message;\n    this.generatedMessage = false;\n  } else {\n    this.message = getMessage(this);\n    this.generatedMessage = true;\n  }\n  var stackStartFunction = options.stackStartFunction || fail;\n\n  if (Error.captureStackTrace) {\n    Error.captureStackTrace(this, stackStartFunction);\n  }\n  else {\n    // non v8 browsers so we can have a stacktrace\n    var err = new Error();\n    if (err.stack) {\n      var out = err.stack;\n\n      // try to strip useless frames\n      var fn_name = stackStartFunction.name;\n      var idx = out.indexOf('\\n' + fn_name);\n      if (idx >= 0) {\n        // once we have located the function frame\n        // we need to strip out everything before it (and its line)\n        var next_line = out.indexOf('\\n', idx + 1);\n        out = out.substring(next_line + 1);\n      }\n\n      this.stack = out;\n    }\n  }\n};\n\n// assert.AssertionError instanceof Error\nutil.inherits(assert.AssertionError, Error);\n\nfunction replacer(key, value) {\n  if (util.isUndefined(value)) {\n    return '' + value;\n  }\n  if (util.isNumber(value) && !isFinite(value)) {\n    return value.toString();\n  }\n  if (util.isFunction(value) || util.isRegExp(value)) {\n    return value.toString();\n  }\n  return value;\n}\n\nfunction truncate(s, n) {\n  if (util.isString(s)) {\n    return s.length < n ? s : s.slice(0, n);\n  } else {\n    return s;\n  }\n}\n\nfunction getMessage(self) {\n  return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' +\n         self.operator + ' ' +\n         truncate(JSON.stringify(self.expected, replacer), 128);\n}\n\n// At present only the three keys mentioned above are used and\n// understood by the spec. Implementations or sub modules can pass\n// other keys to the AssertionError's constructor - they will be\n// ignored.\n\n// 3. All of the following functions must throw an AssertionError\n// when a corresponding condition is not met, with a message that\n// may be undefined if not provided.  All assertion methods provide\n// both the actual and expected values to the assertion error for\n// display purposes.\n\nfunction fail(actual, expected, message, operator, stackStartFunction) {\n  throw new assert.AssertionError({\n    message: message,\n    actual: actual,\n    expected: expected,\n    operator: operator,\n    stackStartFunction: stackStartFunction\n  });\n}\n\n// EXTENSION! allows for well behaved errors defined elsewhere.\nassert.fail = fail;\n\n// 4. Pure assertion tests whether a value is truthy, as determined\n// by !!guard.\n// assert.ok(guard, message_opt);\n// This statement is equivalent to assert.equal(true, !!guard,\n// message_opt);. To test strictly for the value true, use\n// assert.strictEqual(true, guard, message_opt);.\n\nfunction ok(value, message) {\n  if (!value) fail(value, true, message, '==', assert.ok);\n}\nassert.ok = ok;\n\n// 5. The equality assertion tests shallow, coercive equality with\n// ==.\n// assert.equal(actual, expected, message_opt);\n\nassert.equal = function equal(actual, expected, message) {\n  if (actual != expected) fail(actual, expected, message, '==', assert.equal);\n};\n\n// 6. The non-equality assertion tests for whether two objects are not equal\n// with != assert.notEqual(actual, expected, message_opt);\n\nassert.notEqual = function notEqual(actual, expected, message) {\n  if (actual == expected) {\n    fail(actual, expected, message, '!=', assert.notEqual);\n  }\n};\n\n// 7. The equivalence assertion tests a deep equality relation.\n// assert.deepEqual(actual, expected, message_opt);\n\nassert.deepEqual = function deepEqual(actual, expected, message) {\n  if (!_deepEqual(actual, expected)) {\n    fail(actual, expected, message, 'deepEqual', assert.deepEqual);\n  }\n};\n\nfunction _deepEqual(actual, expected) {\n  // 7.1. All identical values are equivalent, as determined by ===.\n  if (actual === expected) {\n    return true;\n\n  } else if (util.isBuffer(actual) && util.isBuffer(expected)) {\n    if (actual.length != expected.length) return false;\n\n    for (var i = 0; i < actual.length; i++) {\n      if (actual[i] !== expected[i]) return false;\n    }\n\n    return true;\n\n  // 7.2. If the expected value is a Date object, the actual value is\n  // equivalent if it is also a Date object that refers to the same time.\n  } else if (util.isDate(actual) && util.isDate(expected)) {\n    return actual.getTime() === expected.getTime();\n\n  // 7.3 If the expected value is a RegExp object, the actual value is\n  // equivalent if it is also a RegExp object with the same source and\n  // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`).\n  } else if (util.isRegExp(actual) && util.isRegExp(expected)) {\n    return actual.source === expected.source &&\n           actual.global === expected.global &&\n           actual.multiline === expected.multiline &&\n           actual.lastIndex === expected.lastIndex &&\n           actual.ignoreCase === expected.ignoreCase;\n\n  // 7.4. Other pairs that do not both pass typeof value == 'object',\n  // equivalence is determined by ==.\n  } else if (!util.isObject(actual) && !util.isObject(expected)) {\n    return actual == expected;\n\n  // 7.5 For all other Object pairs, including Array objects, equivalence is\n  // determined by having the same number of owned properties (as verified\n  // with Object.prototype.hasOwnProperty.call), the same set of keys\n  // (although not necessarily the same order), equivalent values for every\n  // corresponding key, and an identical 'prototype' property. Note: this\n  // accounts for both named and indexed properties on Arrays.\n  } else {\n    return objEquiv(actual, expected);\n  }\n}\n\nfunction isArguments(object) {\n  return Object.prototype.toString.call(object) == '[object Arguments]';\n}\n\nfunction objEquiv(a, b) {\n  if (util.isNullOrUndefined(a) || util.isNullOrUndefined(b))\n    return false;\n  // an identical 'prototype' property.\n  if (a.prototype !== b.prototype) return false;\n  // if one is a primitive, the other must be same\n  if (util.isPrimitive(a) || util.isPrimitive(b)) {\n    return a === b;\n  }\n  var aIsArgs = isArguments(a),\n      bIsArgs = isArguments(b);\n  if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs))\n    return false;\n  if (aIsArgs) {\n    a = pSlice.call(a);\n    b = pSlice.call(b);\n    return _deepEqual(a, b);\n  }\n  var ka = objectKeys(a),\n      kb = objectKeys(b),\n      key, i;\n  // having the same number of owned properties (keys incorporates\n  // hasOwnProperty)\n  if (ka.length != kb.length)\n    return false;\n  //the same set of keys (although not necessarily the same order),\n  ka.sort();\n  kb.sort();\n  //~~~cheap key test\n  for (i = ka.length - 1; i >= 0; i--) {\n    if (ka[i] != kb[i])\n      return false;\n  }\n  //equivalent values for every corresponding key, and\n  //~~~possibly expensive deep test\n  for (i = ka.length - 1; i >= 0; i--) {\n    key = ka[i];\n    if (!_deepEqual(a[key], b[key])) return false;\n  }\n  return true;\n}\n\n// 8. The non-equivalence assertion tests for any deep inequality.\n// assert.notDeepEqual(actual, expected, message_opt);\n\nassert.notDeepEqual = function notDeepEqual(actual, expected, message) {\n  if (_deepEqual(actual, expected)) {\n    fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual);\n  }\n};\n\n// 9. The strict equality assertion tests strict equality, as determined by ===.\n// assert.strictEqual(actual, expected, message_opt);\n\nassert.strictEqual = function strictEqual(actual, expected, message) {\n  if (actual !== expected) {\n    fail(actual, expected, message, '===', assert.strictEqual);\n  }\n};\n\n// 10. The strict non-equality assertion tests for strict inequality, as\n// determined by !==.  assert.notStrictEqual(actual, expected, message_opt);\n\nassert.notStrictEqual = function notStrictEqual(actual, expected, message) {\n  if (actual === expected) {\n    fail(actual, expected, message, '!==', assert.notStrictEqual);\n  }\n};\n\nfunction expectedException(actual, expected) {\n  if (!actual || !expected) {\n    return false;\n  }\n\n  if (Object.prototype.toString.call(expected) == '[object RegExp]') {\n    return expected.test(actual);\n  } else if (actual instanceof expected) {\n    return true;\n  } else if (expected.call({}, actual) === true) {\n    return true;\n  }\n\n  return false;\n}\n\nfunction _throws(shouldThrow, block, expected, message) {\n  var actual;\n\n  if (util.isString(expected)) {\n    message = expected;\n    expected = null;\n  }\n\n  try {\n    block();\n  } catch (e) {\n    actual = e;\n  }\n\n  message = (expected && expected.name ? ' (' + expected.name + ').' : '.') +\n            (message ? ' ' + message : '.');\n\n  if (shouldThrow && !actual) {\n    fail(actual, expected, 'Missing expected exception' + message);\n  }\n\n  if (!shouldThrow && expectedException(actual, expected)) {\n    fail(actual, expected, 'Got unwanted exception' + message);\n  }\n\n  if ((shouldThrow && actual && expected &&\n      !expectedException(actual, expected)) || (!shouldThrow && actual)) {\n    throw actual;\n  }\n}\n\n// 11. Expected to throw an error:\n// assert.throws(block, Error_opt, message_opt);\n\nassert.throws = function(block, /*optional*/error, /*optional*/message) {\n  _throws.apply(this, [true].concat(pSlice.call(arguments)));\n};\n\n// EXTENSION! This is annoying to write outside this module.\nassert.doesNotThrow = function(block, /*optional*/message) {\n  _throws.apply(this, [false].concat(pSlice.call(arguments)));\n};\n\nassert.ifError = function(err) { if (err) {throw err;}};\n\nvar objectKeys = Object.keys || function (obj) {\n  var keys = [];\n  for (var key in obj) {\n    if (hasOwn.call(obj, key)) keys.push(key);\n  }\n  return keys;\n};\n\n},{\"util/\":8}],6:[function(require,module,exports){\nif (typeof Object.create === 'function') {\n  // implementation from standard node.js 'util' module\n  module.exports = function inherits(ctor, superCtor) {\n    ctor.super_ = superCtor\n    ctor.prototype = Object.create(superCtor.prototype, {\n      constructor: {\n        value: ctor,\n        enumerable: false,\n        writable: true,\n        configurable: true\n      }\n    });\n  };\n} else {\n  // old school shim for old browsers\n  module.exports = function inherits(ctor, superCtor) {\n    ctor.super_ = superCtor\n    var TempCtor = function () {}\n    TempCtor.prototype = superCtor.prototype\n    ctor.prototype = new TempCtor()\n    ctor.prototype.constructor = ctor\n  }\n}\n\n},{}],7:[function(require,module,exports){\nmodule.exports = function isBuffer(arg) {\n  return arg && typeof arg === 'object'\n    && typeof arg.copy === 'function'\n    && typeof arg.fill === 'function'\n    && typeof arg.readUInt8 === 'function';\n}\n},{}],8:[function(require,module,exports){\n(function (process,global){\n// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\nvar formatRegExp = /%[sdj%]/g;\nexports.format = function(f) {\n  if (!isString(f)) {\n    var objects = [];\n    for (var i = 0; i < arguments.length; i++) {\n      objects.push(inspect(arguments[i]));\n    }\n    return objects.join(' ');\n  }\n\n  var i = 1;\n  var args = arguments;\n  var len = args.length;\n  var str = String(f).replace(formatRegExp, function(x) {\n    if (x === '%%') return '%';\n    if (i >= len) return x;\n    switch (x) {\n      case '%s': return String(args[i++]);\n      case '%d': return Number(args[i++]);\n      case '%j':\n        try {\n          return JSON.stringify(args[i++]);\n        } catch (_) {\n          return '[Circular]';\n        }\n      default:\n        return x;\n    }\n  });\n  for (var x = args[i]; i < len; x = args[++i]) {\n    if (isNull(x) || !isObject(x)) {\n      str += ' ' + x;\n    } else {\n      str += ' ' + inspect(x);\n    }\n  }\n  return str;\n};\n\n\n// Mark that a method should not be used.\n// Returns a modified function which warns once by default.\n// If --no-deprecation is set, then it is a no-op.\nexports.deprecate = function(fn, msg) {\n  // Allow for deprecating things in the process of starting up.\n  if (isUndefined(global.process)) {\n    return function() {\n      return exports.deprecate(fn, msg).apply(this, arguments);\n    };\n  }\n\n  if (process.noDeprecation === true) {\n    return fn;\n  }\n\n  var warned = false;\n  function deprecated() {\n    if (!warned) {\n      if (process.throwDeprecation) {\n        throw new Error(msg);\n      } else if (process.traceDeprecation) {\n        console.trace(msg);\n      } else {\n        console.error(msg);\n      }\n      warned = true;\n    }\n    return fn.apply(this, arguments);\n  }\n\n  return deprecated;\n};\n\n\nvar debugs = {};\nvar debugEnviron;\nexports.debuglog = function(set) {\n  if (isUndefined(debugEnviron))\n    debugEnviron = process.env.NODE_DEBUG || '';\n  set = set.toUpperCase();\n  if (!debugs[set]) {\n    if (new RegExp('\\\\b' + set + '\\\\b', 'i').test(debugEnviron)) {\n      var pid = process.pid;\n      debugs[set] = function() {\n        var msg = exports.format.apply(exports, arguments);\n        console.error('%s %d: %s', set, pid, msg);\n      };\n    } else {\n      debugs[set] = function() {};\n    }\n  }\n  return debugs[set];\n};\n\n\n/**\n * Echos the value of a value. Trys to print the value out\n * in the best way possible given the different types.\n *\n * @param {Object} obj The object to print out.\n * @param {Object} opts Optional options object that alters the output.\n */\n/* legacy: obj, showHidden, depth, colors*/\nfunction inspect(obj, opts) {\n  // default options\n  var ctx = {\n    seen: [],\n    stylize: stylizeNoColor\n  };\n  // legacy...\n  if (arguments.length >= 3) ctx.depth = arguments[2];\n  if (arguments.length >= 4) ctx.colors = arguments[3];\n  if (isBoolean(opts)) {\n    // legacy...\n    ctx.showHidden = opts;\n  } else if (opts) {\n    // got an \"options\" object\n    exports._extend(ctx, opts);\n  }\n  // set default options\n  if (isUndefined(ctx.showHidden)) ctx.showHidden = false;\n  if (isUndefined(ctx.depth)) ctx.depth = 2;\n  if (isUndefined(ctx.colors)) ctx.colors = false;\n  if (isUndefined(ctx.customInspect)) ctx.customInspect = true;\n  if (ctx.colors) ctx.stylize = stylizeWithColor;\n  return formatValue(ctx, obj, ctx.depth);\n}\nexports.inspect = inspect;\n\n\n// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics\ninspect.colors = {\n  'bold' : [1, 22],\n  'italic' : [3, 23],\n  'underline' : [4, 24],\n  'inverse' : [7, 27],\n  'white' : [37, 39],\n  'grey' : [90, 39],\n  'black' : [30, 39],\n  'blue' : [34, 39],\n  'cyan' : [36, 39],\n  'green' : [32, 39],\n  'magenta' : [35, 39],\n  'red' : [31, 39],\n  'yellow' : [33, 39]\n};\n\n// Don't use 'blue' not visible on cmd.exe\ninspect.styles = {\n  'special': 'cyan',\n  'number': 'yellow',\n  'boolean': 'yellow',\n  'undefined': 'grey',\n  'null': 'bold',\n  'string': 'green',\n  'date': 'magenta',\n  // \"name\": intentionally not styling\n  'regexp': 'red'\n};\n\n\nfunction stylizeWithColor(str, styleType) {\n  var style = inspect.styles[styleType];\n\n  if (style) {\n    return '\\u001b[' + inspect.colors[style][0] + 'm' + str +\n           '\\u001b[' + inspect.colors[style][1] + 'm';\n  } else {\n    return str;\n  }\n}\n\n\nfunction stylizeNoColor(str, styleType) {\n  return str;\n}\n\n\nfunction arrayToHash(array) {\n  var hash = {};\n\n  array.forEach(function(val, idx) {\n    hash[val] = true;\n  });\n\n  return hash;\n}\n\n\nfunction formatValue(ctx, value, recurseTimes) {\n  // Provide a hook for user-specified inspect functions.\n  // Check that value is an object with an inspect function on it\n  if (ctx.customInspect &&\n      value &&\n      isFunction(value.inspect) &&\n      // Filter out the util module, it's inspect function is special\n      value.inspect !== exports.inspect &&\n      // Also filter out any prototype objects using the circular check.\n      !(value.constructor && value.constructor.prototype === value)) {\n    var ret = value.inspect(recurseTimes, ctx);\n    if (!isString(ret)) {\n      ret = formatValue(ctx, ret, recurseTimes);\n    }\n    return ret;\n  }\n\n  // Primitive types cannot have properties\n  var primitive = formatPrimitive(ctx, value);\n  if (primitive) {\n    return primitive;\n  }\n\n  // Look up the keys of the object.\n  var keys = Object.keys(value);\n  var visibleKeys = arrayToHash(keys);\n\n  if (ctx.showHidden) {\n    keys = Object.getOwnPropertyNames(value);\n  }\n\n  // IE doesn't make error fields non-enumerable\n  // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx\n  if (isError(value)\n      && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {\n    return formatError(value);\n  }\n\n  // Some type of object without properties can be shortcutted.\n  if (keys.length === 0) {\n    if (isFunction(value)) {\n      var name = value.name ? ': ' + value.name : '';\n      return ctx.stylize('[Function' + name + ']', 'special');\n    }\n    if (isRegExp(value)) {\n      return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');\n    }\n    if (isDate(value)) {\n      return ctx.stylize(Date.prototype.toString.call(value), 'date');\n    }\n    if (isError(value)) {\n      return formatError(value);\n    }\n  }\n\n  var base = '', array = false, braces = ['{', '}'];\n\n  // Make Array say that they are Array\n  if (isArray(value)) {\n    array = true;\n    braces = ['[', ']'];\n  }\n\n  // Make functions say that they are functions\n  if (isFunction(value)) {\n    var n = value.name ? ': ' + value.name : '';\n    base = ' [Function' + n + ']';\n  }\n\n  // Make RegExps say that they are RegExps\n  if (isRegExp(value)) {\n    base = ' ' + RegExp.prototype.toString.call(value);\n  }\n\n  // Make dates with properties first say the date\n  if (isDate(value)) {\n    base = ' ' + Date.prototype.toUTCString.call(value);\n  }\n\n  // Make error with message first say the error\n  if (isError(value)) {\n    base = ' ' + formatError(value);\n  }\n\n  if (keys.length === 0 && (!array || value.length == 0)) {\n    return braces[0] + base + braces[1];\n  }\n\n  if (recurseTimes < 0) {\n    if (isRegExp(value)) {\n      return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');\n    } else {\n      return ctx.stylize('[Object]', 'special');\n    }\n  }\n\n  ctx.seen.push(value);\n\n  var output;\n  if (array) {\n    output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);\n  } else {\n    output = keys.map(function(key) {\n      return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);\n    });\n  }\n\n  ctx.seen.pop();\n\n  return reduceToSingleString(output, base, braces);\n}\n\n\nfunction formatPrimitive(ctx, value) {\n  if (isUndefined(value))\n    return ctx.stylize('undefined', 'undefined');\n  if (isString(value)) {\n    var simple = '\\'' + JSON.stringify(value).replace(/^\"|\"$/g, '')\n                                             .replace(/'/g, \"\\\\'\")\n                                             .replace(/\\\\\"/g, '\"') + '\\'';\n    return ctx.stylize(simple, 'string');\n  }\n  if (isNumber(value))\n    return ctx.stylize('' + value, 'number');\n  if (isBoolean(value))\n    return ctx.stylize('' + value, 'boolean');\n  // For some reason typeof null is \"object\", so special case here.\n  if (isNull(value))\n    return ctx.stylize('null', 'null');\n}\n\n\nfunction formatError(value) {\n  return '[' + Error.prototype.toString.call(value) + ']';\n}\n\n\nfunction formatArray(ctx, value, recurseTimes, visibleKeys, keys) {\n  var output = [];\n  for (var i = 0, l = value.length; i < l; ++i) {\n    if (hasOwnProperty(value, String(i))) {\n      output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,\n          String(i), true));\n    } else {\n      output.push('');\n    }\n  }\n  keys.forEach(function(key) {\n    if (!key.match(/^\\d+$/)) {\n      output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,\n          key, true));\n    }\n  });\n  return output;\n}\n\n\nfunction formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {\n  var name, str, desc;\n  desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };\n  if (desc.get) {\n    if (desc.set) {\n      str = ctx.stylize('[Getter/Setter]', 'special');\n    } else {\n      str = ctx.stylize('[Getter]', 'special');\n    }\n  } else {\n    if (desc.set) {\n      str = ctx.stylize('[Setter]', 'special');\n    }\n  }\n  if (!hasOwnProperty(visibleKeys, key)) {\n    name = '[' + key + ']';\n  }\n  if (!str) {\n    if (ctx.seen.indexOf(desc.value) < 0) {\n      if (isNull(recurseTimes)) {\n        str = formatValue(ctx, desc.value, null);\n      } else {\n        str = formatValue(ctx, desc.value, recurseTimes - 1);\n      }\n      if (str.indexOf('\\n') > -1) {\n        if (array) {\n          str = str.split('\\n').map(function(line) {\n            return '  ' + line;\n          }).join('\\n').substr(2);\n        } else {\n          str = '\\n' + str.split('\\n').map(function(line) {\n            return '   ' + line;\n          }).join('\\n');\n        }\n      }\n    } else {\n      str = ctx.stylize('[Circular]', 'special');\n    }\n  }\n  if (isUndefined(name)) {\n    if (array && key.match(/^\\d+$/)) {\n      return str;\n    }\n    name = JSON.stringify('' + key);\n    if (name.match(/^\"([a-zA-Z_][a-zA-Z_0-9]*)\"$/)) {\n      name = name.substr(1, name.length - 2);\n      name = ctx.stylize(name, 'name');\n    } else {\n      name = name.replace(/'/g, \"\\\\'\")\n                 .replace(/\\\\\"/g, '\"')\n                 .replace(/(^\"|\"$)/g, \"'\");\n      name = ctx.stylize(name, 'string');\n    }\n  }\n\n  return name + ': ' + str;\n}\n\n\nfunction reduceToSingleString(output, base, braces) {\n  var numLinesEst = 0;\n  var length = output.reduce(function(prev, cur) {\n    numLinesEst++;\n    if (cur.indexOf('\\n') >= 0) numLinesEst++;\n    return prev + cur.replace(/\\u001b\\[\\d\\d?m/g, '').length + 1;\n  }, 0);\n\n  if (length > 60) {\n    return braces[0] +\n           (base === '' ? '' : base + '\\n ') +\n           ' ' +\n           output.join(',\\n  ') +\n           ' ' +\n           braces[1];\n  }\n\n  return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];\n}\n\n\n// NOTE: These type checking functions intentionally don't use `instanceof`\n// because it is fragile and can be easily faked with `Object.create()`.\nfunction isArray(ar) {\n  return Array.isArray(ar);\n}\nexports.isArray = isArray;\n\nfunction isBoolean(arg) {\n  return typeof arg === 'boolean';\n}\nexports.isBoolean = isBoolean;\n\nfunction isNull(arg) {\n  return arg === null;\n}\nexports.isNull = isNull;\n\nfunction isNullOrUndefined(arg) {\n  return arg == null;\n}\nexports.isNullOrUndefined = isNullOrUndefined;\n\nfunction isNumber(arg) {\n  return typeof arg === 'number';\n}\nexports.isNumber = isNumber;\n\nfunction isString(arg) {\n  return typeof arg === 'string';\n}\nexports.isString = isString;\n\nfunction isSymbol(arg) {\n  return typeof arg === 'symbol';\n}\nexports.isSymbol = isSymbol;\n\nfunction isUndefined(arg) {\n  return arg === void 0;\n}\nexports.isUndefined = isUndefined;\n\nfunction isRegExp(re) {\n  return isObject(re) && objectToString(re) === '[object RegExp]';\n}\nexports.isRegExp = isRegExp;\n\nfunction isObject(arg) {\n  return typeof arg === 'object' && arg !== null;\n}\nexports.isObject = isObject;\n\nfunction isDate(d) {\n  return isObject(d) && objectToString(d) === '[object Date]';\n}\nexports.isDate = isDate;\n\nfunction isError(e) {\n  return isObject(e) &&\n      (objectToString(e) === '[object Error]' || e instanceof Error);\n}\nexports.isError = isError;\n\nfunction isFunction(arg) {\n  return typeof arg === 'function';\n}\nexports.isFunction = isFunction;\n\nfunction isPrimitive(arg) {\n  return arg === null ||\n         typeof arg === 'boolean' ||\n         typeof arg === 'number' ||\n         typeof arg === 'string' ||\n         typeof arg === 'symbol' ||  // ES6 symbol\n         typeof arg === 'undefined';\n}\nexports.isPrimitive = isPrimitive;\n\nexports.isBuffer = require('./support/isBuffer');\n\nfunction objectToString(o) {\n  return Object.prototype.toString.call(o);\n}\n\n\nfunction pad(n) {\n  return n < 10 ? '0' + n.toString(10) : n.toString(10);\n}\n\n\nvar months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',\n              'Oct', 'Nov', 'Dec'];\n\n// 26 Feb 16:19:34\nfunction timestamp() {\n  var d = new Date();\n  var time = [pad(d.getHours()),\n              pad(d.getMinutes()),\n              pad(d.getSeconds())].join(':');\n  return [d.getDate(), months[d.getMonth()], time].join(' ');\n}\n\n\n// log is just a thin wrapper to console.log that prepends a timestamp\nexports.log = function() {\n  console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments));\n};\n\n\n/**\n * Inherit the prototype methods from one constructor into another.\n *\n * The Function.prototype.inherits from lang.js rewritten as a standalone\n * function (not on Function.prototype). NOTE: If this file is to be loaded\n * during bootstrapping this function needs to be rewritten using some native\n * functions as prototype setup using normal JavaScript does not work as\n * expected during bootstrapping (see mirror.js in r114903).\n *\n * @param {function} ctor Constructor function which needs to inherit the\n *     prototype.\n * @param {function} superCtor Constructor function to inherit prototype from.\n */\nexports.inherits = require('inherits');\n\nexports._extend = function(origin, add) {\n  // Don't do anything if add isn't an object\n  if (!add || !isObject(add)) return origin;\n\n  var keys = Object.keys(add);\n  var i = keys.length;\n  while (i--) {\n    origin[keys[i]] = add[keys[i]];\n  }\n  return origin;\n};\n\nfunction hasOwnProperty(obj, prop) {\n  return Object.prototype.hasOwnProperty.call(obj, prop);\n}\n\n}).call(this,require('_process'),typeof global !== \"undefined\" ? global : typeof self !== \"undefined\" ? self : typeof window !== \"undefined\" ? window : {})\n},{\"./support/isBuffer\":7,\"_process\":13,\"inherits\":6}],9:[function(require,module,exports){\n(function (global){\n/*global window, global*/\nvar util = require(\"util\")\nvar assert = require(\"assert\")\nvar now = require(\"date-now\")\n\nvar slice = Array.prototype.slice\nvar console\nvar times = {}\n\nif (typeof global !== \"undefined\" && global.console) {\n    console = global.console\n} else if (typeof window !== \"undefined\" && window.console) {\n    console = window.console\n} else {\n    console = {}\n}\n\nvar functions = [\n    [log, \"log\"],\n    [info, \"info\"],\n    [warn, \"warn\"],\n    [error, \"error\"],\n    [time, \"time\"],\n    [timeEnd, \"timeEnd\"],\n    [trace, \"trace\"],\n    [dir, \"dir\"],\n    [consoleAssert, \"assert\"]\n]\n\nfor (var i = 0; i < functions.length; i++) {\n    var tuple = functions[i]\n    var f = tuple[0]\n    var name = tuple[1]\n\n    if (!console[name]) {\n        console[name] = f\n    }\n}\n\nmodule.exports = console\n\nfunction log() {}\n\nfunction info() {\n    console.log.apply(console, arguments)\n}\n\nfunction warn() {\n    console.log.apply(console, arguments)\n}\n\nfunction error() {\n    console.warn.apply(console, arguments)\n}\n\nfunction time(label) {\n    times[label] = now()\n}\n\nfunction timeEnd(label) {\n    var time = times[label]\n    if (!time) {\n        throw new Error(\"No such label: \" + label)\n    }\n\n    var duration = now() - time\n    console.log(label + \": \" + duration + \"ms\")\n}\n\nfunction trace() {\n    var err = new Error()\n    err.name = \"Trace\"\n    err.message = util.format.apply(null, arguments)\n    console.error(err.stack)\n}\n\nfunction dir(object) {\n    console.log(util.inspect(object) + \"\\n\")\n}\n\nfunction consoleAssert(expression) {\n    if (!expression) {\n        var arr = slice.call(arguments, 1)\n        assert.ok(false, util.format.apply(null, arr))\n    }\n}\n\n}).call(this,typeof global !== \"undefined\" ? global : typeof self !== \"undefined\" ? self : typeof window !== \"undefined\" ? window : {})\n},{\"assert\":5,\"date-now\":10,\"util\":16}],10:[function(require,module,exports){\nmodule.exports = now\n\nfunction now() {\n    return new Date().getTime()\n}\n\n},{}],11:[function(require,module,exports){\n// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\nfunction EventEmitter() {\n  this._events = this._events || {};\n  this._maxListeners = this._maxListeners || undefined;\n}\nmodule.exports = EventEmitter;\n\n// Backwards-compat with node 0.10.x\nEventEmitter.EventEmitter = EventEmitter;\n\nEventEmitter.prototype._events = undefined;\nEventEmitter.prototype._maxListeners = undefined;\n\n// By default EventEmitters will print a warning if more than 10 listeners are\n// added to it. This is a useful default which helps finding memory leaks.\nEventEmitter.defaultMaxListeners = 10;\n\n// Obviously not all Emitters should be limited to 10. This function allows\n// that to be increased. Set to zero for unlimited.\nEventEmitter.prototype.setMaxListeners = function(n) {\n  if (!isNumber(n) || n < 0 || isNaN(n))\n    throw TypeError('n must be a positive number');\n  this._maxListeners = n;\n  return this;\n};\n\nEventEmitter.prototype.emit = function(type) {\n  var er, handler, len, args, i, listeners;\n\n  if (!this._events)\n    this._events = {};\n\n  // If there is no 'error' event listener then throw.\n  if (type === 'error') {\n    if (!this._events.error ||\n        (isObject(this._events.error) && !this._events.error.length)) {\n      er = arguments[1];\n      if (er instanceof Error) {\n        throw er; // Unhandled 'error' event\n      }\n      throw TypeError('Uncaught, unspecified \"error\" event.');\n    }\n  }\n\n  handler = this._events[type];\n\n  if (isUndefined(handler))\n    return false;\n\n  if (isFunction(handler)) {\n    switch (arguments.length) {\n      // fast cases\n      case 1:\n        handler.call(this);\n        break;\n      case 2:\n        handler.call(this, arguments[1]);\n        break;\n      case 3:\n        handler.call(this, arguments[1], arguments[2]);\n        break;\n      // slower\n      default:\n        len = arguments.length;\n        args = new Array(len - 1);\n        for (i = 1; i < len; i++)\n          args[i - 1] = arguments[i];\n        handler.apply(this, args);\n    }\n  } else if (isObject(handler)) {\n    len = arguments.length;\n    args = new Array(len - 1);\n    for (i = 1; i < len; i++)\n      args[i - 1] = arguments[i];\n\n    listeners = handler.slice();\n    len = listeners.length;\n    for (i = 0; i < len; i++)\n      listeners[i].apply(this, args);\n  }\n\n  return true;\n};\n\nEventEmitter.prototype.addListener = function(type, listener) {\n  var m;\n\n  if (!isFunction(listener))\n    throw TypeError('listener must be a function');\n\n  if (!this._events)\n    this._events = {};\n\n  // To avoid recursion in the case that type === \"newListener\"! Before\n  // adding it to the listeners, first emit \"newListener\".\n  if (this._events.newListener)\n    this.emit('newListener', type,\n              isFunction(listener.listener) ?\n              listener.listener : listener);\n\n  if (!this._events[type])\n    // Optimize the case of one listener. Don't need the extra array object.\n    this._events[type] = listener;\n  else if (isObject(this._events[type]))\n    // If we've already got an array, just append.\n    this._events[type].push(listener);\n  else\n    // Adding the second element, need to change to array.\n    this._events[type] = [this._events[type], listener];\n\n  // Check for listener leak\n  if (isObject(this._events[type]) && !this._events[type].warned) {\n    var m;\n    if (!isUndefined(this._maxListeners)) {\n      m = this._maxListeners;\n    } else {\n      m = EventEmitter.defaultMaxListeners;\n    }\n\n    if (m && m > 0 && this._events[type].length > m) {\n      this._events[type].warned = true;\n      console.error('(node) warning: possible EventEmitter memory ' +\n                    'leak detected. %d listeners added. ' +\n                    'Use emitter.setMaxListeners() to increase limit.',\n                    this._events[type].length);\n      if (typeof console.trace === 'function') {\n        // not supported in IE 10\n        console.trace();\n      }\n    }\n  }\n\n  return this;\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.once = function(type, listener) {\n  if (!isFunction(listener))\n    throw TypeError('listener must be a function');\n\n  var fired = false;\n\n  function g() {\n    this.removeListener(type, g);\n\n    if (!fired) {\n      fired = true;\n      listener.apply(this, arguments);\n    }\n  }\n\n  g.listener = listener;\n  this.on(type, g);\n\n  return this;\n};\n\n// emits a 'removeListener' event iff the listener was removed\nEventEmitter.prototype.removeListener = function(type, listener) {\n  var list, position, length, i;\n\n  if (!isFunction(listener))\n    throw TypeError('listener must be a function');\n\n  if (!this._events || !this._events[type])\n    return this;\n\n  list = this._events[type];\n  length = list.length;\n  position = -1;\n\n  if (list === listener ||\n      (isFunction(list.listener) && list.listener === listener)) {\n    delete this._events[type];\n    if (this._events.removeListener)\n      this.emit('removeListener', type, listener);\n\n  } else if (isObject(list)) {\n    for (i = length; i-- > 0;) {\n      if (list[i] === listener ||\n          (list[i].listener && list[i].listener === listener)) {\n        position = i;\n        break;\n      }\n    }\n\n    if (position < 0)\n      return this;\n\n    if (list.length === 1) {\n      list.length = 0;\n      delete this._events[type];\n    } else {\n      list.splice(position, 1);\n    }\n\n    if (this._events.removeListener)\n      this.emit('removeListener', type, listener);\n  }\n\n  return this;\n};\n\nEventEmitter.prototype.removeAllListeners = function(type) {\n  var key, listeners;\n\n  if (!this._events)\n    return this;\n\n  // not listening for removeListener, no need to emit\n  if (!this._events.removeListener) {\n    if (arguments.length === 0)\n      this._events = {};\n    else if (this._events[type])\n      delete this._events[type];\n    return this;\n  }\n\n  // emit removeListener for all listeners on all events\n  if (arguments.length === 0) {\n    for (key in this._events) {\n      if (key === 'removeListener') continue;\n      this.removeAllListeners(key);\n    }\n    this.removeAllListeners('removeListener');\n    this._events = {};\n    return this;\n  }\n\n  listeners = this._events[type];\n\n  if (isFunction(listeners)) {\n    this.removeListener(type, listeners);\n  } else {\n    // LIFO order\n    while (listeners.length)\n      this.removeListener(type, listeners[listeners.length - 1]);\n  }\n  delete this._events[type];\n\n  return this;\n};\n\nEventEmitter.prototype.listeners = function(type) {\n  var ret;\n  if (!this._events || !this._events[type])\n    ret = [];\n  else if (isFunction(this._events[type]))\n    ret = [this._events[type]];\n  else\n    ret = this._events[type].slice();\n  return ret;\n};\n\nEventEmitter.listenerCount = function(emitter, type) {\n  var ret;\n  if (!emitter._events || !emitter._events[type])\n    ret = 0;\n  else if (isFunction(emitter._events[type]))\n    ret = 1;\n  else\n    ret = emitter._events[type].length;\n  return ret;\n};\n\nfunction isFunction(arg) {\n  return typeof arg === 'function';\n}\n\nfunction isNumber(arg) {\n  return typeof arg === 'number';\n}\n\nfunction isObject(arg) {\n  return typeof arg === 'object' && arg !== null;\n}\n\nfunction isUndefined(arg) {\n  return arg === void 0;\n}\n\n},{}],12:[function(require,module,exports){\n(function (global){\n/**\n * @license\n * Lodash <https://lodash.com/>\n * Copyright OpenJS Foundation and other contributors <https://openjsf.org/>\n * Released under MIT license <https://lodash.com/license>\n * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>\n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n */\n;(function() {\n\n  /** Used as a safe reference for `undefined` in pre-ES5 environments. */\n  var undefined;\n\n  /** Used as the semantic version number. */\n  var VERSION = '4.17.20';\n\n  /** Used as the size to enable large array optimizations. */\n  var LARGE_ARRAY_SIZE = 200;\n\n  /** Error message constants. */\n  var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://npms.io/search?q=ponyfill.',\n      FUNC_ERROR_TEXT = 'Expected a function';\n\n  /** Used to stand-in for `undefined` hash values. */\n  var HASH_UNDEFINED = '__lodash_hash_undefined__';\n\n  /** Used as the maximum memoize cache size. */\n  var MAX_MEMOIZE_SIZE = 500;\n\n  /** Used as the internal argument placeholder. */\n  var PLACEHOLDER = '__lodash_placeholder__';\n\n  /** Used to compose bitmasks for cloning. */\n  var CLONE_DEEP_FLAG = 1,\n      CLONE_FLAT_FLAG = 2,\n      CLONE_SYMBOLS_FLAG = 4;\n\n  /** Used to compose bitmasks for value comparisons. */\n  var COMPARE_PARTIAL_FLAG = 1,\n      COMPARE_UNORDERED_FLAG = 2;\n\n  /** Used to compose bitmasks for function metadata. */\n  var WRAP_BIND_FLAG = 1,\n      WRAP_BIND_KEY_FLAG = 2,\n      WRAP_CURRY_BOUND_FLAG = 4,\n      WRAP_CURRY_FLAG = 8,\n      WRAP_CURRY_RIGHT_FLAG = 16,\n      WRAP_PARTIAL_FLAG = 32,\n      WRAP_PARTIAL_RIGHT_FLAG = 64,\n      WRAP_ARY_FLAG = 128,\n      WRAP_REARG_FLAG = 256,\n      WRAP_FLIP_FLAG = 512;\n\n  /** Used as default options for `_.truncate`. */\n  var DEFAULT_TRUNC_LENGTH = 30,\n      DEFAULT_TRUNC_OMISSION = '...';\n\n  /** Used to detect hot functions by number of calls within a span of milliseconds. */\n  var HOT_COUNT = 800,\n      HOT_SPAN = 16;\n\n  /** Used to indicate the type of lazy iteratees. */\n  var LAZY_FILTER_FLAG = 1,\n      LAZY_MAP_FLAG = 2,\n      LAZY_WHILE_FLAG = 3;\n\n  /** Used as references for various `Number` constants. */\n  var INFINITY = 1 / 0,\n      MAX_SAFE_INTEGER = 9007199254740991,\n      MAX_INTEGER = 1.7976931348623157e+308,\n      NAN = 0 / 0;\n\n  /** Used as references for the maximum length and index of an array. */\n  var MAX_ARRAY_LENGTH = 4294967295,\n      MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1,\n      HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1;\n\n  /** Used to associate wrap methods with their bit flags. */\n  var wrapFlags = [\n    ['ary', WRAP_ARY_FLAG],\n    ['bind', WRAP_BIND_FLAG],\n    ['bindKey', WRAP_BIND_KEY_FLAG],\n    ['curry', WRAP_CURRY_FLAG],\n    ['curryRight', WRAP_CURRY_RIGHT_FLAG],\n    ['flip', WRAP_FLIP_FLAG],\n    ['partial', WRAP_PARTIAL_FLAG],\n    ['partialRight', WRAP_PARTIAL_RIGHT_FLAG],\n    ['rearg', WRAP_REARG_FLAG]\n  ];\n\n  /** `Object#toString` result references. */\n  var argsTag = '[object Arguments]',\n      arrayTag = '[object Array]',\n      asyncTag = '[object AsyncFunction]',\n      boolTag = '[object Boolean]',\n      dateTag = '[object Date]',\n      domExcTag = '[object DOMException]',\n      errorTag = '[object Error]',\n      funcTag = '[object Function]',\n      genTag = '[object GeneratorFunction]',\n      mapTag = '[object Map]',\n      numberTag = '[object Number]',\n      nullTag = '[object Null]',\n      objectTag = '[object Object]',\n      promiseTag = '[object Promise]',\n      proxyTag = '[object Proxy]',\n      regexpTag = '[object RegExp]',\n      setTag = '[object Set]',\n      stringTag = '[object String]',\n      symbolTag = '[object Symbol]',\n      undefinedTag = '[object Undefined]',\n      weakMapTag = '[object WeakMap]',\n      weakSetTag = '[object WeakSet]';\n\n  var arrayBufferTag = '[object ArrayBuffer]',\n      dataViewTag = '[object DataView]',\n      float32Tag = '[object Float32Array]',\n      float64Tag = '[object Float64Array]',\n      int8Tag = '[object Int8Array]',\n      int16Tag = '[object Int16Array]',\n      int32Tag = '[object Int32Array]',\n      uint8Tag = '[object Uint8Array]',\n      uint8ClampedTag = '[object Uint8ClampedArray]',\n      uint16Tag = '[object Uint16Array]',\n      uint32Tag = '[object Uint32Array]';\n\n  /** Used to match empty string literals in compiled template source. */\n  var reEmptyStringLeading = /\\b__p \\+= '';/g,\n      reEmptyStringMiddle = /\\b(__p \\+=) '' \\+/g,\n      reEmptyStringTrailing = /(__e\\(.*?\\)|\\b__t\\)) \\+\\n'';/g;\n\n  /** Used to match HTML entities and HTML characters. */\n  var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g,\n      reUnescapedHtml = /[&<>\"']/g,\n      reHasEscapedHtml = RegExp(reEscapedHtml.source),\n      reHasUnescapedHtml = RegExp(reUnescapedHtml.source);\n\n  /** Used to match template delimiters. */\n  var reEscape = /<%-([\\s\\S]+?)%>/g,\n      reEvaluate = /<%([\\s\\S]+?)%>/g,\n      reInterpolate = /<%=([\\s\\S]+?)%>/g;\n\n  /** Used to match property names within property paths. */\n  var reIsDeepProp = /\\.|\\[(?:[^[\\]]*|([\"'])(?:(?!\\1)[^\\\\]|\\\\.)*?\\1)\\]/,\n      reIsPlainProp = /^\\w*$/,\n      rePropName = /[^.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))/g;\n\n  /**\n   * Used to match `RegExp`\n   * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).\n   */\n  var reRegExpChar = /[\\\\^$.*+?()[\\]{}|]/g,\n      reHasRegExpChar = RegExp(reRegExpChar.source);\n\n  /** Used to match leading and trailing whitespace. */\n  var reTrim = /^\\s+|\\s+$/g,\n      reTrimStart = /^\\s+/,\n      reTrimEnd = /\\s+$/;\n\n  /** Used to match wrap detail comments. */\n  var reWrapComment = /\\{(?:\\n\\/\\* \\[wrapped with .+\\] \\*\\/)?\\n?/,\n      reWrapDetails = /\\{\\n\\/\\* \\[wrapped with (.+)\\] \\*/,\n      reSplitDetails = /,? & /;\n\n  /** Used to match words composed of alphanumeric characters. */\n  var reAsciiWord = /[^\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7f]+/g;\n\n  /** Used to match backslashes in property paths. */\n  var reEscapeChar = /\\\\(\\\\)?/g;\n\n  /**\n   * Used to match\n   * [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components).\n   */\n  var reEsTemplate = /\\$\\{([^\\\\}]*(?:\\\\.[^\\\\}]*)*)\\}/g;\n\n  /** Used to match `RegExp` flags from their coerced string values. */\n  var reFlags = /\\w*$/;\n\n  /** Used to detect bad signed hexadecimal string values. */\n  var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;\n\n  /** Used to detect binary string values. */\n  var reIsBinary = /^0b[01]+$/i;\n\n  /** Used to detect host constructors (Safari). */\n  var reIsHostCtor = /^\\[object .+?Constructor\\]$/;\n\n  /** Used to detect octal string values. */\n  var reIsOctal = /^0o[0-7]+$/i;\n\n  /** Used to detect unsigned integer values. */\n  var reIsUint = /^(?:0|[1-9]\\d*)$/;\n\n  /** Used to match Latin Unicode letters (excluding mathematical operators). */\n  var reLatin = /[\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\xff\\u0100-\\u017f]/g;\n\n  /** Used to ensure capturing order of template delimiters. */\n  var reNoMatch = /($^)/;\n\n  /** Used to match unescaped characters in compiled string literals. */\n  var reUnescapedString = /['\\n\\r\\u2028\\u2029\\\\]/g;\n\n  /** Used to compose unicode character classes. */\n  var rsAstralRange = '\\\\ud800-\\\\udfff',\n      rsComboMarksRange = '\\\\u0300-\\\\u036f',\n      reComboHalfMarksRange = '\\\\ufe20-\\\\ufe2f',\n      rsComboSymbolsRange = '\\\\u20d0-\\\\u20ff',\n      rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange,\n      rsDingbatRange = '\\\\u2700-\\\\u27bf',\n      rsLowerRange = 'a-z\\\\xdf-\\\\xf6\\\\xf8-\\\\xff',\n      rsMathOpRange = '\\\\xac\\\\xb1\\\\xd7\\\\xf7',\n      rsNonCharRange = '\\\\x00-\\\\x2f\\\\x3a-\\\\x40\\\\x5b-\\\\x60\\\\x7b-\\\\xbf',\n      rsPunctuationRange = '\\\\u2000-\\\\u206f',\n      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',\n      rsUpperRange = 'A-Z\\\\xc0-\\\\xd6\\\\xd8-\\\\xde',\n      rsVarRange = '\\\\ufe0e\\\\ufe0f',\n      rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange;\n\n  /** Used to compose unicode capture groups. */\n  var rsApos = \"['\\u2019]\",\n      rsAstral = '[' + rsAstralRange + ']',\n      rsBreak = '[' + rsBreakRange + ']',\n      rsCombo = '[' + rsComboRange + ']',\n      rsDigits = '\\\\d+',\n      rsDingbat = '[' + rsDingbatRange + ']',\n      rsLower = '[' + rsLowerRange + ']',\n      rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']',\n      rsFitz = '\\\\ud83c[\\\\udffb-\\\\udfff]',\n      rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')',\n      rsNonAstral = '[^' + rsAstralRange + ']',\n      rsRegional = '(?:\\\\ud83c[\\\\udde6-\\\\uddff]){2}',\n      rsSurrPair = '[\\\\ud800-\\\\udbff][\\\\udc00-\\\\udfff]',\n      rsUpper = '[' + rsUpperRange + ']',\n      rsZWJ = '\\\\u200d';\n\n  /** Used to compose unicode regexes. */\n  var rsMiscLower = '(?:' + rsLower + '|' + rsMisc + ')',\n      rsMiscUpper = '(?:' + rsUpper + '|' + rsMisc + ')',\n      rsOptContrLower = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?',\n      rsOptContrUpper = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?',\n      reOptMod = rsModifier + '?',\n      rsOptVar = '[' + rsVarRange + ']?',\n      rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*',\n      rsOrdLower = '\\\\d*(?:1st|2nd|3rd|(?![123])\\\\dth)(?=\\\\b|[A-Z_])',\n      rsOrdUpper = '\\\\d*(?:1ST|2ND|3RD|(?![123])\\\\dTH)(?=\\\\b|[a-z_])',\n      rsSeq = rsOptVar + reOptMod + rsOptJoin,\n      rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq,\n      rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';\n\n  /** Used to match apostrophes. */\n  var reApos = RegExp(rsApos, 'g');\n\n  /**\n   * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and\n   * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols).\n   */\n  var reComboMark = RegExp(rsCombo, 'g');\n\n  /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */\n  var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');\n\n  /** Used to match complex or compound words. */\n  var reUnicodeWord = RegExp([\n    rsUpper + '?' + rsLower + '+' + rsOptContrLower + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')',\n    rsMiscUpper + '+' + rsOptContrUpper + '(?=' + [rsBreak, rsUpper + rsMiscLower, '$'].join('|') + ')',\n    rsUpper + '?' + rsMiscLower + '+' + rsOptContrLower,\n    rsUpper + '+' + rsOptContrUpper,\n    rsOrdUpper,\n    rsOrdLower,\n    rsDigits,\n    rsEmoji\n  ].join('|'), 'g');\n\n  /** 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/). */\n  var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange  + rsComboRange + rsVarRange + ']');\n\n  /** Used to detect strings that need a more robust regexp to match words. */\n  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 ]/;\n\n  /** Used to assign default `context` object properties. */\n  var contextProps = [\n    'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array',\n    'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object',\n    'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array',\n    'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap',\n    '_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout'\n  ];\n\n  /** Used to make template sourceURLs easier to identify. */\n  var templateCounter = -1;\n\n  /** Used to identify `toStringTag` values of typed arrays. */\n  var typedArrayTags = {};\n  typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =\n  typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =\n  typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =\n  typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =\n  typedArrayTags[uint32Tag] = true;\n  typedArrayTags[argsTag] = typedArrayTags[arrayTag] =\n  typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =\n  typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =\n  typedArrayTags[errorTag] = typedArrayTags[funcTag] =\n  typedArrayTags[mapTag] = typedArrayTags[numberTag] =\n  typedArrayTags[objectTag] = typedArrayTags[regexpTag] =\n  typedArrayTags[setTag] = typedArrayTags[stringTag] =\n  typedArrayTags[weakMapTag] = false;\n\n  /** Used to identify `toStringTag` values supported by `_.clone`. */\n  var cloneableTags = {};\n  cloneableTags[argsTag] = cloneableTags[arrayTag] =\n  cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =\n  cloneableTags[boolTag] = cloneableTags[dateTag] =\n  cloneableTags[float32Tag] = cloneableTags[float64Tag] =\n  cloneableTags[int8Tag] = cloneableTags[int16Tag] =\n  cloneableTags[int32Tag] = cloneableTags[mapTag] =\n  cloneableTags[numberTag] = cloneableTags[objectTag] =\n  cloneableTags[regexpTag] = cloneableTags[setTag] =\n  cloneableTags[stringTag] = cloneableTags[symbolTag] =\n  cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =\n  cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true;\n  cloneableTags[errorTag] = cloneableTags[funcTag] =\n  cloneableTags[weakMapTag] = false;\n\n  /** Used to map Latin Unicode letters to basic Latin letters. */\n  var deburredLetters = {\n    // Latin-1 Supplement block.\n    '\\xc0': 'A',  '\\xc1': 'A', '\\xc2': 'A', '\\xc3': 'A', '\\xc4': 'A', '\\xc5': 'A',\n    '\\xe0': 'a',  '\\xe1': 'a', '\\xe2': 'a', '\\xe3': 'a', '\\xe4': 'a', '\\xe5': 'a',\n    '\\xc7': 'C',  '\\xe7': 'c',\n    '\\xd0': 'D',  '\\xf0': 'd',\n    '\\xc8': 'E',  '\\xc9': 'E', '\\xca': 'E', '\\xcb': 'E',\n    '\\xe8': 'e',  '\\xe9': 'e', '\\xea': 'e', '\\xeb': 'e',\n    '\\xcc': 'I',  '\\xcd': 'I', '\\xce': 'I', '\\xcf': 'I',\n    '\\xec': 'i',  '\\xed': 'i', '\\xee': 'i', '\\xef': 'i',\n    '\\xd1': 'N',  '\\xf1': 'n',\n    '\\xd2': 'O',  '\\xd3': 'O', '\\xd4': 'O', '\\xd5': 'O', '\\xd6': 'O', '\\xd8': 'O',\n    '\\xf2': 'o',  '\\xf3': 'o', '\\xf4': 'o', '\\xf5': 'o', '\\xf6': 'o', '\\xf8': 'o',\n    '\\xd9': 'U',  '\\xda': 'U', '\\xdb': 'U', '\\xdc': 'U',\n    '\\xf9': 'u',  '\\xfa': 'u', '\\xfb': 'u', '\\xfc': 'u',\n    '\\xdd': 'Y',  '\\xfd': 'y', '\\xff': 'y',\n    '\\xc6': 'Ae', '\\xe6': 'ae',\n    '\\xde': 'Th', '\\xfe': 'th',\n    '\\xdf': 'ss',\n    // Latin Extended-A block.\n    '\\u0100': 'A',  '\\u0102': 'A', '\\u0104': 'A',\n    '\\u0101': 'a',  '\\u0103': 'a', '\\u0105': 'a',\n    '\\u0106': 'C',  '\\u0108': 'C', '\\u010a': 'C', '\\u010c': 'C',\n    '\\u0107': 'c',  '\\u0109': 'c', '\\u010b': 'c', '\\u010d': 'c',\n    '\\u010e': 'D',  '\\u0110': 'D', '\\u010f': 'd', '\\u0111': 'd',\n    '\\u0112': 'E',  '\\u0114': 'E', '\\u0116': 'E', '\\u0118': 'E', '\\u011a': 'E',\n    '\\u0113': 'e',  '\\u0115': 'e', '\\u0117': 'e', '\\u0119': 'e', '\\u011b': 'e',\n    '\\u011c': 'G',  '\\u011e': 'G', '\\u0120': 'G', '\\u0122': 'G',\n    '\\u011d': 'g',  '\\u011f': 'g', '\\u0121': 'g', '\\u0123': 'g',\n    '\\u0124': 'H',  '\\u0126': 'H', '\\u0125': 'h', '\\u0127': 'h',\n    '\\u0128': 'I',  '\\u012a': 'I', '\\u012c': 'I', '\\u012e': 'I', '\\u0130': 'I',\n    '\\u0129': 'i',  '\\u012b': 'i', '\\u012d': 'i', '\\u012f': 'i', '\\u0131': 'i',\n    '\\u0134': 'J',  '\\u0135': 'j',\n    '\\u0136': 'K',  '\\u0137': 'k', '\\u0138': 'k',\n    '\\u0139': 'L',  '\\u013b': 'L', '\\u013d': 'L', '\\u013f': 'L', '\\u0141': 'L',\n    '\\u013a': 'l',  '\\u013c': 'l', '\\u013e': 'l', '\\u0140': 'l', '\\u0142': 'l',\n    '\\u0143': 'N',  '\\u0145': 'N', '\\u0147': 'N', '\\u014a': 'N',\n    '\\u0144': 'n',  '\\u0146': 'n', '\\u0148': 'n', '\\u014b': 'n',\n    '\\u014c': 'O',  '\\u014e': 'O', '\\u0150': 'O',\n    '\\u014d': 'o',  '\\u014f': 'o', '\\u0151': 'o',\n    '\\u0154': 'R',  '\\u0156': 'R', '\\u0158': 'R',\n    '\\u0155': 'r',  '\\u0157': 'r', '\\u0159': 'r',\n    '\\u015a': 'S',  '\\u015c': 'S', '\\u015e': 'S', '\\u0160': 'S',\n    '\\u015b': 's',  '\\u015d': 's', '\\u015f': 's', '\\u0161': 's',\n    '\\u0162': 'T',  '\\u0164': 'T', '\\u0166': 'T',\n    '\\u0163': 't',  '\\u0165': 't', '\\u0167': 't',\n    '\\u0168': 'U',  '\\u016a': 'U', '\\u016c': 'U', '\\u016e': 'U', '\\u0170': 'U', '\\u0172': 'U',\n    '\\u0169': 'u',  '\\u016b': 'u', '\\u016d': 'u', '\\u016f': 'u', '\\u0171': 'u', '\\u0173': 'u',\n    '\\u0174': 'W',  '\\u0175': 'w',\n    '\\u0176': 'Y',  '\\u0177': 'y', '\\u0178': 'Y',\n    '\\u0179': 'Z',  '\\u017b': 'Z', '\\u017d': 'Z',\n    '\\u017a': 'z',  '\\u017c': 'z', '\\u017e': 'z',\n    '\\u0132': 'IJ', '\\u0133': 'ij',\n    '\\u0152': 'Oe', '\\u0153': 'oe',\n    '\\u0149': \"'n\", '\\u017f': 's'\n  };\n\n  /** Used to map characters to HTML entities. */\n  var htmlEscapes = {\n    '&': '&amp;',\n    '<': '&lt;',\n    '>': '&gt;',\n    '\"': '&quot;',\n    \"'\": '&#39;'\n  };\n\n  /** Used to map HTML entities to characters. */\n  var htmlUnescapes = {\n    '&amp;': '&',\n    '&lt;': '<',\n    '&gt;': '>',\n    '&quot;': '\"',\n    '&#39;': \"'\"\n  };\n\n  /** Used to escape characters for inclusion in compiled string literals. */\n  var stringEscapes = {\n    '\\\\': '\\\\',\n    \"'\": \"'\",\n    '\\n': 'n',\n    '\\r': 'r',\n    '\\u2028': 'u2028',\n    '\\u2029': 'u2029'\n  };\n\n  /** Built-in method references without a dependency on `root`. */\n  var freeParseFloat = parseFloat,\n      freeParseInt = parseInt;\n\n  /** Detect free variable `global` from Node.js. */\n  var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;\n\n  /** Detect free variable `self`. */\n  var freeSelf = typeof self == 'object' && self && self.Object === Object && self;\n\n  /** Used as a reference to the global object. */\n  var root = freeGlobal || freeSelf || Function('return this')();\n\n  /** Detect free variable `exports`. */\n  var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;\n\n  /** Detect free variable `module`. */\n  var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;\n\n  /** Detect the popular CommonJS extension `module.exports`. */\n  var moduleExports = freeModule && freeModule.exports === freeExports;\n\n  /** Detect free variable `process` from Node.js. */\n  var freeProcess = moduleExports && freeGlobal.process;\n\n  /** Used to access faster Node.js helpers. */\n  var nodeUtil = (function() {\n    try {\n      // Use `util.types` for Node.js 10+.\n      var types = freeModule && freeModule.require && freeModule.require('util').types;\n\n      if (types) {\n        return types;\n      }\n\n      // Legacy `process.binding('util')` for Node.js < 10.\n      return freeProcess && freeProcess.binding && freeProcess.binding('util');\n    } catch (e) {}\n  }());\n\n  /* Node.js helper references. */\n  var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer,\n      nodeIsDate = nodeUtil && nodeUtil.isDate,\n      nodeIsMap = nodeUtil && nodeUtil.isMap,\n      nodeIsRegExp = nodeUtil && nodeUtil.isRegExp,\n      nodeIsSet = nodeUtil && nodeUtil.isSet,\n      nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;\n\n  /*--------------------------------------------------------------------------*/\n\n  /**\n   * A faster alternative to `Function#apply`, this function invokes `func`\n   * with the `this` binding of `thisArg` and the arguments of `args`.\n   *\n   * @private\n   * @param {Function} func The function to invoke.\n   * @param {*} thisArg The `this` binding of `func`.\n   * @param {Array} args The arguments to invoke `func` with.\n   * @returns {*} Returns the result of `func`.\n   */\n  function apply(func, thisArg, args) {\n    switch (args.length) {\n      case 0: return func.call(thisArg);\n      case 1: return func.call(thisArg, args[0]);\n      case 2: return func.call(thisArg, args[0], args[1]);\n      case 3: return func.call(thisArg, args[0], args[1], args[2]);\n    }\n    return func.apply(thisArg, args);\n  }\n\n  /**\n   * A specialized version of `baseAggregator` for arrays.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} setter The function to set `accumulator` values.\n   * @param {Function} iteratee The iteratee to transform keys.\n   * @param {Object} accumulator The initial aggregated object.\n   * @returns {Function} Returns `accumulator`.\n   */\n  function arrayAggregator(array, setter, iteratee, accumulator) {\n    var index = -1,\n        length = array == null ? 0 : array.length;\n\n    while (++index < length) {\n      var value = array[index];\n      setter(accumulator, value, iteratee(value), array);\n    }\n    return accumulator;\n  }\n\n  /**\n   * A specialized version of `_.forEach` for arrays without support for\n   * iteratee shorthands.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @returns {Array} Returns `array`.\n   */\n  function arrayEach(array, iteratee) {\n    var index = -1,\n        length = array == null ? 0 : array.length;\n\n    while (++index < length) {\n      if (iteratee(array[index], index, array) === false) {\n        break;\n      }\n    }\n    return array;\n  }\n\n  /**\n   * A specialized version of `_.forEachRight` for arrays without support for\n   * iteratee shorthands.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @returns {Array} Returns `array`.\n   */\n  function arrayEachRight(array, iteratee) {\n    var length = array == null ? 0 : array.length;\n\n    while (length--) {\n      if (iteratee(array[length], length, array) === false) {\n        break;\n      }\n    }\n    return array;\n  }\n\n  /**\n   * A specialized version of `_.every` for arrays without support for\n   * iteratee shorthands.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} predicate The function invoked per iteration.\n   * @returns {boolean} Returns `true` if all elements pass the predicate check,\n   *  else `false`.\n   */\n  function arrayEvery(array, predicate) {\n    var index = -1,\n        length = array == null ? 0 : array.length;\n\n    while (++index < length) {\n      if (!predicate(array[index], index, array)) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * A specialized version of `_.filter` for arrays without support for\n   * iteratee shorthands.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} predicate The function invoked per iteration.\n   * @returns {Array} Returns the new filtered array.\n   */\n  function arrayFilter(array, predicate) {\n    var index = -1,\n        length = array == null ? 0 : array.length,\n        resIndex = 0,\n        result = [];\n\n    while (++index < length) {\n      var value = array[index];\n      if (predicate(value, index, array)) {\n        result[resIndex++] = value;\n      }\n    }\n    return result;\n  }\n\n  /**\n   * A specialized version of `_.includes` for arrays without support for\n   * specifying an index to search from.\n   *\n   * @private\n   * @param {Array} [array] The array to inspect.\n   * @param {*} target The value to search for.\n   * @returns {boolean} Returns `true` if `target` is found, else `false`.\n   */\n  function arrayIncludes(array, value) {\n    var length = array == null ? 0 : array.length;\n    return !!length && baseIndexOf(array, value, 0) > -1;\n  }\n\n  /**\n   * This function is like `arrayIncludes` except that it accepts a comparator.\n   *\n   * @private\n   * @param {Array} [array] The array to inspect.\n   * @param {*} target The value to search for.\n   * @param {Function} comparator The comparator invoked per element.\n   * @returns {boolean} Returns `true` if `target` is found, else `false`.\n   */\n  function arrayIncludesWith(array, value, comparator) {\n    var index = -1,\n        length = array == null ? 0 : array.length;\n\n    while (++index < length) {\n      if (comparator(value, array[index])) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * A specialized version of `_.map` for arrays without support for iteratee\n   * shorthands.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @returns {Array} Returns the new mapped array.\n   */\n  function arrayMap(array, iteratee) {\n    var index = -1,\n        length = array == null ? 0 : array.length,\n        result = Array(length);\n\n    while (++index < length) {\n      result[index] = iteratee(array[index], index, array);\n    }\n    return result;\n  }\n\n  /**\n   * Appends the elements of `values` to `array`.\n   *\n   * @private\n   * @param {Array} array The array to modify.\n   * @param {Array} values The values to append.\n   * @returns {Array} Returns `array`.\n   */\n  function arrayPush(array, values) {\n    var index = -1,\n        length = values.length,\n        offset = array.length;\n\n    while (++index < length) {\n      array[offset + index] = values[index];\n    }\n    return array;\n  }\n\n  /**\n   * A specialized version of `_.reduce` for arrays without support for\n   * iteratee shorthands.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @param {*} [accumulator] The initial value.\n   * @param {boolean} [initAccum] Specify using the first element of `array` as\n   *  the initial value.\n   * @returns {*} Returns the accumulated value.\n   */\n  function arrayReduce(array, iteratee, accumulator, initAccum) {\n    var index = -1,\n        length = array == null ? 0 : array.length;\n\n    if (initAccum && length) {\n      accumulator = array[++index];\n    }\n    while (++index < length) {\n      accumulator = iteratee(accumulator, array[index], index, array);\n    }\n    return accumulator;\n  }\n\n  /**\n   * A specialized version of `_.reduceRight` for arrays without support for\n   * iteratee shorthands.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @param {*} [accumulator] The initial value.\n   * @param {boolean} [initAccum] Specify using the last element of `array` as\n   *  the initial value.\n   * @returns {*} Returns the accumulated value.\n   */\n  function arrayReduceRight(array, iteratee, accumulator, initAccum) {\n    var length = array == null ? 0 : array.length;\n    if (initAccum && length) {\n      accumulator = array[--length];\n    }\n    while (length--) {\n      accumulator = iteratee(accumulator, array[length], length, array);\n    }\n    return accumulator;\n  }\n\n  /**\n   * A specialized version of `_.some` for arrays without support for iteratee\n   * shorthands.\n   *\n   * @private\n   * @param {Array} [array] The array to iterate over.\n   * @param {Function} predicate The function invoked per iteration.\n   * @returns {boolean} Returns `true` if any element passes the predicate check,\n   *  else `false`.\n   */\n  function arraySome(array, predicate) {\n    var index = -1,\n        length = array == null ? 0 : array.length;\n\n    while (++index < length) {\n      if (predicate(array[index], index, array)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Gets the size of an ASCII `string`.\n   *\n   * @private\n   * @param {string} string The string inspect.\n   * @returns {number} Returns the string size.\n   */\n  var asciiSize = baseProperty('length');\n\n  /**\n   * Converts an ASCII `string` to an array.\n   *\n   * @private\n   * @param {string} string The string to convert.\n   * @returns {Array} Returns the converted array.\n   */\n  function asciiToArray(string) {\n    return string.split('');\n  }\n\n  /**\n   * Splits an ASCII `string` into an array of its words.\n   *\n   * @private\n   * @param {string} The string to inspect.\n   * @returns {Array} Returns the words of `string`.\n   */\n  function asciiWords(string) {\n    return string.match(reAsciiWord) || [];\n  }\n\n  /**\n   * The base implementation of methods like `_.findKey` and `_.findLastKey`,\n   * without support for iteratee shorthands, which iterates over `collection`\n   * using `eachFunc`.\n   *\n   * @private\n   * @param {Array|Object} collection The collection to inspect.\n   * @param {Function} predicate The function invoked per iteration.\n   * @param {Function} eachFunc The function to iterate over `collection`.\n   * @returns {*} Returns the found element or its key, else `undefined`.\n   */\n  function baseFindKey(collection, predicate, eachFunc) {\n    var result;\n    eachFunc(collection, function(value, key, collection) {\n      if (predicate(value, key, collection)) {\n        result = key;\n        return false;\n      }\n    });\n    return result;\n  }\n\n  /**\n   * The base implementation of `_.findIndex` and `_.findLastIndex` without\n   * support for iteratee shorthands.\n   *\n   * @private\n   * @param {Array} array The array to inspect.\n   * @param {Function} predicate The function invoked per iteration.\n   * @param {number} fromIndex The index to search from.\n   * @param {boolean} [fromRight] Specify iterating from right to left.\n   * @returns {number} Returns the index of the matched value, else `-1`.\n   */\n  function baseFindIndex(array, predicate, fromIndex, fromRight) {\n    var length = array.length,\n        index = fromIndex + (fromRight ? 1 : -1);\n\n    while ((fromRight ? index-- : ++index < length)) {\n      if (predicate(array[index], index, array)) {\n        return index;\n      }\n    }\n    return -1;\n  }\n\n  /**\n   * The base implementation of `_.indexOf` without `fromIndex` bounds checks.\n   *\n   * @private\n   * @param {Array} array The array to inspect.\n   * @param {*} value The value to search for.\n   * @param {number} fromIndex The index to search from.\n   * @returns {number} Returns the index of the matched value, else `-1`.\n   */\n  function baseIndexOf(array, value, fromIndex) {\n    return value === value\n      ? strictIndexOf(array, value, fromIndex)\n      : baseFindIndex(array, baseIsNaN, fromIndex);\n  }\n\n  /**\n   * This function is like `baseIndexOf` except that it accepts a comparator.\n   *\n   * @private\n   * @param {Array} array The array to inspect.\n   * @param {*} value The value to search for.\n   * @param {number} fromIndex The index to search from.\n   * @param {Function} comparator The comparator invoked per element.\n   * @returns {number} Returns the index of the matched value, else `-1`.\n   */\n  function baseIndexOfWith(array, value, fromIndex, comparator) {\n    var index = fromIndex - 1,\n        length = array.length;\n\n    while (++index < length) {\n      if (comparator(array[index], value)) {\n        return index;\n      }\n    }\n    return -1;\n  }\n\n  /**\n   * The base implementation of `_.isNaN` without support for number objects.\n   *\n   * @private\n   * @param {*} value The value to check.\n   * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.\n   */\n  function baseIsNaN(value) {\n    return value !== value;\n  }\n\n  /**\n   * The base implementation of `_.mean` and `_.meanBy` without support for\n   * iteratee shorthands.\n   *\n   * @private\n   * @param {Array} array The array to iterate over.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @returns {number} Returns the mean.\n   */\n  function baseMean(array, iteratee) {\n    var length = array == null ? 0 : array.length;\n    return length ? (baseSum(array, iteratee) / length) : NAN;\n  }\n\n  /**\n   * The base implementation of `_.property` without support for deep paths.\n   *\n   * @private\n   * @param {string} key The key of the property to get.\n   * @returns {Function} Returns the new accessor function.\n   */\n  function baseProperty(key) {\n    return function(object) {\n      return object == null ? undefined : object[key];\n    };\n  }\n\n  /**\n   * The base implementation of `_.propertyOf` without support for deep paths.\n   *\n   * @private\n   * @param {Object} object The object to query.\n   * @returns {Function} Returns the new accessor function.\n   */\n  function basePropertyOf(object) {\n    return function(key) {\n      return object == null ? undefined : object[key];\n    };\n  }\n\n  /**\n   * The base implementation of `_.reduce` and `_.reduceRight`, without support\n   * for iteratee shorthands, which iterates over `collection` using `eachFunc`.\n   *\n   * @private\n   * @param {Array|Object} collection The collection to iterate over.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @param {*} accumulator The initial value.\n   * @param {boolean} initAccum Specify using the first or last element of\n   *  `collection` as the initial value.\n   * @param {Function} eachFunc The function to iterate over `collection`.\n   * @returns {*} Returns the accumulated value.\n   */\n  function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) {\n    eachFunc(collection, function(value, index, collection) {\n      accumulator = initAccum\n        ? (initAccum = false, value)\n        : iteratee(accumulator, value, index, collection);\n    });\n    return accumulator;\n  }\n\n  /**\n   * The base implementation of `_.sortBy` which uses `comparer` to define the\n   * sort order of `array` and replaces criteria objects with their corresponding\n   * values.\n   *\n   * @private\n   * @param {Array} array The array to sort.\n   * @param {Function} comparer The function to define sort order.\n   * @returns {Array} Returns `array`.\n   */\n  function baseSortBy(array, comparer) {\n    var length = array.length;\n\n    array.sort(comparer);\n    while (length--) {\n      array[length] = array[length].value;\n    }\n    return array;\n  }\n\n  /**\n   * The base implementation of `_.sum` and `_.sumBy` without support for\n   * iteratee shorthands.\n   *\n   * @private\n   * @param {Array} array The array to iterate over.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @returns {number} Returns the sum.\n   */\n  function baseSum(array, iteratee) {\n    var result,\n        index = -1,\n        length = array.length;\n\n    while (++index < length) {\n      var current = iteratee(array[index]);\n      if (current !== undefined) {\n        result = result === undefined ? current : (result + current);\n      }\n    }\n    return result;\n  }\n\n  /**\n   * The base implementation of `_.times` without support for iteratee shorthands\n   * or max array length checks.\n   *\n   * @private\n   * @param {number} n The number of times to invoke `iteratee`.\n   * @param {Function} iteratee The function invoked per iteration.\n   * @returns {Array} Returns the array of results.\n   */\n  function baseTimes(n, iteratee) {\n    var index = -1,\n        result = Array(n);\n\n    while (++index < n) {\n      result[index] = iteratee(index);\n    }\n    return result;\n  }\n\n  /**\n   * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array\n   * of key-value pairs for `object` corresponding to the property names of `props`.\n   *\n   * @private\n   * @param {Object} object The object to query.\n   * @param {Array} props The property names to get values for.\n   * @returns {Object} Returns the key-value pairs.\n   */\n  function baseToPairs(object, props) {\n    return arrayMap(props, function(key) {\n      return [key, object[key]];\n    });\n  }\n\n  /**\n   * The base implementation of `_.unary` without support for storing metadata.\n   *\n   * @private\n   * @param {Function} func The function to cap arguments for.\n   * @returns {Function} Returns the new capped function.\n   */\n  function baseUnary(func) {\n    return function(value) {\n      return func(value);\n    };\n  }\n\n  /**\n   * The base implementation of `_.values` and `_.valuesIn` which creates an\n   * array of `object` property values corresponding to the property names\n   * of `props`.\n   *\n   * @private\n   * @param {Object} object The object to query.\n   * @param {Array} props The property names to get values for.\n   * @returns {Object} Returns the array of property values.\n   */\n  function baseValues(object, props) {\n    return arrayMap(props, function(key) {\n      return object[key];\n    });\n  }\n\n  /**\n   * Checks if a `cache` value for `key` exists.\n   *\n   * @private\n   * @param {Object} cache The cache to query.\n   * @param {string} key The key of the entry to check.\n   * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n   */\n  function cacheHas(cache, key) {\n    return cache.has(key);\n  }\n\n  /**\n   * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol\n   * that is not found in the character symbols.\n   *\n   * @private\n   * @param {Array} strSymbols The string symbols to inspect.\n   * @param {Array} chrSymbols The character symbols to find.\n   * @returns {number} Returns the index of the first unmatched string symbol.\n   */\n  function charsStartIndex(strSymbols, chrSymbols) {\n    var index = -1,\n        length = strSymbols.length;\n\n    while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}\n    return index;\n  }\n\n  /**\n   * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol\n   * that is not found in the character symbols.\n   *\n   * @private\n   * @param {Array} strSymbols The string symbols to inspect.\n   * @param {Array} chrSymbols The character symbols to find.\n   * @returns {number} Returns the index of the last unmatched string symbol.\n   */\n  function charsEndIndex(strSymbols, chrSymbols) {\n    var index = strSymbols.length;\n\n    while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {}\n    return index;\n  }\n\n  /**\n   * Gets the number of `placeholder` occurrences in `array`.\n   *\n   * @private\n   * @param {Array} array The array to inspect.\n   * @param {*} placeholder The placeholder to search for.\n   * @returns {number} Returns the placeholder count.\n   */\n  function countHolders(array, placeholder) {\n    var length = array.length,\n        result = 0;\n\n    while (length--) {\n      if (array[length] === placeholder) {\n        ++result;\n      }\n    }\n    return result;\n  }\n\n  /**\n   * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A\n   * letters to basic Latin letters.\n   *\n   * @private\n   * @param {string} letter The matched letter to deburr.\n   * @returns {string} Returns the deburred letter.\n   */\n  var deburrLetter = basePropertyOf(deburredLetters);\n\n  /**\n   * Used by `_.escape` to convert characters to HTML entities.\n   *\n   * @private\n   * @param {string} chr The matched character to escape.\n   * @returns {string} Returns the escaped character.\n   */\n  var escapeHtmlChar = basePropertyOf(htmlEscapes);\n\n  /**\n   * Used by `_.template` to escape characters for inclusion in compiled string literals.\n   *\n   * @private\n   * @param {string} chr The matched character to escape.\n   * @returns {string} Returns the escaped character.\n   */\n  function escapeStringChar(chr) {\n    return '\\\\' + stringEscapes[chr];\n  }\n\n  /**\n   * Gets the value at `key` of `object`.\n   *\n   * @private\n   * @param {Object} [object] The object to query.\n   * @param {string} key The key of the property to get.\n   * @returns {*} Returns the property value.\n   */\n  function getValue(object, key) {\n    return object == null ? undefined : object[key];\n  }\n\n  /**\n   * Checks if `string` contains Unicode symbols.\n   *\n   * @private\n   * @param {string} string The string to inspect.\n   * @returns {boolean} Returns `true` if a symbol is found, else `false`.\n   */\n  function hasUnicode(string) {\n    return reHasUnicode.test(string);\n  }\n\n  /**\n   * Checks if `string` contains a word composed of Unicode symbols.\n   *\n   * @private\n   * @param {string} string The string to inspect.\n   * @returns {boolean} Returns `true` if a word is found, else `false`.\n   */\n  function hasUnicodeWord(string) {\n    return reHasUnicodeWord.test(string);\n  }\n\n  /**\n   * Converts `iterator` to an array.\n   *\n   * @private\n   * @param {Object} iterator The iterator to convert.\n   * @returns {Array} Returns the converted array.\n   */\n  function iteratorToArray(iterator) {\n    var data,\n        result = [];\n\n    while (!(data = iterator.next()).done) {\n      result.push(data.value);\n    }\n    return result;\n  }\n\n  /**\n   * Converts `map` to its key-value pairs.\n   *\n   * @private\n   * @param {Object} map The map to convert.\n   * @returns {Array} Returns the key-value pairs.\n   */\n  function mapToArray(map) {\n    var index = -1,\n        result = Array(map.size);\n\n    map.forEach(function(value, key) {\n      result[++index] = [key, value];\n    });\n    return result;\n  }\n\n  /**\n   * Creates a unary function that invokes `func` with its argument transformed.\n   *\n   * @private\n   * @param {Function} func The function to wrap.\n   * @param {Function} transform The argument transform.\n   * @returns {Function} Returns the new function.\n   */\n  function overArg(func, transform) {\n    return function(arg) {\n      return func(transform(arg));\n    };\n  }\n\n  /**\n   * Replaces all `placeholder` elements in `array` with an internal placeholder\n   * and returns an array of their indexes.\n   *\n   * @private\n   * @param {Array} array The array to modify.\n   * @param {*} placeholder The placeholder to replace.\n   * @returns {Array} Returns the new array of placeholder indexes.\n   */\n  function replaceHolders(array, placeholder) {\n    var index = -1,\n        length = array.length,\n        resIndex = 0,\n        result = [];\n\n    while (++index < length) {\n      var value = array[index];\n      if (value === placeholder || value === PLACEHOLDER) {\n        array[index] = PLACEHOLDER;\n        result[resIndex++] = index;\n      }\n    }\n    return result;\n  }\n\n  /**\n   * Converts `set` to an array of its values.\n   *\n   * @private\n   * @param {Object} set The set to convert.\n   * @returns {Array} Returns the values.\n   */\n  function setToArray(set) {\n    var index = -1,\n        result = Array(set.size);\n\n    set.forEach(function(value) {\n      result[++index] = value;\n    });\n    return result;\n  }\n\n  /**\n   * Converts `set` to its value-value pairs.\n   *\n   * @private\n   * @param {Object} set The set to convert.\n   * @returns {Array} Returns the value-value pairs.\n   */\n  function setToPairs(set) {\n    var index = -1,\n        result = Array(set.size);\n\n    set.forEach(function(value) {\n      result[++index] = [value, value];\n    });\n    return result;\n  }\n\n  /**\n   * A specialized version of `_.indexOf` which performs strict equality\n   * comparisons of values, i.e. `===`.\n   *\n   * @private\n   * @param {Array} array The array to inspect.\n   * @param {*} value The value to search for.\n   * @param {number} fromIndex The index to search from.\n   * @returns {number} Returns the index of the matched value, else `-1`.\n   */\n  function strictIndexOf(array, value, fromIndex) {\n    var index = fromIndex - 1,\n        length = array.length;\n\n    while (++index < length) {\n      if (array[index] === value) {\n        return index;\n      }\n    }\n    return -1;\n  }\n\n  /**\n   * A specialized version of `_.lastIndexOf` which performs strict equality\n   * comparisons of values, i.e. `===`.\n   *\n   * @private\n   * @param {Array} array The array to inspect.\n   * @param {*} value The value to search for.\n   * @param {number} fromIndex The index to search from.\n   * @returns {number} Returns the index of the matched value, else `-1`.\n   */\n  function strictLastIndexOf(array, value, fromIndex) {\n    var index = fromIndex + 1;\n    while (index--) {\n      if (array[index] === value) {\n        return index;\n      }\n    }\n    return index;\n  }\n\n  /**\n   * Gets the number of symbols in `string`.\n   *\n   * @private\n   * @param {string} string The string to inspect.\n   * @returns {number} Returns the string size.\n   */\n  function stringSize(string) {\n    return hasUnicode(string)\n      ? unicodeSize(string)\n      : asciiSize(string);\n  }\n\n  /**\n   * Converts `string` to an array.\n   *\n   * @private\n   * @param {string} string The string to convert.\n   * @returns {Array} Returns the converted array.\n   */\n  function stringToArray(string) {\n    return hasUnicode(string)\n      ? unicodeToArray(string)\n      : asciiToArray(string);\n  }\n\n  /**\n   * Used by `_.unescape` to convert HTML entities to characters.\n   *\n   * @private\n   * @param {string} chr The matched character to unescape.\n   * @returns {string} Returns the unescaped character.\n   */\n  var unescapeHtmlChar = basePropertyOf(htmlUnescapes);\n\n  /**\n   * Gets the size of a Unicode `string`.\n   *\n   * @private\n   * @param {string} string The string inspect.\n   * @returns {number} Returns the string size.\n   */\n  function unicodeSize(string) {\n    var result = reUnicode.lastIndex = 0;\n    while (reUnicode.test(string)) {\n      ++result;\n    }\n    return result;\n  }\n\n  /**\n   * Converts a Unicode `string` to an array.\n   *\n   * @private\n   * @param {string} string The string to convert.\n   * @returns {Array} Returns the converted array.\n   */\n  function unicodeToArray(string) {\n    return string.match(reUnicode) || [];\n  }\n\n  /**\n   * Splits a Unicode `string` into an array of its words.\n   *\n   * @private\n   * @param {string} The string to inspect.\n   * @returns {Array} Returns the words of `string`.\n   */\n  function unicodeWords(string) {\n    return string.match(reUnicodeWord) || [];\n  }\n\n  /*--------------------------------------------------------------------------*/\n\n  /**\n   * Create a new pristine `lodash` function using the `context` object.\n   *\n   * @static\n   * @memberOf _\n   * @since 1.1.0\n   * @category Util\n   * @param {Object} [context=root] The context object.\n   * @returns {Function} Returns a new `lodash` function.\n   * @example\n   *\n   * _.mixin({ 'foo': _.constant('foo') });\n   *\n   * var lodash = _.runInContext();\n   * lodash.mixin({ 'bar': lodash.constant('bar') });\n   *\n   * _.isFunction(_.foo);\n   * // => true\n   * _.isFunction(_.bar);\n   * // => false\n   *\n   * lodash.isFunction(lodash.foo);\n   * // => false\n   * lodash.isFunction(lodash.bar);\n   * // => true\n   *\n   * // Create a suped-up `defer` in Node.js.\n   * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer;\n   */\n  var runInContext = (function runInContext(context) {\n    context = context == null ? root : _.defaults(root.Object(), context, _.pick(root, contextProps));\n\n    /** Built-in constructor references. */\n    var Array = context.Array,\n        Date = context.Date,\n        Error = context.Error,\n        Function = context.Function,\n        Math = context.Math,\n        Object = context.Object,\n        RegExp = context.RegExp,\n        String = context.String,\n        TypeError = context.TypeError;\n\n    /** Used for built-in method references. */\n    var arrayProto = Array.prototype,\n        funcProto = Function.prototype,\n        objectProto = Object.prototype;\n\n    /** Used to detect overreaching core-js shims. */\n    var coreJsData = context['__core-js_shared__'];\n\n    /** Used to resolve the decompiled source of functions. */\n    var funcToString = funcProto.toString;\n\n    /** Used to check objects for own properties. */\n    var hasOwnProperty = objectProto.hasOwnProperty;\n\n    /** Used to generate unique IDs. */\n    var idCounter = 0;\n\n    /** Used to detect methods masquerading as native. */\n    var maskSrcKey = (function() {\n      var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');\n      return uid ? ('Symbol(src)_1.' + uid) : '';\n    }());\n\n    /**\n     * Used to resolve the\n     * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)\n     * of values.\n     */\n    var nativeObjectToString = objectProto.toString;\n\n    /** Used to infer the `Object` constructor. */\n    var objectCtorString = funcToString.call(Object);\n\n    /** Used to restore the original `_` reference in `_.noConflict`. */\n    var oldDash = root._;\n\n    /** Used to detect if a method is native. */\n    var reIsNative = RegExp('^' +\n      funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\\\$&')\n      .replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g, '$1.*?') + '$'\n    );\n\n    /** Built-in value references. */\n    var Buffer = moduleExports ? context.Buffer : undefined,\n        Symbol = context.Symbol,\n        Uint8Array = context.Uint8Array,\n        allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined,\n        getPrototype = overArg(Object.getPrototypeOf, Object),\n        objectCreate = Object.create,\n        propertyIsEnumerable = objectProto.propertyIsEnumerable,\n        splice = arrayProto.splice,\n        spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined,\n        symIterator = Symbol ? Symbol.iterator : undefined,\n        symToStringTag = Symbol ? Symbol.toStringTag : undefined;\n\n    var defineProperty = (function() {\n      try {\n        var func = getNative(Object, 'defineProperty');\n        func({}, '', {});\n        return func;\n      } catch (e) {}\n    }());\n\n    /** Mocked built-ins. */\n    var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout,\n        ctxNow = Date && Date.now !== root.Date.now && Date.now,\n        ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout;\n\n    /* Built-in method references for those with the same name as other `lodash` methods. */\n    var nativeCeil = Math.ceil,\n        nativeFloor = Math.floor,\n        nativeGetSymbols = Object.getOwnPropertySymbols,\n        nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined,\n        nativeIsFinite = context.isFinite,\n        nativeJoin = arrayProto.join,\n        nativeKeys = overArg(Object.keys, Object),\n        nativeMax = Math.max,\n        nativeMin = Math.min,\n        nativeNow = Date.now,\n        nativeParseInt = context.parseInt,\n        nativeRandom = Math.random,\n        nativeReverse = arrayProto.reverse;\n\n    /* Built-in method references that are verified to be native. */\n    var DataView = getNative(context, 'DataView'),\n        Map = getNative(context, 'Map'),\n        Promise = getNative(context, 'Promise'),\n        Set = getNative(context, 'Set'),\n        WeakMap = getNative(context, 'WeakMap'),\n        nativeCreate = getNative(Object, 'create');\n\n    /** Used to store function metadata. */\n    var metaMap = WeakMap && new WeakMap;\n\n    /** Used to lookup unminified function names. */\n    var realNames = {};\n\n    /** Used to detect maps, sets, and weakmaps. */\n    var dataViewCtorString = toSource(DataView),\n        mapCtorString = toSource(Map),\n        promiseCtorString = toSource(Promise),\n        setCtorString = toSource(Set),\n        weakMapCtorString = toSource(WeakMap);\n\n    /** Used to convert symbols to primitives and strings. */\n    var symbolProto = Symbol ? Symbol.prototype : undefined,\n        symbolValueOf = symbolProto ? symbolProto.valueOf : undefined,\n        symbolToString = symbolProto ? symbolProto.toString : undefined;\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates a `lodash` object which wraps `value` to enable implicit method\n     * chain sequences. Methods that operate on and return arrays, collections,\n     * and functions can be chained together. Methods that retrieve a single value\n     * or may return a primitive value will automatically end the chain sequence\n     * and return the unwrapped value. Otherwise, the value must be unwrapped\n     * with `_#value`.\n     *\n     * Explicit chain sequences, which must be unwrapped with `_#value`, may be\n     * enabled using `_.chain`.\n     *\n     * The execution of chained methods is lazy, that is, it's deferred until\n     * `_#value` is implicitly or explicitly called.\n     *\n     * Lazy evaluation allows several methods to support shortcut fusion.\n     * Shortcut fusion is an optimization to merge iteratee calls; this avoids\n     * the creation of intermediate arrays and can greatly reduce the number of\n     * iteratee executions. Sections of a chain sequence qualify for shortcut\n     * fusion if the section is applied to an array and iteratees accept only\n     * one argument. The heuristic for whether a section qualifies for shortcut\n     * fusion is subject to change.\n     *\n     * Chaining is supported in custom builds as long as the `_#value` method is\n     * directly or indirectly included in the build.\n     *\n     * In addition to lodash methods, wrappers have `Array` and `String` methods.\n     *\n     * The wrapper `Array` methods are:\n     * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift`\n     *\n     * The wrapper `String` methods are:\n     * `replace` and `split`\n     *\n     * The wrapper methods that support shortcut fusion are:\n     * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`,\n     * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`,\n     * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray`\n     *\n     * The chainable wrapper methods are:\n     * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`,\n     * `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`,\n     * `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`,\n     * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`,\n     * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`,\n     * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`,\n     * `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`,\n     * `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`,\n     * `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`,\n     * `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`,\n     * `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`,\n     * `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`,\n     * `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`,\n     * `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`,\n     * `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`,\n     * `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`,\n     * `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`,\n     * `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`,\n     * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`,\n     * `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`,\n     * `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`,\n     * `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`,\n     * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`,\n     * `zipObject`, `zipObjectDeep`, and `zipWith`\n     *\n     * The wrapper methods that are **not** chainable by default are:\n     * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`,\n     * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`,\n     * `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`,\n     * `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`,\n     * `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`,\n     * `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`,\n     * `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`,\n     * `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`,\n     * `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`,\n     * `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`,\n     * `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`,\n     * `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`,\n     * `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`,\n     * `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`,\n     * `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`,\n     * `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`,\n     * `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`,\n     * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`,\n     * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`,\n     * `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`,\n     * `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`,\n     * `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`,\n     * `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`,\n     * `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`,\n     * `upperFirst`, `value`, and `words`\n     *\n     * @name _\n     * @constructor\n     * @category Seq\n     * @param {*} value The value to wrap in a `lodash` instance.\n     * @returns {Object} Returns the new `lodash` wrapper instance.\n     * @example\n     *\n     * function square(n) {\n     *   return n * n;\n     * }\n     *\n     * var wrapped = _([1, 2, 3]);\n     *\n     * // Returns an unwrapped value.\n     * wrapped.reduce(_.add);\n     * // => 6\n     *\n     * // Returns a wrapped value.\n     * var squares = wrapped.map(square);\n     *\n     * _.isArray(squares);\n     * // => false\n     *\n     * _.isArray(squares.value());\n     * // => true\n     */\n    function lodash(value) {\n      if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {\n        if (value instanceof LodashWrapper) {\n          return value;\n        }\n        if (hasOwnProperty.call(value, '__wrapped__')) {\n          return wrapperClone(value);\n        }\n      }\n      return new LodashWrapper(value);\n    }\n\n    /**\n     * The base implementation of `_.create` without support for assigning\n     * properties to the created object.\n     *\n     * @private\n     * @param {Object} proto The object to inherit from.\n     * @returns {Object} Returns the new object.\n     */\n    var baseCreate = (function() {\n      function object() {}\n      return function(proto) {\n        if (!isObject(proto)) {\n          return {};\n        }\n        if (objectCreate) {\n          return objectCreate(proto);\n        }\n        object.prototype = proto;\n        var result = new object;\n        object.prototype = undefined;\n        return result;\n      };\n    }());\n\n    /**\n     * The function whose prototype chain sequence wrappers inherit from.\n     *\n     * @private\n     */\n    function baseLodash() {\n      // No operation performed.\n    }\n\n    /**\n     * The base constructor for creating `lodash` wrapper objects.\n     *\n     * @private\n     * @param {*} value The value to wrap.\n     * @param {boolean} [chainAll] Enable explicit method chain sequences.\n     */\n    function LodashWrapper(value, chainAll) {\n      this.__wrapped__ = value;\n      this.__actions__ = [];\n      this.__chain__ = !!chainAll;\n      this.__index__ = 0;\n      this.__values__ = undefined;\n    }\n\n    /**\n     * By default, the template delimiters used by lodash are like those in\n     * embedded Ruby (ERB) as well as ES2015 template strings. Change the\n     * following template settings to use alternative delimiters.\n     *\n     * @static\n     * @memberOf _\n     * @type {Object}\n     */\n    lodash.templateSettings = {\n\n      /**\n       * Used to detect `data` property values to be HTML-escaped.\n       *\n       * @memberOf _.templateSettings\n       * @type {RegExp}\n       */\n      'escape': reEscape,\n\n      /**\n       * Used to detect code to be evaluated.\n       *\n       * @memberOf _.templateSettings\n       * @type {RegExp}\n       */\n      'evaluate': reEvaluate,\n\n      /**\n       * Used to detect `data` property values to inject.\n       *\n       * @memberOf _.templateSettings\n       * @type {RegExp}\n       */\n      'interpolate': reInterpolate,\n\n      /**\n       * Used to reference the data object in the template text.\n       *\n       * @memberOf _.templateSettings\n       * @type {string}\n       */\n      'variable': '',\n\n      /**\n       * Used to import variables into the compiled template.\n       *\n       * @memberOf _.templateSettings\n       * @type {Object}\n       */\n      'imports': {\n\n        /**\n         * A reference to the `lodash` function.\n         *\n         * @memberOf _.templateSettings.imports\n         * @type {Function}\n         */\n        '_': lodash\n      }\n    };\n\n    // Ensure wrappers are instances of `baseLodash`.\n    lodash.prototype = baseLodash.prototype;\n    lodash.prototype.constructor = lodash;\n\n    LodashWrapper.prototype = baseCreate(baseLodash.prototype);\n    LodashWrapper.prototype.constructor = LodashWrapper;\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation.\n     *\n     * @private\n     * @constructor\n     * @param {*} value The value to wrap.\n     */\n    function LazyWrapper(value) {\n      this.__wrapped__ = value;\n      this.__actions__ = [];\n      this.__dir__ = 1;\n      this.__filtered__ = false;\n      this.__iteratees__ = [];\n      this.__takeCount__ = MAX_ARRAY_LENGTH;\n      this.__views__ = [];\n    }\n\n    /**\n     * Creates a clone of the lazy wrapper object.\n     *\n     * @private\n     * @name clone\n     * @memberOf LazyWrapper\n     * @returns {Object} Returns the cloned `LazyWrapper` object.\n     */\n    function lazyClone() {\n      var result = new LazyWrapper(this.__wrapped__);\n      result.__actions__ = copyArray(this.__actions__);\n      result.__dir__ = this.__dir__;\n      result.__filtered__ = this.__filtered__;\n      result.__iteratees__ = copyArray(this.__iteratees__);\n      result.__takeCount__ = this.__takeCount__;\n      result.__views__ = copyArray(this.__views__);\n      return result;\n    }\n\n    /**\n     * Reverses the direction of lazy iteration.\n     *\n     * @private\n     * @name reverse\n     * @memberOf LazyWrapper\n     * @returns {Object} Returns the new reversed `LazyWrapper` object.\n     */\n    function lazyReverse() {\n      if (this.__filtered__) {\n        var result = new LazyWrapper(this);\n        result.__dir__ = -1;\n        result.__filtered__ = true;\n      } else {\n        result = this.clone();\n        result.__dir__ *= -1;\n      }\n      return result;\n    }\n\n    /**\n     * Extracts the unwrapped value from its lazy wrapper.\n     *\n     * @private\n     * @name value\n     * @memberOf LazyWrapper\n     * @returns {*} Returns the unwrapped value.\n     */\n    function lazyValue() {\n      var array = this.__wrapped__.value(),\n          dir = this.__dir__,\n          isArr = isArray(array),\n          isRight = dir < 0,\n          arrLength = isArr ? array.length : 0,\n          view = getView(0, arrLength, this.__views__),\n          start = view.start,\n          end = view.end,\n          length = end - start,\n          index = isRight ? end : (start - 1),\n          iteratees = this.__iteratees__,\n          iterLength = iteratees.length,\n          resIndex = 0,\n          takeCount = nativeMin(length, this.__takeCount__);\n\n      if (!isArr || (!isRight && arrLength == length && takeCount == length)) {\n        return baseWrapperValue(array, this.__actions__);\n      }\n      var result = [];\n\n      outer:\n      while (length-- && resIndex < takeCount) {\n        index += dir;\n\n        var iterIndex = -1,\n            value = array[index];\n\n        while (++iterIndex < iterLength) {\n          var data = iteratees[iterIndex],\n              iteratee = data.iteratee,\n              type = data.type,\n              computed = iteratee(value);\n\n          if (type == LAZY_MAP_FLAG) {\n            value = computed;\n          } else if (!computed) {\n            if (type == LAZY_FILTER_FLAG) {\n              continue outer;\n            } else {\n              break outer;\n            }\n          }\n        }\n        result[resIndex++] = value;\n      }\n      return result;\n    }\n\n    // Ensure `LazyWrapper` is an instance of `baseLodash`.\n    LazyWrapper.prototype = baseCreate(baseLodash.prototype);\n    LazyWrapper.prototype.constructor = LazyWrapper;\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates a hash object.\n     *\n     * @private\n     * @constructor\n     * @param {Array} [entries] The key-value pairs to cache.\n     */\n    function Hash(entries) {\n      var index = -1,\n          length = entries == null ? 0 : entries.length;\n\n      this.clear();\n      while (++index < length) {\n        var entry = entries[index];\n        this.set(entry[0], entry[1]);\n      }\n    }\n\n    /**\n     * Removes all key-value entries from the hash.\n     *\n     * @private\n     * @name clear\n     * @memberOf Hash\n     */\n    function hashClear() {\n      this.__data__ = nativeCreate ? nativeCreate(null) : {};\n      this.size = 0;\n    }\n\n    /**\n     * Removes `key` and its value from the hash.\n     *\n     * @private\n     * @name delete\n     * @memberOf Hash\n     * @param {Object} hash The hash to modify.\n     * @param {string} key The key of the value to remove.\n     * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n     */\n    function hashDelete(key) {\n      var result = this.has(key) && delete this.__data__[key];\n      this.size -= result ? 1 : 0;\n      return result;\n    }\n\n    /**\n     * Gets the hash value for `key`.\n     *\n     * @private\n     * @name get\n     * @memberOf Hash\n     * @param {string} key The key of the value to get.\n     * @returns {*} Returns the entry value.\n     */\n    function hashGet(key) {\n      var data = this.__data__;\n      if (nativeCreate) {\n        var result = data[key];\n        return result === HASH_UNDEFINED ? undefined : result;\n      }\n      return hasOwnProperty.call(data, key) ? data[key] : undefined;\n    }\n\n    /**\n     * Checks if a hash value for `key` exists.\n     *\n     * @private\n     * @name has\n     * @memberOf Hash\n     * @param {string} key The key of the entry to check.\n     * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n     */\n    function hashHas(key) {\n      var data = this.__data__;\n      return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key);\n    }\n\n    /**\n     * Sets the hash `key` to `value`.\n     *\n     * @private\n     * @name set\n     * @memberOf Hash\n     * @param {string} key The key of the value to set.\n     * @param {*} value The value to set.\n     * @returns {Object} Returns the hash instance.\n     */\n    function hashSet(key, value) {\n      var data = this.__data__;\n      this.size += this.has(key) ? 0 : 1;\n      data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;\n      return this;\n    }\n\n    // Add methods to `Hash`.\n    Hash.prototype.clear = hashClear;\n    Hash.prototype['delete'] = hashDelete;\n    Hash.prototype.get = hashGet;\n    Hash.prototype.has = hashHas;\n    Hash.prototype.set = hashSet;\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates an list cache object.\n     *\n     * @private\n     * @constructor\n     * @param {Array} [entries] The key-value pairs to cache.\n     */\n    function ListCache(entries) {\n      var index = -1,\n          length = entries == null ? 0 : entries.length;\n\n      this.clear();\n      while (++index < length) {\n        var entry = entries[index];\n        this.set(entry[0], entry[1]);\n      }\n    }\n\n    /**\n     * Removes all key-value entries from the list cache.\n     *\n     * @private\n     * @name clear\n     * @memberOf ListCache\n     */\n    function listCacheClear() {\n      this.__data__ = [];\n      this.size = 0;\n    }\n\n    /**\n     * Removes `key` and its value from the list cache.\n     *\n     * @private\n     * @name delete\n     * @memberOf ListCache\n     * @param {string} key The key of the value to remove.\n     * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n     */\n    function listCacheDelete(key) {\n      var data = this.__data__,\n          index = assocIndexOf(data, key);\n\n      if (index < 0) {\n        return false;\n      }\n      var lastIndex = data.length - 1;\n      if (index == lastIndex) {\n        data.pop();\n      } else {\n        splice.call(data, index, 1);\n      }\n      --this.size;\n      return true;\n    }\n\n    /**\n     * Gets the list cache value for `key`.\n     *\n     * @private\n     * @name get\n     * @memberOf ListCache\n     * @param {string} key The key of the value to get.\n     * @returns {*} Returns the entry value.\n     */\n    function listCacheGet(key) {\n      var data = this.__data__,\n          index = assocIndexOf(data, key);\n\n      return index < 0 ? undefined : data[index][1];\n    }\n\n    /**\n     * Checks if a list cache value for `key` exists.\n     *\n     * @private\n     * @name has\n     * @memberOf ListCache\n     * @param {string} key The key of the entry to check.\n     * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n     */\n    function listCacheHas(key) {\n      return assocIndexOf(this.__data__, key) > -1;\n    }\n\n    /**\n     * Sets the list cache `key` to `value`.\n     *\n     * @private\n     * @name set\n     * @memberOf ListCache\n     * @param {string} key The key of the value to set.\n     * @param {*} value The value to set.\n     * @returns {Object} Returns the list cache instance.\n     */\n    function listCacheSet(key, value) {\n      var data = this.__data__,\n          index = assocIndexOf(data, key);\n\n      if (index < 0) {\n        ++this.size;\n        data.push([key, value]);\n      } else {\n        data[index][1] = value;\n      }\n      return this;\n    }\n\n    // Add methods to `ListCache`.\n    ListCache.prototype.clear = listCacheClear;\n    ListCache.prototype['delete'] = listCacheDelete;\n    ListCache.prototype.get = listCacheGet;\n    ListCache.prototype.has = listCacheHas;\n    ListCache.prototype.set = listCacheSet;\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates a map cache object to store key-value pairs.\n     *\n     * @private\n     * @constructor\n     * @param {Array} [entries] The key-value pairs to cache.\n     */\n    function MapCache(entries) {\n      var index = -1,\n          length = entries == null ? 0 : entries.length;\n\n      this.clear();\n      while (++index < length) {\n        var entry = entries[index];\n        this.set(entry[0], entry[1]);\n      }\n    }\n\n    /**\n     * Removes all key-value entries from the map.\n     *\n     * @private\n     * @name clear\n     * @memberOf MapCache\n     */\n    function mapCacheClear() {\n      this.size = 0;\n      this.__data__ = {\n        'hash': new Hash,\n        'map': new (Map || ListCache),\n        'string': new Hash\n      };\n    }\n\n    /**\n     * Removes `key` and its value from the map.\n     *\n     * @private\n     * @name delete\n     * @memberOf MapCache\n     * @param {string} key The key of the value to remove.\n     * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n     */\n    function mapCacheDelete(key) {\n      var result = getMapData(this, key)['delete'](key);\n      this.size -= result ? 1 : 0;\n      return result;\n    }\n\n    /**\n     * Gets the map value for `key`.\n     *\n     * @private\n     * @name get\n     * @memberOf MapCache\n     * @param {string} key The key of the value to get.\n     * @returns {*} Returns the entry value.\n     */\n    function mapCacheGet(key) {\n      return getMapData(this, key).get(key);\n    }\n\n    /**\n     * Checks if a map value for `key` exists.\n     *\n     * @private\n     * @name has\n     * @memberOf MapCache\n     * @param {string} key The key of the entry to check.\n     * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n     */\n    function mapCacheHas(key) {\n      return getMapData(this, key).has(key);\n    }\n\n    /**\n     * Sets the map `key` to `value`.\n     *\n     * @private\n     * @name set\n     * @memberOf MapCache\n     * @param {string} key The key of the value to set.\n     * @param {*} value The value to set.\n     * @returns {Object} Returns the map cache instance.\n     */\n    function mapCacheSet(key, value) {\n      var data = getMapData(this, key),\n          size = data.size;\n\n      data.set(key, value);\n      this.size += data.size == size ? 0 : 1;\n      return this;\n    }\n\n    // Add methods to `MapCache`.\n    MapCache.prototype.clear = mapCacheClear;\n    MapCache.prototype['delete'] = mapCacheDelete;\n    MapCache.prototype.get = mapCacheGet;\n    MapCache.prototype.has = mapCacheHas;\n    MapCache.prototype.set = mapCacheSet;\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     *\n     * Creates an array cache object to store unique values.\n     *\n     * @private\n     * @constructor\n     * @param {Array} [values] The values to cache.\n     */\n    function SetCache(values) {\n      var index = -1,\n          length = values == null ? 0 : values.length;\n\n      this.__data__ = new MapCache;\n      while (++index < length) {\n        this.add(values[index]);\n      }\n    }\n\n    /**\n     * Adds `value` to the array cache.\n     *\n     * @private\n     * @name add\n     * @memberOf SetCache\n     * @alias push\n     * @param {*} value The value to cache.\n     * @returns {Object} Returns the cache instance.\n     */\n    function setCacheAdd(value) {\n      this.__data__.set(value, HASH_UNDEFINED);\n      return this;\n    }\n\n    /**\n     * Checks if `value` is in the array cache.\n     *\n     * @private\n     * @name has\n     * @memberOf SetCache\n     * @param {*} value The value to search for.\n     * @returns {number} Returns `true` if `value` is found, else `false`.\n     */\n    function setCacheHas(value) {\n      return this.__data__.has(value);\n    }\n\n    // Add methods to `SetCache`.\n    SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;\n    SetCache.prototype.has = setCacheHas;\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates a stack cache object to store key-value pairs.\n     *\n     * @private\n     * @constructor\n     * @param {Array} [entries] The key-value pairs to cache.\n     */\n    function Stack(entries) {\n      var data = this.__data__ = new ListCache(entries);\n      this.size = data.size;\n    }\n\n    /**\n     * Removes all key-value entries from the stack.\n     *\n     * @private\n     * @name clear\n     * @memberOf Stack\n     */\n    function stackClear() {\n      this.__data__ = new ListCache;\n      this.size = 0;\n    }\n\n    /**\n     * Removes `key` and its value from the stack.\n     *\n     * @private\n     * @name delete\n     * @memberOf Stack\n     * @param {string} key The key of the value to remove.\n     * @returns {boolean} Returns `true` if the entry was removed, else `false`.\n     */\n    function stackDelete(key) {\n      var data = this.__data__,\n          result = data['delete'](key);\n\n      this.size = data.size;\n      return result;\n    }\n\n    /**\n     * Gets the stack value for `key`.\n     *\n     * @private\n     * @name get\n     * @memberOf Stack\n     * @param {string} key The key of the value to get.\n     * @returns {*} Returns the entry value.\n     */\n    function stackGet(key) {\n      return this.__data__.get(key);\n    }\n\n    /**\n     * Checks if a stack value for `key` exists.\n     *\n     * @private\n     * @name has\n     * @memberOf Stack\n     * @param {string} key The key of the entry to check.\n     * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.\n     */\n    function stackHas(key) {\n      return this.__data__.has(key);\n    }\n\n    /**\n     * Sets the stack `key` to `value`.\n     *\n     * @private\n     * @name set\n     * @memberOf Stack\n     * @param {string} key The key of the value to set.\n     * @param {*} value The value to set.\n     * @returns {Object} Returns the stack cache instance.\n     */\n    function stackSet(key, value) {\n      var data = this.__data__;\n      if (data instanceof ListCache) {\n        var pairs = data.__data__;\n        if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {\n          pairs.push([key, value]);\n          this.size = ++data.size;\n          return this;\n        }\n        data = this.__data__ = new MapCache(pairs);\n      }\n      data.set(key, value);\n      this.size = data.size;\n      return this;\n    }\n\n    // Add methods to `Stack`.\n    Stack.prototype.clear = stackClear;\n    Stack.prototype['delete'] = stackDelete;\n    Stack.prototype.get = stackGet;\n    Stack.prototype.has = stackHas;\n    Stack.prototype.set = stackSet;\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates an array of the enumerable property names of the array-like `value`.\n     *\n     * @private\n     * @param {*} value The value to query.\n     * @param {boolean} inherited Specify returning inherited property names.\n     * @returns {Array} Returns the array of property names.\n     */\n    function arrayLikeKeys(value, inherited) {\n      var isArr = isArray(value),\n          isArg = !isArr && isArguments(value),\n          isBuff = !isArr && !isArg && isBuffer(value),\n          isType = !isArr && !isArg && !isBuff && isTypedArray(value),\n          skipIndexes = isArr || isArg || isBuff || isType,\n          result = skipIndexes ? baseTimes(value.length, String) : [],\n          length = result.length;\n\n      for (var key in value) {\n        if ((inherited || hasOwnProperty.call(value, key)) &&\n            !(skipIndexes && (\n               // Safari 9 has enumerable `arguments.length` in strict mode.\n               key == 'length' ||\n               // Node.js 0.10 has enumerable non-index properties on buffers.\n               (isBuff && (key == 'offset' || key == 'parent')) ||\n               // PhantomJS 2 has enumerable non-index properties on typed arrays.\n               (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||\n               // Skip index properties.\n               isIndex(key, length)\n            ))) {\n          result.push(key);\n        }\n      }\n      return result;\n    }\n\n    /**\n     * A specialized version of `_.sample` for arrays.\n     *\n     * @private\n     * @param {Array} array The array to sample.\n     * @returns {*} Returns the random element.\n     */\n    function arraySample(array) {\n      var length = array.length;\n      return length ? array[baseRandom(0, length - 1)] : undefined;\n    }\n\n    /**\n     * A specialized version of `_.sampleSize` for arrays.\n     *\n     * @private\n     * @param {Array} array The array to sample.\n     * @param {number} n The number of elements to sample.\n     * @returns {Array} Returns the random elements.\n     */\n    function arraySampleSize(array, n) {\n      return shuffleSelf(copyArray(array), baseClamp(n, 0, array.length));\n    }\n\n    /**\n     * A specialized version of `_.shuffle` for arrays.\n     *\n     * @private\n     * @param {Array} array The array to shuffle.\n     * @returns {Array} Returns the new shuffled array.\n     */\n    function arrayShuffle(array) {\n      return shuffleSelf(copyArray(array));\n    }\n\n    /**\n     * This function is like `assignValue` except that it doesn't assign\n     * `undefined` values.\n     *\n     * @private\n     * @param {Object} object The object to modify.\n     * @param {string} key The key of the property to assign.\n     * @param {*} value The value to assign.\n     */\n    function assignMergeValue(object, key, value) {\n      if ((value !== undefined && !eq(object[key], value)) ||\n          (value === undefined && !(key in object))) {\n        baseAssignValue(object, key, value);\n      }\n    }\n\n    /**\n     * Assigns `value` to `key` of `object` if the existing value is not equivalent\n     * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * for equality comparisons.\n     *\n     * @private\n     * @param {Object} object The object to modify.\n     * @param {string} key The key of the property to assign.\n     * @param {*} value The value to assign.\n     */\n    function assignValue(object, key, value) {\n      var objValue = object[key];\n      if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||\n          (value === undefined && !(key in object))) {\n        baseAssignValue(object, key, value);\n      }\n    }\n\n    /**\n     * Gets the index at which the `key` is found in `array` of key-value pairs.\n     *\n     * @private\n     * @param {Array} array The array to inspect.\n     * @param {*} key The key to search for.\n     * @returns {number} Returns the index of the matched value, else `-1`.\n     */\n    function assocIndexOf(array, key) {\n      var length = array.length;\n      while (length--) {\n        if (eq(array[length][0], key)) {\n          return length;\n        }\n      }\n      return -1;\n    }\n\n    /**\n     * Aggregates elements of `collection` on `accumulator` with keys transformed\n     * by `iteratee` and values set by `setter`.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} setter The function to set `accumulator` values.\n     * @param {Function} iteratee The iteratee to transform keys.\n     * @param {Object} accumulator The initial aggregated object.\n     * @returns {Function} Returns `accumulator`.\n     */\n    function baseAggregator(collection, setter, iteratee, accumulator) {\n      baseEach(collection, function(value, key, collection) {\n        setter(accumulator, value, iteratee(value), collection);\n      });\n      return accumulator;\n    }\n\n    /**\n     * The base implementation of `_.assign` without support for multiple sources\n     * or `customizer` functions.\n     *\n     * @private\n     * @param {Object} object The destination object.\n     * @param {Object} source The source object.\n     * @returns {Object} Returns `object`.\n     */\n    function baseAssign(object, source) {\n      return object && copyObject(source, keys(source), object);\n    }\n\n    /**\n     * The base implementation of `_.assignIn` without support for multiple sources\n     * or `customizer` functions.\n     *\n     * @private\n     * @param {Object} object The destination object.\n     * @param {Object} source The source object.\n     * @returns {Object} Returns `object`.\n     */\n    function baseAssignIn(object, source) {\n      return object && copyObject(source, keysIn(source), object);\n    }\n\n    /**\n     * The base implementation of `assignValue` and `assignMergeValue` without\n     * value checks.\n     *\n     * @private\n     * @param {Object} object The object to modify.\n     * @param {string} key The key of the property to assign.\n     * @param {*} value The value to assign.\n     */\n    function baseAssignValue(object, key, value) {\n      if (key == '__proto__' && defineProperty) {\n        defineProperty(object, key, {\n          'configurable': true,\n          'enumerable': true,\n          'value': value,\n          'writable': true\n        });\n      } else {\n        object[key] = value;\n      }\n    }\n\n    /**\n     * The base implementation of `_.at` without support for individual paths.\n     *\n     * @private\n     * @param {Object} object The object to iterate over.\n     * @param {string[]} paths The property paths to pick.\n     * @returns {Array} Returns the picked elements.\n     */\n    function baseAt(object, paths) {\n      var index = -1,\n          length = paths.length,\n          result = Array(length),\n          skip = object == null;\n\n      while (++index < length) {\n        result[index] = skip ? undefined : get(object, paths[index]);\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.clamp` which doesn't coerce arguments.\n     *\n     * @private\n     * @param {number} number The number to clamp.\n     * @param {number} [lower] The lower bound.\n     * @param {number} upper The upper bound.\n     * @returns {number} Returns the clamped number.\n     */\n    function baseClamp(number, lower, upper) {\n      if (number === number) {\n        if (upper !== undefined) {\n          number = number <= upper ? number : upper;\n        }\n        if (lower !== undefined) {\n          number = number >= lower ? number : lower;\n        }\n      }\n      return number;\n    }\n\n    /**\n     * The base implementation of `_.clone` and `_.cloneDeep` which tracks\n     * traversed objects.\n     *\n     * @private\n     * @param {*} value The value to clone.\n     * @param {boolean} bitmask The bitmask flags.\n     *  1 - Deep clone\n     *  2 - Flatten inherited properties\n     *  4 - Clone symbols\n     * @param {Function} [customizer] The function to customize cloning.\n     * @param {string} [key] The key of `value`.\n     * @param {Object} [object] The parent object of `value`.\n     * @param {Object} [stack] Tracks traversed objects and their clone counterparts.\n     * @returns {*} Returns the cloned value.\n     */\n    function baseClone(value, bitmask, customizer, key, object, stack) {\n      var result,\n          isDeep = bitmask & CLONE_DEEP_FLAG,\n          isFlat = bitmask & CLONE_FLAT_FLAG,\n          isFull = bitmask & CLONE_SYMBOLS_FLAG;\n\n      if (customizer) {\n        result = object ? customizer(value, key, object, stack) : customizer(value);\n      }\n      if (result !== undefined) {\n        return result;\n      }\n      if (!isObject(value)) {\n        return value;\n      }\n      var isArr = isArray(value);\n      if (isArr) {\n        result = initCloneArray(value);\n        if (!isDeep) {\n          return copyArray(value, result);\n        }\n      } else {\n        var tag = getTag(value),\n            isFunc = tag == funcTag || tag == genTag;\n\n        if (isBuffer(value)) {\n          return cloneBuffer(value, isDeep);\n        }\n        if (tag == objectTag || tag == argsTag || (isFunc && !object)) {\n          result = (isFlat || isFunc) ? {} : initCloneObject(value);\n          if (!isDeep) {\n            return isFlat\n              ? copySymbolsIn(value, baseAssignIn(result, value))\n              : copySymbols(value, baseAssign(result, value));\n          }\n        } else {\n          if (!cloneableTags[tag]) {\n            return object ? value : {};\n          }\n          result = initCloneByTag(value, tag, isDeep);\n        }\n      }\n      // Check for circular references and return its corresponding clone.\n      stack || (stack = new Stack);\n      var stacked = stack.get(value);\n      if (stacked) {\n        return stacked;\n      }\n      stack.set(value, result);\n\n      if (isSet(value)) {\n        value.forEach(function(subValue) {\n          result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));\n        });\n      } else if (isMap(value)) {\n        value.forEach(function(subValue, key) {\n          result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));\n        });\n      }\n\n      var keysFunc = isFull\n        ? (isFlat ? getAllKeysIn : getAllKeys)\n        : (isFlat ? keysIn : keys);\n\n      var props = isArr ? undefined : keysFunc(value);\n      arrayEach(props || value, function(subValue, key) {\n        if (props) {\n          key = subValue;\n          subValue = value[key];\n        }\n        // Recursively populate clone (susceptible to call stack limits).\n        assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));\n      });\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.conforms` which doesn't clone `source`.\n     *\n     * @private\n     * @param {Object} source The object of property predicates to conform to.\n     * @returns {Function} Returns the new spec function.\n     */\n    function baseConforms(source) {\n      var props = keys(source);\n      return function(object) {\n        return baseConformsTo(object, source, props);\n      };\n    }\n\n    /**\n     * The base implementation of `_.conformsTo` which accepts `props` to check.\n     *\n     * @private\n     * @param {Object} object The object to inspect.\n     * @param {Object} source The object of property predicates to conform to.\n     * @returns {boolean} Returns `true` if `object` conforms, else `false`.\n     */\n    function baseConformsTo(object, source, props) {\n      var length = props.length;\n      if (object == null) {\n        return !length;\n      }\n      object = Object(object);\n      while (length--) {\n        var key = props[length],\n            predicate = source[key],\n            value = object[key];\n\n        if ((value === undefined && !(key in object)) || !predicate(value)) {\n          return false;\n        }\n      }\n      return true;\n    }\n\n    /**\n     * The base implementation of `_.delay` and `_.defer` which accepts `args`\n     * to provide to `func`.\n     *\n     * @private\n     * @param {Function} func The function to delay.\n     * @param {number} wait The number of milliseconds to delay invocation.\n     * @param {Array} args The arguments to provide to `func`.\n     * @returns {number|Object} Returns the timer id or timeout object.\n     */\n    function baseDelay(func, wait, args) {\n      if (typeof func != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      return setTimeout(function() { func.apply(undefined, args); }, wait);\n    }\n\n    /**\n     * The base implementation of methods like `_.difference` without support\n     * for excluding multiple arrays or iteratee shorthands.\n     *\n     * @private\n     * @param {Array} array The array to inspect.\n     * @param {Array} values The values to exclude.\n     * @param {Function} [iteratee] The iteratee invoked per element.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new array of filtered values.\n     */\n    function baseDifference(array, values, iteratee, comparator) {\n      var index = -1,\n          includes = arrayIncludes,\n          isCommon = true,\n          length = array.length,\n          result = [],\n          valuesLength = values.length;\n\n      if (!length) {\n        return result;\n      }\n      if (iteratee) {\n        values = arrayMap(values, baseUnary(iteratee));\n      }\n      if (comparator) {\n        includes = arrayIncludesWith;\n        isCommon = false;\n      }\n      else if (values.length >= LARGE_ARRAY_SIZE) {\n        includes = cacheHas;\n        isCommon = false;\n        values = new SetCache(values);\n      }\n      outer:\n      while (++index < length) {\n        var value = array[index],\n            computed = iteratee == null ? value : iteratee(value);\n\n        value = (comparator || value !== 0) ? value : 0;\n        if (isCommon && computed === computed) {\n          var valuesIndex = valuesLength;\n          while (valuesIndex--) {\n            if (values[valuesIndex] === computed) {\n              continue outer;\n            }\n          }\n          result.push(value);\n        }\n        else if (!includes(values, computed, comparator)) {\n          result.push(value);\n        }\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.forEach` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} iteratee The function invoked per iteration.\n     * @returns {Array|Object} Returns `collection`.\n     */\n    var baseEach = createBaseEach(baseForOwn);\n\n    /**\n     * The base implementation of `_.forEachRight` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} iteratee The function invoked per iteration.\n     * @returns {Array|Object} Returns `collection`.\n     */\n    var baseEachRight = createBaseEach(baseForOwnRight, true);\n\n    /**\n     * The base implementation of `_.every` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} predicate The function invoked per iteration.\n     * @returns {boolean} Returns `true` if all elements pass the predicate check,\n     *  else `false`\n     */\n    function baseEvery(collection, predicate) {\n      var result = true;\n      baseEach(collection, function(value, index, collection) {\n        result = !!predicate(value, index, collection);\n        return result;\n      });\n      return result;\n    }\n\n    /**\n     * The base implementation of methods like `_.max` and `_.min` which accepts a\n     * `comparator` to determine the extremum value.\n     *\n     * @private\n     * @param {Array} array The array to iterate over.\n     * @param {Function} iteratee The iteratee invoked per iteration.\n     * @param {Function} comparator The comparator used to compare values.\n     * @returns {*} Returns the extremum value.\n     */\n    function baseExtremum(array, iteratee, comparator) {\n      var index = -1,\n          length = array.length;\n\n      while (++index < length) {\n        var value = array[index],\n            current = iteratee(value);\n\n        if (current != null && (computed === undefined\n              ? (current === current && !isSymbol(current))\n              : comparator(current, computed)\n            )) {\n          var computed = current,\n              result = value;\n        }\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.fill` without an iteratee call guard.\n     *\n     * @private\n     * @param {Array} array The array to fill.\n     * @param {*} value The value to fill `array` with.\n     * @param {number} [start=0] The start position.\n     * @param {number} [end=array.length] The end position.\n     * @returns {Array} Returns `array`.\n     */\n    function baseFill(array, value, start, end) {\n      var length = array.length;\n\n      start = toInteger(start);\n      if (start < 0) {\n        start = -start > length ? 0 : (length + start);\n      }\n      end = (end === undefined || end > length) ? length : toInteger(end);\n      if (end < 0) {\n        end += length;\n      }\n      end = start > end ? 0 : toLength(end);\n      while (start < end) {\n        array[start++] = value;\n      }\n      return array;\n    }\n\n    /**\n     * The base implementation of `_.filter` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} predicate The function invoked per iteration.\n     * @returns {Array} Returns the new filtered array.\n     */\n    function baseFilter(collection, predicate) {\n      var result = [];\n      baseEach(collection, function(value, index, collection) {\n        if (predicate(value, index, collection)) {\n          result.push(value);\n        }\n      });\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.flatten` with support for restricting flattening.\n     *\n     * @private\n     * @param {Array} array The array to flatten.\n     * @param {number} depth The maximum recursion depth.\n     * @param {boolean} [predicate=isFlattenable] The function invoked per iteration.\n     * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks.\n     * @param {Array} [result=[]] The initial result value.\n     * @returns {Array} Returns the new flattened array.\n     */\n    function baseFlatten(array, depth, predicate, isStrict, result) {\n      var index = -1,\n          length = array.length;\n\n      predicate || (predicate = isFlattenable);\n      result || (result = []);\n\n      while (++index < length) {\n        var value = array[index];\n        if (depth > 0 && predicate(value)) {\n          if (depth > 1) {\n            // Recursively flatten arrays (susceptible to call stack limits).\n            baseFlatten(value, depth - 1, predicate, isStrict, result);\n          } else {\n            arrayPush(result, value);\n          }\n        } else if (!isStrict) {\n          result[result.length] = value;\n        }\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `baseForOwn` which iterates over `object`\n     * properties returned by `keysFunc` and invokes `iteratee` for each property.\n     * Iteratee functions may exit iteration early by explicitly returning `false`.\n     *\n     * @private\n     * @param {Object} object The object to iterate over.\n     * @param {Function} iteratee The function invoked per iteration.\n     * @param {Function} keysFunc The function to get the keys of `object`.\n     * @returns {Object} Returns `object`.\n     */\n    var baseFor = createBaseFor();\n\n    /**\n     * This function is like `baseFor` except that it iterates over properties\n     * in the opposite order.\n     *\n     * @private\n     * @param {Object} object The object to iterate over.\n     * @param {Function} iteratee The function invoked per iteration.\n     * @param {Function} keysFunc The function to get the keys of `object`.\n     * @returns {Object} Returns `object`.\n     */\n    var baseForRight = createBaseFor(true);\n\n    /**\n     * The base implementation of `_.forOwn` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Object} object The object to iterate over.\n     * @param {Function} iteratee The function invoked per iteration.\n     * @returns {Object} Returns `object`.\n     */\n    function baseForOwn(object, iteratee) {\n      return object && baseFor(object, iteratee, keys);\n    }\n\n    /**\n     * The base implementation of `_.forOwnRight` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Object} object The object to iterate over.\n     * @param {Function} iteratee The function invoked per iteration.\n     * @returns {Object} Returns `object`.\n     */\n    function baseForOwnRight(object, iteratee) {\n      return object && baseForRight(object, iteratee, keys);\n    }\n\n    /**\n     * The base implementation of `_.functions` which creates an array of\n     * `object` function property names filtered from `props`.\n     *\n     * @private\n     * @param {Object} object The object to inspect.\n     * @param {Array} props The property names to filter.\n     * @returns {Array} Returns the function names.\n     */\n    function baseFunctions(object, props) {\n      return arrayFilter(props, function(key) {\n        return isFunction(object[key]);\n      });\n    }\n\n    /**\n     * The base implementation of `_.get` without support for default values.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @param {Array|string} path The path of the property to get.\n     * @returns {*} Returns the resolved value.\n     */\n    function baseGet(object, path) {\n      path = castPath(path, object);\n\n      var index = 0,\n          length = path.length;\n\n      while (object != null && index < length) {\n        object = object[toKey(path[index++])];\n      }\n      return (index && index == length) ? object : undefined;\n    }\n\n    /**\n     * The base implementation of `getAllKeys` and `getAllKeysIn` which uses\n     * `keysFunc` and `symbolsFunc` to get the enumerable property names and\n     * symbols of `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @param {Function} keysFunc The function to get the keys of `object`.\n     * @param {Function} symbolsFunc The function to get the symbols of `object`.\n     * @returns {Array} Returns the array of property names and symbols.\n     */\n    function baseGetAllKeys(object, keysFunc, symbolsFunc) {\n      var result = keysFunc(object);\n      return isArray(object) ? result : arrayPush(result, symbolsFunc(object));\n    }\n\n    /**\n     * The base implementation of `getTag` without fallbacks for buggy environments.\n     *\n     * @private\n     * @param {*} value The value to query.\n     * @returns {string} Returns the `toStringTag`.\n     */\n    function baseGetTag(value) {\n      if (value == null) {\n        return value === undefined ? undefinedTag : nullTag;\n      }\n      return (symToStringTag && symToStringTag in Object(value))\n        ? getRawTag(value)\n        : objectToString(value);\n    }\n\n    /**\n     * The base implementation of `_.gt` which doesn't coerce arguments.\n     *\n     * @private\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {boolean} Returns `true` if `value` is greater than `other`,\n     *  else `false`.\n     */\n    function baseGt(value, other) {\n      return value > other;\n    }\n\n    /**\n     * The base implementation of `_.has` without support for deep paths.\n     *\n     * @private\n     * @param {Object} [object] The object to query.\n     * @param {Array|string} key The key to check.\n     * @returns {boolean} Returns `true` if `key` exists, else `false`.\n     */\n    function baseHas(object, key) {\n      return object != null && hasOwnProperty.call(object, key);\n    }\n\n    /**\n     * The base implementation of `_.hasIn` without support for deep paths.\n     *\n     * @private\n     * @param {Object} [object] The object to query.\n     * @param {Array|string} key The key to check.\n     * @returns {boolean} Returns `true` if `key` exists, else `false`.\n     */\n    function baseHasIn(object, key) {\n      return object != null && key in Object(object);\n    }\n\n    /**\n     * The base implementation of `_.inRange` which doesn't coerce arguments.\n     *\n     * @private\n     * @param {number} number The number to check.\n     * @param {number} start The start of the range.\n     * @param {number} end The end of the range.\n     * @returns {boolean} Returns `true` if `number` is in the range, else `false`.\n     */\n    function baseInRange(number, start, end) {\n      return number >= nativeMin(start, end) && number < nativeMax(start, end);\n    }\n\n    /**\n     * The base implementation of methods like `_.intersection`, without support\n     * for iteratee shorthands, that accepts an array of arrays to inspect.\n     *\n     * @private\n     * @param {Array} arrays The arrays to inspect.\n     * @param {Function} [iteratee] The iteratee invoked per element.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new array of shared values.\n     */\n    function baseIntersection(arrays, iteratee, comparator) {\n      var includes = comparator ? arrayIncludesWith : arrayIncludes,\n          length = arrays[0].length,\n          othLength = arrays.length,\n          othIndex = othLength,\n          caches = Array(othLength),\n          maxLength = Infinity,\n          result = [];\n\n      while (othIndex--) {\n        var array = arrays[othIndex];\n        if (othIndex && iteratee) {\n          array = arrayMap(array, baseUnary(iteratee));\n        }\n        maxLength = nativeMin(array.length, maxLength);\n        caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120))\n          ? new SetCache(othIndex && array)\n          : undefined;\n      }\n      array = arrays[0];\n\n      var index = -1,\n          seen = caches[0];\n\n      outer:\n      while (++index < length && result.length < maxLength) {\n        var value = array[index],\n            computed = iteratee ? iteratee(value) : value;\n\n        value = (comparator || value !== 0) ? value : 0;\n        if (!(seen\n              ? cacheHas(seen, computed)\n              : includes(result, computed, comparator)\n            )) {\n          othIndex = othLength;\n          while (--othIndex) {\n            var cache = caches[othIndex];\n            if (!(cache\n                  ? cacheHas(cache, computed)\n                  : includes(arrays[othIndex], computed, comparator))\n                ) {\n              continue outer;\n            }\n          }\n          if (seen) {\n            seen.push(computed);\n          }\n          result.push(value);\n        }\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.invert` and `_.invertBy` which inverts\n     * `object` with values transformed by `iteratee` and set by `setter`.\n     *\n     * @private\n     * @param {Object} object The object to iterate over.\n     * @param {Function} setter The function to set `accumulator` values.\n     * @param {Function} iteratee The iteratee to transform values.\n     * @param {Object} accumulator The initial inverted object.\n     * @returns {Function} Returns `accumulator`.\n     */\n    function baseInverter(object, setter, iteratee, accumulator) {\n      baseForOwn(object, function(value, key, object) {\n        setter(accumulator, iteratee(value), key, object);\n      });\n      return accumulator;\n    }\n\n    /**\n     * The base implementation of `_.invoke` without support for individual\n     * method arguments.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @param {Array|string} path The path of the method to invoke.\n     * @param {Array} args The arguments to invoke the method with.\n     * @returns {*} Returns the result of the invoked method.\n     */\n    function baseInvoke(object, path, args) {\n      path = castPath(path, object);\n      object = parent(object, path);\n      var func = object == null ? object : object[toKey(last(path))];\n      return func == null ? undefined : apply(func, object, args);\n    }\n\n    /**\n     * The base implementation of `_.isArguments`.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an `arguments` object,\n     */\n    function baseIsArguments(value) {\n      return isObjectLike(value) && baseGetTag(value) == argsTag;\n    }\n\n    /**\n     * The base implementation of `_.isArrayBuffer` without Node.js optimizations.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`.\n     */\n    function baseIsArrayBuffer(value) {\n      return isObjectLike(value) && baseGetTag(value) == arrayBufferTag;\n    }\n\n    /**\n     * The base implementation of `_.isDate` without Node.js optimizations.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a date object, else `false`.\n     */\n    function baseIsDate(value) {\n      return isObjectLike(value) && baseGetTag(value) == dateTag;\n    }\n\n    /**\n     * The base implementation of `_.isEqual` which supports partial comparisons\n     * and tracks traversed objects.\n     *\n     * @private\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @param {boolean} bitmask The bitmask flags.\n     *  1 - Unordered comparison\n     *  2 - Partial comparison\n     * @param {Function} [customizer] The function to customize comparisons.\n     * @param {Object} [stack] Tracks traversed `value` and `other` objects.\n     * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n     */\n    function baseIsEqual(value, other, bitmask, customizer, stack) {\n      if (value === other) {\n        return true;\n      }\n      if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {\n        return value !== value && other !== other;\n      }\n      return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);\n    }\n\n    /**\n     * A specialized version of `baseIsEqual` for arrays and objects which performs\n     * deep comparisons and tracks traversed objects enabling objects with circular\n     * references to be compared.\n     *\n     * @private\n     * @param {Object} object The object to compare.\n     * @param {Object} other The other object to compare.\n     * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.\n     * @param {Function} customizer The function to customize comparisons.\n     * @param {Function} equalFunc The function to determine equivalents of values.\n     * @param {Object} [stack] Tracks traversed `object` and `other` objects.\n     * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.\n     */\n    function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) {\n      var objIsArr = isArray(object),\n          othIsArr = isArray(other),\n          objTag = objIsArr ? arrayTag : getTag(object),\n          othTag = othIsArr ? arrayTag : getTag(other);\n\n      objTag = objTag == argsTag ? objectTag : objTag;\n      othTag = othTag == argsTag ? objectTag : othTag;\n\n      var objIsObj = objTag == objectTag,\n          othIsObj = othTag == objectTag,\n          isSameTag = objTag == othTag;\n\n      if (isSameTag && isBuffer(object)) {\n        if (!isBuffer(other)) {\n          return false;\n        }\n        objIsArr = true;\n        objIsObj = false;\n      }\n      if (isSameTag && !objIsObj) {\n        stack || (stack = new Stack);\n        return (objIsArr || isTypedArray(object))\n          ? equalArrays(object, other, bitmask, customizer, equalFunc, stack)\n          : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);\n      }\n      if (!(bitmask & COMPARE_PARTIAL_FLAG)) {\n        var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),\n            othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');\n\n        if (objIsWrapped || othIsWrapped) {\n          var objUnwrapped = objIsWrapped ? object.value() : object,\n              othUnwrapped = othIsWrapped ? other.value() : other;\n\n          stack || (stack = new Stack);\n          return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);\n        }\n      }\n      if (!isSameTag) {\n        return false;\n      }\n      stack || (stack = new Stack);\n      return equalObjects(object, other, bitmask, customizer, equalFunc, stack);\n    }\n\n    /**\n     * The base implementation of `_.isMap` without Node.js optimizations.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a map, else `false`.\n     */\n    function baseIsMap(value) {\n      return isObjectLike(value) && getTag(value) == mapTag;\n    }\n\n    /**\n     * The base implementation of `_.isMatch` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Object} object The object to inspect.\n     * @param {Object} source The object of property values to match.\n     * @param {Array} matchData The property names, values, and compare flags to match.\n     * @param {Function} [customizer] The function to customize comparisons.\n     * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n     */\n    function baseIsMatch(object, source, matchData, customizer) {\n      var index = matchData.length,\n          length = index,\n          noCustomizer = !customizer;\n\n      if (object == null) {\n        return !length;\n      }\n      object = Object(object);\n      while (index--) {\n        var data = matchData[index];\n        if ((noCustomizer && data[2])\n              ? data[1] !== object[data[0]]\n              : !(data[0] in object)\n            ) {\n          return false;\n        }\n      }\n      while (++index < length) {\n        data = matchData[index];\n        var key = data[0],\n            objValue = object[key],\n            srcValue = data[1];\n\n        if (noCustomizer && data[2]) {\n          if (objValue === undefined && !(key in object)) {\n            return false;\n          }\n        } else {\n          var stack = new Stack;\n          if (customizer) {\n            var result = customizer(objValue, srcValue, key, object, source, stack);\n          }\n          if (!(result === undefined\n                ? baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG, customizer, stack)\n                : result\n              )) {\n            return false;\n          }\n        }\n      }\n      return true;\n    }\n\n    /**\n     * The base implementation of `_.isNative` without bad shim checks.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a native function,\n     *  else `false`.\n     */\n    function baseIsNative(value) {\n      if (!isObject(value) || isMasked(value)) {\n        return false;\n      }\n      var pattern = isFunction(value) ? reIsNative : reIsHostCtor;\n      return pattern.test(toSource(value));\n    }\n\n    /**\n     * The base implementation of `_.isRegExp` without Node.js optimizations.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a regexp, else `false`.\n     */\n    function baseIsRegExp(value) {\n      return isObjectLike(value) && baseGetTag(value) == regexpTag;\n    }\n\n    /**\n     * The base implementation of `_.isSet` without Node.js optimizations.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a set, else `false`.\n     */\n    function baseIsSet(value) {\n      return isObjectLike(value) && getTag(value) == setTag;\n    }\n\n    /**\n     * The base implementation of `_.isTypedArray` without Node.js optimizations.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.\n     */\n    function baseIsTypedArray(value) {\n      return isObjectLike(value) &&\n        isLength(value.length) && !!typedArrayTags[baseGetTag(value)];\n    }\n\n    /**\n     * The base implementation of `_.iteratee`.\n     *\n     * @private\n     * @param {*} [value=_.identity] The value to convert to an iteratee.\n     * @returns {Function} Returns the iteratee.\n     */\n    function baseIteratee(value) {\n      // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9.\n      // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details.\n      if (typeof value == 'function') {\n        return value;\n      }\n      if (value == null) {\n        return identity;\n      }\n      if (typeof value == 'object') {\n        return isArray(value)\n          ? baseMatchesProperty(value[0], value[1])\n          : baseMatches(value);\n      }\n      return property(value);\n    }\n\n    /**\n     * The base implementation of `_.keys` which doesn't treat sparse arrays as dense.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property names.\n     */\n    function baseKeys(object) {\n      if (!isPrototype(object)) {\n        return nativeKeys(object);\n      }\n      var result = [];\n      for (var key in Object(object)) {\n        if (hasOwnProperty.call(object, key) && key != 'constructor') {\n          result.push(key);\n        }\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property names.\n     */\n    function baseKeysIn(object) {\n      if (!isObject(object)) {\n        return nativeKeysIn(object);\n      }\n      var isProto = isPrototype(object),\n          result = [];\n\n      for (var key in object) {\n        if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {\n          result.push(key);\n        }\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.lt` which doesn't coerce arguments.\n     *\n     * @private\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {boolean} Returns `true` if `value` is less than `other`,\n     *  else `false`.\n     */\n    function baseLt(value, other) {\n      return value < other;\n    }\n\n    /**\n     * The base implementation of `_.map` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} iteratee The function invoked per iteration.\n     * @returns {Array} Returns the new mapped array.\n     */\n    function baseMap(collection, iteratee) {\n      var index = -1,\n          result = isArrayLike(collection) ? Array(collection.length) : [];\n\n      baseEach(collection, function(value, key, collection) {\n        result[++index] = iteratee(value, key, collection);\n      });\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.matches` which doesn't clone `source`.\n     *\n     * @private\n     * @param {Object} source The object of property values to match.\n     * @returns {Function} Returns the new spec function.\n     */\n    function baseMatches(source) {\n      var matchData = getMatchData(source);\n      if (matchData.length == 1 && matchData[0][2]) {\n        return matchesStrictComparable(matchData[0][0], matchData[0][1]);\n      }\n      return function(object) {\n        return object === source || baseIsMatch(object, source, matchData);\n      };\n    }\n\n    /**\n     * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`.\n     *\n     * @private\n     * @param {string} path The path of the property to get.\n     * @param {*} srcValue The value to match.\n     * @returns {Function} Returns the new spec function.\n     */\n    function baseMatchesProperty(path, srcValue) {\n      if (isKey(path) && isStrictComparable(srcValue)) {\n        return matchesStrictComparable(toKey(path), srcValue);\n      }\n      return function(object) {\n        var objValue = get(object, path);\n        return (objValue === undefined && objValue === srcValue)\n          ? hasIn(object, path)\n          : baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG);\n      };\n    }\n\n    /**\n     * The base implementation of `_.merge` without support for multiple sources.\n     *\n     * @private\n     * @param {Object} object The destination object.\n     * @param {Object} source The source object.\n     * @param {number} srcIndex The index of `source`.\n     * @param {Function} [customizer] The function to customize merged values.\n     * @param {Object} [stack] Tracks traversed source values and their merged\n     *  counterparts.\n     */\n    function baseMerge(object, source, srcIndex, customizer, stack) {\n      if (object === source) {\n        return;\n      }\n      baseFor(source, function(srcValue, key) {\n        stack || (stack = new Stack);\n        if (isObject(srcValue)) {\n          baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);\n        }\n        else {\n          var newValue = customizer\n            ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack)\n            : undefined;\n\n          if (newValue === undefined) {\n            newValue = srcValue;\n          }\n          assignMergeValue(object, key, newValue);\n        }\n      }, keysIn);\n    }\n\n    /**\n     * A specialized version of `baseMerge` for arrays and objects which performs\n     * deep merges and tracks traversed objects enabling objects with circular\n     * references to be merged.\n     *\n     * @private\n     * @param {Object} object The destination object.\n     * @param {Object} source The source object.\n     * @param {string} key The key of the value to merge.\n     * @param {number} srcIndex The index of `source`.\n     * @param {Function} mergeFunc The function to merge values.\n     * @param {Function} [customizer] The function to customize assigned values.\n     * @param {Object} [stack] Tracks traversed source values and their merged\n     *  counterparts.\n     */\n    function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {\n      var objValue = safeGet(object, key),\n          srcValue = safeGet(source, key),\n          stacked = stack.get(srcValue);\n\n      if (stacked) {\n        assignMergeValue(object, key, stacked);\n        return;\n      }\n      var newValue = customizer\n        ? customizer(objValue, srcValue, (key + ''), object, source, stack)\n        : undefined;\n\n      var isCommon = newValue === undefined;\n\n      if (isCommon) {\n        var isArr = isArray(srcValue),\n            isBuff = !isArr && isBuffer(srcValue),\n            isTyped = !isArr && !isBuff && isTypedArray(srcValue);\n\n        newValue = srcValue;\n        if (isArr || isBuff || isTyped) {\n          if (isArray(objValue)) {\n            newValue = objValue;\n          }\n          else if (isArrayLikeObject(objValue)) {\n            newValue = copyArray(objValue);\n          }\n          else if (isBuff) {\n            isCommon = false;\n            newValue = cloneBuffer(srcValue, true);\n          }\n          else if (isTyped) {\n            isCommon = false;\n            newValue = cloneTypedArray(srcValue, true);\n          }\n          else {\n            newValue = [];\n          }\n        }\n        else if (isPlainObject(srcValue) || isArguments(srcValue)) {\n          newValue = objValue;\n          if (isArguments(objValue)) {\n            newValue = toPlainObject(objValue);\n          }\n          else if (!isObject(objValue) || isFunction(objValue)) {\n            newValue = initCloneObject(srcValue);\n          }\n        }\n        else {\n          isCommon = false;\n        }\n      }\n      if (isCommon) {\n        // Recursively merge objects and arrays (susceptible to call stack limits).\n        stack.set(srcValue, newValue);\n        mergeFunc(newValue, srcValue, srcIndex, customizer, stack);\n        stack['delete'](srcValue);\n      }\n      assignMergeValue(object, key, newValue);\n    }\n\n    /**\n     * The base implementation of `_.nth` which doesn't coerce arguments.\n     *\n     * @private\n     * @param {Array} array The array to query.\n     * @param {number} n The index of the element to return.\n     * @returns {*} Returns the nth element of `array`.\n     */\n    function baseNth(array, n) {\n      var length = array.length;\n      if (!length) {\n        return;\n      }\n      n += n < 0 ? length : 0;\n      return isIndex(n, length) ? array[n] : undefined;\n    }\n\n    /**\n     * The base implementation of `_.orderBy` without param guards.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by.\n     * @param {string[]} orders The sort orders of `iteratees`.\n     * @returns {Array} Returns the new sorted array.\n     */\n    function baseOrderBy(collection, iteratees, orders) {\n      if (iteratees.length) {\n        iteratees = arrayMap(iteratees, function(iteratee) {\n          if (isArray(iteratee)) {\n            return function(value) {\n              return baseGet(value, iteratee.length === 1 ? iteratee[0] : iteratee);\n            }\n          }\n          return iteratee;\n        });\n      } else {\n        iteratees = [identity];\n      }\n\n      var index = -1;\n      iteratees = arrayMap(iteratees, baseUnary(getIteratee()));\n\n      var result = baseMap(collection, function(value, key, collection) {\n        var criteria = arrayMap(iteratees, function(iteratee) {\n          return iteratee(value);\n        });\n        return { 'criteria': criteria, 'index': ++index, 'value': value };\n      });\n\n      return baseSortBy(result, function(object, other) {\n        return compareMultiple(object, other, orders);\n      });\n    }\n\n    /**\n     * The base implementation of `_.pick` without support for individual\n     * property identifiers.\n     *\n     * @private\n     * @param {Object} object The source object.\n     * @param {string[]} paths The property paths to pick.\n     * @returns {Object} Returns the new object.\n     */\n    function basePick(object, paths) {\n      return basePickBy(object, paths, function(value, path) {\n        return hasIn(object, path);\n      });\n    }\n\n    /**\n     * The base implementation of  `_.pickBy` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Object} object The source object.\n     * @param {string[]} paths The property paths to pick.\n     * @param {Function} predicate The function invoked per property.\n     * @returns {Object} Returns the new object.\n     */\n    function basePickBy(object, paths, predicate) {\n      var index = -1,\n          length = paths.length,\n          result = {};\n\n      while (++index < length) {\n        var path = paths[index],\n            value = baseGet(object, path);\n\n        if (predicate(value, path)) {\n          baseSet(result, castPath(path, object), value);\n        }\n      }\n      return result;\n    }\n\n    /**\n     * A specialized version of `baseProperty` which supports deep paths.\n     *\n     * @private\n     * @param {Array|string} path The path of the property to get.\n     * @returns {Function} Returns the new accessor function.\n     */\n    function basePropertyDeep(path) {\n      return function(object) {\n        return baseGet(object, path);\n      };\n    }\n\n    /**\n     * The base implementation of `_.pullAllBy` without support for iteratee\n     * shorthands.\n     *\n     * @private\n     * @param {Array} array The array to modify.\n     * @param {Array} values The values to remove.\n     * @param {Function} [iteratee] The iteratee invoked per element.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns `array`.\n     */\n    function basePullAll(array, values, iteratee, comparator) {\n      var indexOf = comparator ? baseIndexOfWith : baseIndexOf,\n          index = -1,\n          length = values.length,\n          seen = array;\n\n      if (array === values) {\n        values = copyArray(values);\n      }\n      if (iteratee) {\n        seen = arrayMap(array, baseUnary(iteratee));\n      }\n      while (++index < length) {\n        var fromIndex = 0,\n            value = values[index],\n            computed = iteratee ? iteratee(value) : value;\n\n        while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) {\n          if (seen !== array) {\n            splice.call(seen, fromIndex, 1);\n          }\n          splice.call(array, fromIndex, 1);\n        }\n      }\n      return array;\n    }\n\n    /**\n     * The base implementation of `_.pullAt` without support for individual\n     * indexes or capturing the removed elements.\n     *\n     * @private\n     * @param {Array} array The array to modify.\n     * @param {number[]} indexes The indexes of elements to remove.\n     * @returns {Array} Returns `array`.\n     */\n    function basePullAt(array, indexes) {\n      var length = array ? indexes.length : 0,\n          lastIndex = length - 1;\n\n      while (length--) {\n        var index = indexes[length];\n        if (length == lastIndex || index !== previous) {\n          var previous = index;\n          if (isIndex(index)) {\n            splice.call(array, index, 1);\n          } else {\n            baseUnset(array, index);\n          }\n        }\n      }\n      return array;\n    }\n\n    /**\n     * The base implementation of `_.random` without support for returning\n     * floating-point numbers.\n     *\n     * @private\n     * @param {number} lower The lower bound.\n     * @param {number} upper The upper bound.\n     * @returns {number} Returns the random number.\n     */\n    function baseRandom(lower, upper) {\n      return lower + nativeFloor(nativeRandom() * (upper - lower + 1));\n    }\n\n    /**\n     * The base implementation of `_.range` and `_.rangeRight` which doesn't\n     * coerce arguments.\n     *\n     * @private\n     * @param {number} start The start of the range.\n     * @param {number} end The end of the range.\n     * @param {number} step The value to increment or decrement by.\n     * @param {boolean} [fromRight] Specify iterating from right to left.\n     * @returns {Array} Returns the range of numbers.\n     */\n    function baseRange(start, end, step, fromRight) {\n      var index = -1,\n          length = nativeMax(nativeCeil((end - start) / (step || 1)), 0),\n          result = Array(length);\n\n      while (length--) {\n        result[fromRight ? length : ++index] = start;\n        start += step;\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.repeat` which doesn't coerce arguments.\n     *\n     * @private\n     * @param {string} string The string to repeat.\n     * @param {number} n The number of times to repeat the string.\n     * @returns {string} Returns the repeated string.\n     */\n    function baseRepeat(string, n) {\n      var result = '';\n      if (!string || n < 1 || n > MAX_SAFE_INTEGER) {\n        return result;\n      }\n      // Leverage the exponentiation by squaring algorithm for a faster repeat.\n      // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details.\n      do {\n        if (n % 2) {\n          result += string;\n        }\n        n = nativeFloor(n / 2);\n        if (n) {\n          string += string;\n        }\n      } while (n);\n\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.rest` which doesn't validate or coerce arguments.\n     *\n     * @private\n     * @param {Function} func The function to apply a rest parameter to.\n     * @param {number} [start=func.length-1] The start position of the rest parameter.\n     * @returns {Function} Returns the new function.\n     */\n    function baseRest(func, start) {\n      return setToString(overRest(func, start, identity), func + '');\n    }\n\n    /**\n     * The base implementation of `_.sample`.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to sample.\n     * @returns {*} Returns the random element.\n     */\n    function baseSample(collection) {\n      return arraySample(values(collection));\n    }\n\n    /**\n     * The base implementation of `_.sampleSize` without param guards.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to sample.\n     * @param {number} n The number of elements to sample.\n     * @returns {Array} Returns the random elements.\n     */\n    function baseSampleSize(collection, n) {\n      var array = values(collection);\n      return shuffleSelf(array, baseClamp(n, 0, array.length));\n    }\n\n    /**\n     * The base implementation of `_.set`.\n     *\n     * @private\n     * @param {Object} object The object to modify.\n     * @param {Array|string} path The path of the property to set.\n     * @param {*} value The value to set.\n     * @param {Function} [customizer] The function to customize path creation.\n     * @returns {Object} Returns `object`.\n     */\n    function baseSet(object, path, value, customizer) {\n      if (!isObject(object)) {\n        return object;\n      }\n      path = castPath(path, object);\n\n      var index = -1,\n          length = path.length,\n          lastIndex = length - 1,\n          nested = object;\n\n      while (nested != null && ++index < length) {\n        var key = toKey(path[index]),\n            newValue = value;\n\n        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n          return object;\n        }\n\n        if (index != lastIndex) {\n          var objValue = nested[key];\n          newValue = customizer ? customizer(objValue, key, nested) : undefined;\n          if (newValue === undefined) {\n            newValue = isObject(objValue)\n              ? objValue\n              : (isIndex(path[index + 1]) ? [] : {});\n          }\n        }\n        assignValue(nested, key, newValue);\n        nested = nested[key];\n      }\n      return object;\n    }\n\n    /**\n     * The base implementation of `setData` without support for hot loop shorting.\n     *\n     * @private\n     * @param {Function} func The function to associate metadata with.\n     * @param {*} data The metadata.\n     * @returns {Function} Returns `func`.\n     */\n    var baseSetData = !metaMap ? identity : function(func, data) {\n      metaMap.set(func, data);\n      return func;\n    };\n\n    /**\n     * The base implementation of `setToString` without support for hot loop shorting.\n     *\n     * @private\n     * @param {Function} func The function to modify.\n     * @param {Function} string The `toString` result.\n     * @returns {Function} Returns `func`.\n     */\n    var baseSetToString = !defineProperty ? identity : function(func, string) {\n      return defineProperty(func, 'toString', {\n        'configurable': true,\n        'enumerable': false,\n        'value': constant(string),\n        'writable': true\n      });\n    };\n\n    /**\n     * The base implementation of `_.shuffle`.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to shuffle.\n     * @returns {Array} Returns the new shuffled array.\n     */\n    function baseShuffle(collection) {\n      return shuffleSelf(values(collection));\n    }\n\n    /**\n     * The base implementation of `_.slice` without an iteratee call guard.\n     *\n     * @private\n     * @param {Array} array The array to slice.\n     * @param {number} [start=0] The start position.\n     * @param {number} [end=array.length] The end position.\n     * @returns {Array} Returns the slice of `array`.\n     */\n    function baseSlice(array, start, end) {\n      var index = -1,\n          length = array.length;\n\n      if (start < 0) {\n        start = -start > length ? 0 : (length + start);\n      }\n      end = end > length ? length : end;\n      if (end < 0) {\n        end += length;\n      }\n      length = start > end ? 0 : ((end - start) >>> 0);\n      start >>>= 0;\n\n      var result = Array(length);\n      while (++index < length) {\n        result[index] = array[index + start];\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.some` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} predicate The function invoked per iteration.\n     * @returns {boolean} Returns `true` if any element passes the predicate check,\n     *  else `false`.\n     */\n    function baseSome(collection, predicate) {\n      var result;\n\n      baseEach(collection, function(value, index, collection) {\n        result = predicate(value, index, collection);\n        return !result;\n      });\n      return !!result;\n    }\n\n    /**\n     * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which\n     * performs a binary search of `array` to determine the index at which `value`\n     * should be inserted into `array` in order to maintain its sort order.\n     *\n     * @private\n     * @param {Array} array The sorted array to inspect.\n     * @param {*} value The value to evaluate.\n     * @param {boolean} [retHighest] Specify returning the highest qualified index.\n     * @returns {number} Returns the index at which `value` should be inserted\n     *  into `array`.\n     */\n    function baseSortedIndex(array, value, retHighest) {\n      var low = 0,\n          high = array == null ? low : array.length;\n\n      if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) {\n        while (low < high) {\n          var mid = (low + high) >>> 1,\n              computed = array[mid];\n\n          if (computed !== null && !isSymbol(computed) &&\n              (retHighest ? (computed <= value) : (computed < value))) {\n            low = mid + 1;\n          } else {\n            high = mid;\n          }\n        }\n        return high;\n      }\n      return baseSortedIndexBy(array, value, identity, retHighest);\n    }\n\n    /**\n     * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy`\n     * which invokes `iteratee` for `value` and each element of `array` to compute\n     * their sort ranking. The iteratee is invoked with one argument; (value).\n     *\n     * @private\n     * @param {Array} array The sorted array to inspect.\n     * @param {*} value The value to evaluate.\n     * @param {Function} iteratee The iteratee invoked per element.\n     * @param {boolean} [retHighest] Specify returning the highest qualified index.\n     * @returns {number} Returns the index at which `value` should be inserted\n     *  into `array`.\n     */\n    function baseSortedIndexBy(array, value, iteratee, retHighest) {\n      var low = 0,\n          high = array == null ? 0 : array.length;\n      if (high === 0) {\n        return 0;\n      }\n\n      value = iteratee(value);\n      var valIsNaN = value !== value,\n          valIsNull = value === null,\n          valIsSymbol = isSymbol(value),\n          valIsUndefined = value === undefined;\n\n      while (low < high) {\n        var mid = nativeFloor((low + high) / 2),\n            computed = iteratee(array[mid]),\n            othIsDefined = computed !== undefined,\n            othIsNull = computed === null,\n            othIsReflexive = computed === computed,\n            othIsSymbol = isSymbol(computed);\n\n        if (valIsNaN) {\n          var setLow = retHighest || othIsReflexive;\n        } else if (valIsUndefined) {\n          setLow = othIsReflexive && (retHighest || othIsDefined);\n        } else if (valIsNull) {\n          setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);\n        } else if (valIsSymbol) {\n          setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol);\n        } else if (othIsNull || othIsSymbol) {\n          setLow = false;\n        } else {\n          setLow = retHighest ? (computed <= value) : (computed < value);\n        }\n        if (setLow) {\n          low = mid + 1;\n        } else {\n          high = mid;\n        }\n      }\n      return nativeMin(high, MAX_ARRAY_INDEX);\n    }\n\n    /**\n     * The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without\n     * support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array} array The array to inspect.\n     * @param {Function} [iteratee] The iteratee invoked per element.\n     * @returns {Array} Returns the new duplicate free array.\n     */\n    function baseSortedUniq(array, iteratee) {\n      var index = -1,\n          length = array.length,\n          resIndex = 0,\n          result = [];\n\n      while (++index < length) {\n        var value = array[index],\n            computed = iteratee ? iteratee(value) : value;\n\n        if (!index || !eq(computed, seen)) {\n          var seen = computed;\n          result[resIndex++] = value === 0 ? 0 : value;\n        }\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.toNumber` which doesn't ensure correct\n     * conversions of binary, hexadecimal, or octal string values.\n     *\n     * @private\n     * @param {*} value The value to process.\n     * @returns {number} Returns the number.\n     */\n    function baseToNumber(value) {\n      if (typeof value == 'number') {\n        return value;\n      }\n      if (isSymbol(value)) {\n        return NAN;\n      }\n      return +value;\n    }\n\n    /**\n     * The base implementation of `_.toString` which doesn't convert nullish\n     * values to empty strings.\n     *\n     * @private\n     * @param {*} value The value to process.\n     * @returns {string} Returns the string.\n     */\n    function baseToString(value) {\n      // Exit early for strings to avoid a performance hit in some environments.\n      if (typeof value == 'string') {\n        return value;\n      }\n      if (isArray(value)) {\n        // Recursively convert values (susceptible to call stack limits).\n        return arrayMap(value, baseToString) + '';\n      }\n      if (isSymbol(value)) {\n        return symbolToString ? symbolToString.call(value) : '';\n      }\n      var result = (value + '');\n      return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;\n    }\n\n    /**\n     * The base implementation of `_.uniqBy` without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array} array The array to inspect.\n     * @param {Function} [iteratee] The iteratee invoked per element.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new duplicate free array.\n     */\n    function baseUniq(array, iteratee, comparator) {\n      var index = -1,\n          includes = arrayIncludes,\n          length = array.length,\n          isCommon = true,\n          result = [],\n          seen = result;\n\n      if (comparator) {\n        isCommon = false;\n        includes = arrayIncludesWith;\n      }\n      else if (length >= LARGE_ARRAY_SIZE) {\n        var set = iteratee ? null : createSet(array);\n        if (set) {\n          return setToArray(set);\n        }\n        isCommon = false;\n        includes = cacheHas;\n        seen = new SetCache;\n      }\n      else {\n        seen = iteratee ? [] : result;\n      }\n      outer:\n      while (++index < length) {\n        var value = array[index],\n            computed = iteratee ? iteratee(value) : value;\n\n        value = (comparator || value !== 0) ? value : 0;\n        if (isCommon && computed === computed) {\n          var seenIndex = seen.length;\n          while (seenIndex--) {\n            if (seen[seenIndex] === computed) {\n              continue outer;\n            }\n          }\n          if (iteratee) {\n            seen.push(computed);\n          }\n          result.push(value);\n        }\n        else if (!includes(seen, computed, comparator)) {\n          if (seen !== result) {\n            seen.push(computed);\n          }\n          result.push(value);\n        }\n      }\n      return result;\n    }\n\n    /**\n     * The base implementation of `_.unset`.\n     *\n     * @private\n     * @param {Object} object The object to modify.\n     * @param {Array|string} path The property path to unset.\n     * @returns {boolean} Returns `true` if the property is deleted, else `false`.\n     */\n    function baseUnset(object, path) {\n      path = castPath(path, object);\n      object = parent(object, path);\n      return object == null || delete object[toKey(last(path))];\n    }\n\n    /**\n     * The base implementation of `_.update`.\n     *\n     * @private\n     * @param {Object} object The object to modify.\n     * @param {Array|string} path The path of the property to update.\n     * @param {Function} updater The function to produce the updated value.\n     * @param {Function} [customizer] The function to customize path creation.\n     * @returns {Object} Returns `object`.\n     */\n    function baseUpdate(object, path, updater, customizer) {\n      return baseSet(object, path, updater(baseGet(object, path)), customizer);\n    }\n\n    /**\n     * The base implementation of methods like `_.dropWhile` and `_.takeWhile`\n     * without support for iteratee shorthands.\n     *\n     * @private\n     * @param {Array} array The array to query.\n     * @param {Function} predicate The function invoked per iteration.\n     * @param {boolean} [isDrop] Specify dropping elements instead of taking them.\n     * @param {boolean} [fromRight] Specify iterating from right to left.\n     * @returns {Array} Returns the slice of `array`.\n     */\n    function baseWhile(array, predicate, isDrop, fromRight) {\n      var length = array.length,\n          index = fromRight ? length : -1;\n\n      while ((fromRight ? index-- : ++index < length) &&\n        predicate(array[index], index, array)) {}\n\n      return isDrop\n        ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length))\n        : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index));\n    }\n\n    /**\n     * The base implementation of `wrapperValue` which returns the result of\n     * performing a sequence of actions on the unwrapped `value`, where each\n     * successive action is supplied the return value of the previous.\n     *\n     * @private\n     * @param {*} value The unwrapped value.\n     * @param {Array} actions Actions to perform to resolve the unwrapped value.\n     * @returns {*} Returns the resolved value.\n     */\n    function baseWrapperValue(value, actions) {\n      var result = value;\n      if (result instanceof LazyWrapper) {\n        result = result.value();\n      }\n      return arrayReduce(actions, function(result, action) {\n        return action.func.apply(action.thisArg, arrayPush([result], action.args));\n      }, result);\n    }\n\n    /**\n     * The base implementation of methods like `_.xor`, without support for\n     * iteratee shorthands, that accepts an array of arrays to inspect.\n     *\n     * @private\n     * @param {Array} arrays The arrays to inspect.\n     * @param {Function} [iteratee] The iteratee invoked per element.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new array of values.\n     */\n    function baseXor(arrays, iteratee, comparator) {\n      var length = arrays.length;\n      if (length < 2) {\n        return length ? baseUniq(arrays[0]) : [];\n      }\n      var index = -1,\n          result = Array(length);\n\n      while (++index < length) {\n        var array = arrays[index],\n            othIndex = -1;\n\n        while (++othIndex < length) {\n          if (othIndex != index) {\n            result[index] = baseDifference(result[index] || array, arrays[othIndex], iteratee, comparator);\n          }\n        }\n      }\n      return baseUniq(baseFlatten(result, 1), iteratee, comparator);\n    }\n\n    /**\n     * This base implementation of `_.zipObject` which assigns values using `assignFunc`.\n     *\n     * @private\n     * @param {Array} props The property identifiers.\n     * @param {Array} values The property values.\n     * @param {Function} assignFunc The function to assign values.\n     * @returns {Object} Returns the new object.\n     */\n    function baseZipObject(props, values, assignFunc) {\n      var index = -1,\n          length = props.length,\n          valsLength = values.length,\n          result = {};\n\n      while (++index < length) {\n        var value = index < valsLength ? values[index] : undefined;\n        assignFunc(result, props[index], value);\n      }\n      return result;\n    }\n\n    /**\n     * Casts `value` to an empty array if it's not an array like object.\n     *\n     * @private\n     * @param {*} value The value to inspect.\n     * @returns {Array|Object} Returns the cast array-like object.\n     */\n    function castArrayLikeObject(value) {\n      return isArrayLikeObject(value) ? value : [];\n    }\n\n    /**\n     * Casts `value` to `identity` if it's not a function.\n     *\n     * @private\n     * @param {*} value The value to inspect.\n     * @returns {Function} Returns cast function.\n     */\n    function castFunction(value) {\n      return typeof value == 'function' ? value : identity;\n    }\n\n    /**\n     * Casts `value` to a path array if it's not one.\n     *\n     * @private\n     * @param {*} value The value to inspect.\n     * @param {Object} [object] The object to query keys on.\n     * @returns {Array} Returns the cast property path array.\n     */\n    function castPath(value, object) {\n      if (isArray(value)) {\n        return value;\n      }\n      return isKey(value, object) ? [value] : stringToPath(toString(value));\n    }\n\n    /**\n     * A `baseRest` alias which can be replaced with `identity` by module\n     * replacement plugins.\n     *\n     * @private\n     * @type {Function}\n     * @param {Function} func The function to apply a rest parameter to.\n     * @returns {Function} Returns the new function.\n     */\n    var castRest = baseRest;\n\n    /**\n     * Casts `array` to a slice if it's needed.\n     *\n     * @private\n     * @param {Array} array The array to inspect.\n     * @param {number} start The start position.\n     * @param {number} [end=array.length] The end position.\n     * @returns {Array} Returns the cast slice.\n     */\n    function castSlice(array, start, end) {\n      var length = array.length;\n      end = end === undefined ? length : end;\n      return (!start && end >= length) ? array : baseSlice(array, start, end);\n    }\n\n    /**\n     * A simple wrapper around the global [`clearTimeout`](https://mdn.io/clearTimeout).\n     *\n     * @private\n     * @param {number|Object} id The timer id or timeout object of the timer to clear.\n     */\n    var clearTimeout = ctxClearTimeout || function(id) {\n      return root.clearTimeout(id);\n    };\n\n    /**\n     * Creates a clone of  `buffer`.\n     *\n     * @private\n     * @param {Buffer} buffer The buffer to clone.\n     * @param {boolean} [isDeep] Specify a deep clone.\n     * @returns {Buffer} Returns the cloned buffer.\n     */\n    function cloneBuffer(buffer, isDeep) {\n      if (isDeep) {\n        return buffer.slice();\n      }\n      var length = buffer.length,\n          result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);\n\n      buffer.copy(result);\n      return result;\n    }\n\n    /**\n     * Creates a clone of `arrayBuffer`.\n     *\n     * @private\n     * @param {ArrayBuffer} arrayBuffer The array buffer to clone.\n     * @returns {ArrayBuffer} Returns the cloned array buffer.\n     */\n    function cloneArrayBuffer(arrayBuffer) {\n      var result = new arrayBuffer.constructor(arrayBuffer.byteLength);\n      new Uint8Array(result).set(new Uint8Array(arrayBuffer));\n      return result;\n    }\n\n    /**\n     * Creates a clone of `dataView`.\n     *\n     * @private\n     * @param {Object} dataView The data view to clone.\n     * @param {boolean} [isDeep] Specify a deep clone.\n     * @returns {Object} Returns the cloned data view.\n     */\n    function cloneDataView(dataView, isDeep) {\n      var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;\n      return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);\n    }\n\n    /**\n     * Creates a clone of `regexp`.\n     *\n     * @private\n     * @param {Object} regexp The regexp to clone.\n     * @returns {Object} Returns the cloned regexp.\n     */\n    function cloneRegExp(regexp) {\n      var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));\n      result.lastIndex = regexp.lastIndex;\n      return result;\n    }\n\n    /**\n     * Creates a clone of the `symbol` object.\n     *\n     * @private\n     * @param {Object} symbol The symbol object to clone.\n     * @returns {Object} Returns the cloned symbol object.\n     */\n    function cloneSymbol(symbol) {\n      return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};\n    }\n\n    /**\n     * Creates a clone of `typedArray`.\n     *\n     * @private\n     * @param {Object} typedArray The typed array to clone.\n     * @param {boolean} [isDeep] Specify a deep clone.\n     * @returns {Object} Returns the cloned typed array.\n     */\n    function cloneTypedArray(typedArray, isDeep) {\n      var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;\n      return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);\n    }\n\n    /**\n     * Compares values to sort them in ascending order.\n     *\n     * @private\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {number} Returns the sort order indicator for `value`.\n     */\n    function compareAscending(value, other) {\n      if (value !== other) {\n        var valIsDefined = value !== undefined,\n            valIsNull = value === null,\n            valIsReflexive = value === value,\n            valIsSymbol = isSymbol(value);\n\n        var othIsDefined = other !== undefined,\n            othIsNull = other === null,\n            othIsReflexive = other === other,\n            othIsSymbol = isSymbol(other);\n\n        if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) ||\n            (valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) ||\n            (valIsNull && othIsDefined && othIsReflexive) ||\n            (!valIsDefined && othIsReflexive) ||\n            !valIsReflexive) {\n          return 1;\n        }\n        if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) ||\n            (othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) ||\n            (othIsNull && valIsDefined && valIsReflexive) ||\n            (!othIsDefined && valIsReflexive) ||\n            !othIsReflexive) {\n          return -1;\n        }\n      }\n      return 0;\n    }\n\n    /**\n     * Used by `_.orderBy` to compare multiple properties of a value to another\n     * and stable sort them.\n     *\n     * If `orders` is unspecified, all values are sorted in ascending order. Otherwise,\n     * specify an order of \"desc\" for descending or \"asc\" for ascending sort order\n     * of corresponding values.\n     *\n     * @private\n     * @param {Object} object The object to compare.\n     * @param {Object} other The other object to compare.\n     * @param {boolean[]|string[]} orders The order to sort by for each property.\n     * @returns {number} Returns the sort order indicator for `object`.\n     */\n    function compareMultiple(object, other, orders) {\n      var index = -1,\n          objCriteria = object.criteria,\n          othCriteria = other.criteria,\n          length = objCriteria.length,\n          ordersLength = orders.length;\n\n      while (++index < length) {\n        var result = compareAscending(objCriteria[index], othCriteria[index]);\n        if (result) {\n          if (index >= ordersLength) {\n            return result;\n          }\n          var order = orders[index];\n          return result * (order == 'desc' ? -1 : 1);\n        }\n      }\n      // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications\n      // that causes it, under certain circumstances, to provide the same value for\n      // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247\n      // for more details.\n      //\n      // This also ensures a stable sort in V8 and other engines.\n      // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details.\n      return object.index - other.index;\n    }\n\n    /**\n     * Creates an array that is the composition of partially applied arguments,\n     * placeholders, and provided arguments into a single array of arguments.\n     *\n     * @private\n     * @param {Array} args The provided arguments.\n     * @param {Array} partials The arguments to prepend to those provided.\n     * @param {Array} holders The `partials` placeholder indexes.\n     * @params {boolean} [isCurried] Specify composing for a curried function.\n     * @returns {Array} Returns the new array of composed arguments.\n     */\n    function composeArgs(args, partials, holders, isCurried) {\n      var argsIndex = -1,\n          argsLength = args.length,\n          holdersLength = holders.length,\n          leftIndex = -1,\n          leftLength = partials.length,\n          rangeLength = nativeMax(argsLength - holdersLength, 0),\n          result = Array(leftLength + rangeLength),\n          isUncurried = !isCurried;\n\n      while (++leftIndex < leftLength) {\n        result[leftIndex] = partials[leftIndex];\n      }\n      while (++argsIndex < holdersLength) {\n        if (isUncurried || argsIndex < argsLength) {\n          result[holders[argsIndex]] = args[argsIndex];\n        }\n      }\n      while (rangeLength--) {\n        result[leftIndex++] = args[argsIndex++];\n      }\n      return result;\n    }\n\n    /**\n     * This function is like `composeArgs` except that the arguments composition\n     * is tailored for `_.partialRight`.\n     *\n     * @private\n     * @param {Array} args The provided arguments.\n     * @param {Array} partials The arguments to append to those provided.\n     * @param {Array} holders The `partials` placeholder indexes.\n     * @params {boolean} [isCurried] Specify composing for a curried function.\n     * @returns {Array} Returns the new array of composed arguments.\n     */\n    function composeArgsRight(args, partials, holders, isCurried) {\n      var argsIndex = -1,\n          argsLength = args.length,\n          holdersIndex = -1,\n          holdersLength = holders.length,\n          rightIndex = -1,\n          rightLength = partials.length,\n          rangeLength = nativeMax(argsLength - holdersLength, 0),\n          result = Array(rangeLength + rightLength),\n          isUncurried = !isCurried;\n\n      while (++argsIndex < rangeLength) {\n        result[argsIndex] = args[argsIndex];\n      }\n      var offset = argsIndex;\n      while (++rightIndex < rightLength) {\n        result[offset + rightIndex] = partials[rightIndex];\n      }\n      while (++holdersIndex < holdersLength) {\n        if (isUncurried || argsIndex < argsLength) {\n          result[offset + holders[holdersIndex]] = args[argsIndex++];\n        }\n      }\n      return result;\n    }\n\n    /**\n     * Copies the values of `source` to `array`.\n     *\n     * @private\n     * @param {Array} source The array to copy values from.\n     * @param {Array} [array=[]] The array to copy values to.\n     * @returns {Array} Returns `array`.\n     */\n    function copyArray(source, array) {\n      var index = -1,\n          length = source.length;\n\n      array || (array = Array(length));\n      while (++index < length) {\n        array[index] = source[index];\n      }\n      return array;\n    }\n\n    /**\n     * Copies properties of `source` to `object`.\n     *\n     * @private\n     * @param {Object} source The object to copy properties from.\n     * @param {Array} props The property identifiers to copy.\n     * @param {Object} [object={}] The object to copy properties to.\n     * @param {Function} [customizer] The function to customize copied values.\n     * @returns {Object} Returns `object`.\n     */\n    function copyObject(source, props, object, customizer) {\n      var isNew = !object;\n      object || (object = {});\n\n      var index = -1,\n          length = props.length;\n\n      while (++index < length) {\n        var key = props[index];\n\n        var newValue = customizer\n          ? customizer(object[key], source[key], key, object, source)\n          : undefined;\n\n        if (newValue === undefined) {\n          newValue = source[key];\n        }\n        if (isNew) {\n          baseAssignValue(object, key, newValue);\n        } else {\n          assignValue(object, key, newValue);\n        }\n      }\n      return object;\n    }\n\n    /**\n     * Copies own symbols of `source` to `object`.\n     *\n     * @private\n     * @param {Object} source The object to copy symbols from.\n     * @param {Object} [object={}] The object to copy symbols to.\n     * @returns {Object} Returns `object`.\n     */\n    function copySymbols(source, object) {\n      return copyObject(source, getSymbols(source), object);\n    }\n\n    /**\n     * Copies own and inherited symbols of `source` to `object`.\n     *\n     * @private\n     * @param {Object} source The object to copy symbols from.\n     * @param {Object} [object={}] The object to copy symbols to.\n     * @returns {Object} Returns `object`.\n     */\n    function copySymbolsIn(source, object) {\n      return copyObject(source, getSymbolsIn(source), object);\n    }\n\n    /**\n     * Creates a function like `_.groupBy`.\n     *\n     * @private\n     * @param {Function} setter The function to set accumulator values.\n     * @param {Function} [initializer] The accumulator object initializer.\n     * @returns {Function} Returns the new aggregator function.\n     */\n    function createAggregator(setter, initializer) {\n      return function(collection, iteratee) {\n        var func = isArray(collection) ? arrayAggregator : baseAggregator,\n            accumulator = initializer ? initializer() : {};\n\n        return func(collection, setter, getIteratee(iteratee, 2), accumulator);\n      };\n    }\n\n    /**\n     * Creates a function like `_.assign`.\n     *\n     * @private\n     * @param {Function} assigner The function to assign values.\n     * @returns {Function} Returns the new assigner function.\n     */\n    function createAssigner(assigner) {\n      return baseRest(function(object, sources) {\n        var index = -1,\n            length = sources.length,\n            customizer = length > 1 ? sources[length - 1] : undefined,\n            guard = length > 2 ? sources[2] : undefined;\n\n        customizer = (assigner.length > 3 && typeof customizer == 'function')\n          ? (length--, customizer)\n          : undefined;\n\n        if (guard && isIterateeCall(sources[0], sources[1], guard)) {\n          customizer = length < 3 ? undefined : customizer;\n          length = 1;\n        }\n        object = Object(object);\n        while (++index < length) {\n          var source = sources[index];\n          if (source) {\n            assigner(object, source, index, customizer);\n          }\n        }\n        return object;\n      });\n    }\n\n    /**\n     * Creates a `baseEach` or `baseEachRight` function.\n     *\n     * @private\n     * @param {Function} eachFunc The function to iterate over a collection.\n     * @param {boolean} [fromRight] Specify iterating from right to left.\n     * @returns {Function} Returns the new base function.\n     */\n    function createBaseEach(eachFunc, fromRight) {\n      return function(collection, iteratee) {\n        if (collection == null) {\n          return collection;\n        }\n        if (!isArrayLike(collection)) {\n          return eachFunc(collection, iteratee);\n        }\n        var length = collection.length,\n            index = fromRight ? length : -1,\n            iterable = Object(collection);\n\n        while ((fromRight ? index-- : ++index < length)) {\n          if (iteratee(iterable[index], index, iterable) === false) {\n            break;\n          }\n        }\n        return collection;\n      };\n    }\n\n    /**\n     * Creates a base function for methods like `_.forIn` and `_.forOwn`.\n     *\n     * @private\n     * @param {boolean} [fromRight] Specify iterating from right to left.\n     * @returns {Function} Returns the new base function.\n     */\n    function createBaseFor(fromRight) {\n      return function(object, iteratee, keysFunc) {\n        var index = -1,\n            iterable = Object(object),\n            props = keysFunc(object),\n            length = props.length;\n\n        while (length--) {\n          var key = props[fromRight ? length : ++index];\n          if (iteratee(iterable[key], key, iterable) === false) {\n            break;\n          }\n        }\n        return object;\n      };\n    }\n\n    /**\n     * Creates a function that wraps `func` to invoke it with the optional `this`\n     * binding of `thisArg`.\n     *\n     * @private\n     * @param {Function} func The function to wrap.\n     * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n     * @param {*} [thisArg] The `this` binding of `func`.\n     * @returns {Function} Returns the new wrapped function.\n     */\n    function createBind(func, bitmask, thisArg) {\n      var isBind = bitmask & WRAP_BIND_FLAG,\n          Ctor = createCtor(func);\n\n      function wrapper() {\n        var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;\n        return fn.apply(isBind ? thisArg : this, arguments);\n      }\n      return wrapper;\n    }\n\n    /**\n     * Creates a function like `_.lowerFirst`.\n     *\n     * @private\n     * @param {string} methodName The name of the `String` case method to use.\n     * @returns {Function} Returns the new case function.\n     */\n    function createCaseFirst(methodName) {\n      return function(string) {\n        string = toString(string);\n\n        var strSymbols = hasUnicode(string)\n          ? stringToArray(string)\n          : undefined;\n\n        var chr = strSymbols\n          ? strSymbols[0]\n          : string.charAt(0);\n\n        var trailing = strSymbols\n          ? castSlice(strSymbols, 1).join('')\n          : string.slice(1);\n\n        return chr[methodName]() + trailing;\n      };\n    }\n\n    /**\n     * Creates a function like `_.camelCase`.\n     *\n     * @private\n     * @param {Function} callback The function to combine each word.\n     * @returns {Function} Returns the new compounder function.\n     */\n    function createCompounder(callback) {\n      return function(string) {\n        return arrayReduce(words(deburr(string).replace(reApos, '')), callback, '');\n      };\n    }\n\n    /**\n     * Creates a function that produces an instance of `Ctor` regardless of\n     * whether it was invoked as part of a `new` expression or by `call` or `apply`.\n     *\n     * @private\n     * @param {Function} Ctor The constructor to wrap.\n     * @returns {Function} Returns the new wrapped function.\n     */\n    function createCtor(Ctor) {\n      return function() {\n        // Use a `switch` statement to work with class constructors. See\n        // http://ecma-international.org/ecma-262/7.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist\n        // for more details.\n        var args = arguments;\n        switch (args.length) {\n          case 0: return new Ctor;\n          case 1: return new Ctor(args[0]);\n          case 2: return new Ctor(args[0], args[1]);\n          case 3: return new Ctor(args[0], args[1], args[2]);\n          case 4: return new Ctor(args[0], args[1], args[2], args[3]);\n          case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]);\n          case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]);\n          case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);\n        }\n        var thisBinding = baseCreate(Ctor.prototype),\n            result = Ctor.apply(thisBinding, args);\n\n        // Mimic the constructor's `return` behavior.\n        // See https://es5.github.io/#x13.2.2 for more details.\n        return isObject(result) ? result : thisBinding;\n      };\n    }\n\n    /**\n     * Creates a function that wraps `func` to enable currying.\n     *\n     * @private\n     * @param {Function} func The function to wrap.\n     * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n     * @param {number} arity The arity of `func`.\n     * @returns {Function} Returns the new wrapped function.\n     */\n    function createCurry(func, bitmask, arity) {\n      var Ctor = createCtor(func);\n\n      function wrapper() {\n        var length = arguments.length,\n            args = Array(length),\n            index = length,\n            placeholder = getHolder(wrapper);\n\n        while (index--) {\n          args[index] = arguments[index];\n        }\n        var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder)\n          ? []\n          : replaceHolders(args, placeholder);\n\n        length -= holders.length;\n        if (length < arity) {\n          return createRecurry(\n            func, bitmask, createHybrid, wrapper.placeholder, undefined,\n            args, holders, undefined, undefined, arity - length);\n        }\n        var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;\n        return apply(fn, this, args);\n      }\n      return wrapper;\n    }\n\n    /**\n     * Creates a `_.find` or `_.findLast` function.\n     *\n     * @private\n     * @param {Function} findIndexFunc The function to find the collection index.\n     * @returns {Function} Returns the new find function.\n     */\n    function createFind(findIndexFunc) {\n      return function(collection, predicate, fromIndex) {\n        var iterable = Object(collection);\n        if (!isArrayLike(collection)) {\n          var iteratee = getIteratee(predicate, 3);\n          collection = keys(collection);\n          predicate = function(key) { return iteratee(iterable[key], key, iterable); };\n        }\n        var index = findIndexFunc(collection, predicate, fromIndex);\n        return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined;\n      };\n    }\n\n    /**\n     * Creates a `_.flow` or `_.flowRight` function.\n     *\n     * @private\n     * @param {boolean} [fromRight] Specify iterating from right to left.\n     * @returns {Function} Returns the new flow function.\n     */\n    function createFlow(fromRight) {\n      return flatRest(function(funcs) {\n        var length = funcs.length,\n            index = length,\n            prereq = LodashWrapper.prototype.thru;\n\n        if (fromRight) {\n          funcs.reverse();\n        }\n        while (index--) {\n          var func = funcs[index];\n          if (typeof func != 'function') {\n            throw new TypeError(FUNC_ERROR_TEXT);\n          }\n          if (prereq && !wrapper && getFuncName(func) == 'wrapper') {\n            var wrapper = new LodashWrapper([], true);\n          }\n        }\n        index = wrapper ? index : length;\n        while (++index < length) {\n          func = funcs[index];\n\n          var funcName = getFuncName(func),\n              data = funcName == 'wrapper' ? getData(func) : undefined;\n\n          if (data && isLaziable(data[0]) &&\n                data[1] == (WRAP_ARY_FLAG | WRAP_CURRY_FLAG | WRAP_PARTIAL_FLAG | WRAP_REARG_FLAG) &&\n                !data[4].length && data[9] == 1\n              ) {\n            wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]);\n          } else {\n            wrapper = (func.length == 1 && isLaziable(func))\n              ? wrapper[funcName]()\n              : wrapper.thru(func);\n          }\n        }\n        return function() {\n          var args = arguments,\n              value = args[0];\n\n          if (wrapper && args.length == 1 && isArray(value)) {\n            return wrapper.plant(value).value();\n          }\n          var index = 0,\n              result = length ? funcs[index].apply(this, args) : value;\n\n          while (++index < length) {\n            result = funcs[index].call(this, result);\n          }\n          return result;\n        };\n      });\n    }\n\n    /**\n     * Creates a function that wraps `func` to invoke it with optional `this`\n     * binding of `thisArg`, partial application, and currying.\n     *\n     * @private\n     * @param {Function|string} func The function or method name to wrap.\n     * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n     * @param {*} [thisArg] The `this` binding of `func`.\n     * @param {Array} [partials] The arguments to prepend to those provided to\n     *  the new function.\n     * @param {Array} [holders] The `partials` placeholder indexes.\n     * @param {Array} [partialsRight] The arguments to append to those provided\n     *  to the new function.\n     * @param {Array} [holdersRight] The `partialsRight` placeholder indexes.\n     * @param {Array} [argPos] The argument positions of the new function.\n     * @param {number} [ary] The arity cap of `func`.\n     * @param {number} [arity] The arity of `func`.\n     * @returns {Function} Returns the new wrapped function.\n     */\n    function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {\n      var isAry = bitmask & WRAP_ARY_FLAG,\n          isBind = bitmask & WRAP_BIND_FLAG,\n          isBindKey = bitmask & WRAP_BIND_KEY_FLAG,\n          isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG),\n          isFlip = bitmask & WRAP_FLIP_FLAG,\n          Ctor = isBindKey ? undefined : createCtor(func);\n\n      function wrapper() {\n        var length = arguments.length,\n            args = Array(length),\n            index = length;\n\n        while (index--) {\n          args[index] = arguments[index];\n        }\n        if (isCurried) {\n          var placeholder = getHolder(wrapper),\n              holdersCount = countHolders(args, placeholder);\n        }\n        if (partials) {\n          args = composeArgs(args, partials, holders, isCurried);\n        }\n        if (partialsRight) {\n          args = composeArgsRight(args, partialsRight, holdersRight, isCurried);\n        }\n        length -= holdersCount;\n        if (isCurried && length < arity) {\n          var newHolders = replaceHolders(args, placeholder);\n          return createRecurry(\n            func, bitmask, createHybrid, wrapper.placeholder, thisArg,\n            args, newHolders, argPos, ary, arity - length\n          );\n        }\n        var thisBinding = isBind ? thisArg : this,\n            fn = isBindKey ? thisBinding[func] : func;\n\n        length = args.length;\n        if (argPos) {\n          args = reorder(args, argPos);\n        } else if (isFlip && length > 1) {\n          args.reverse();\n        }\n        if (isAry && ary < length) {\n          args.length = ary;\n        }\n        if (this && this !== root && this instanceof wrapper) {\n          fn = Ctor || createCtor(fn);\n        }\n        return fn.apply(thisBinding, args);\n      }\n      return wrapper;\n    }\n\n    /**\n     * Creates a function like `_.invertBy`.\n     *\n     * @private\n     * @param {Function} setter The function to set accumulator values.\n     * @param {Function} toIteratee The function to resolve iteratees.\n     * @returns {Function} Returns the new inverter function.\n     */\n    function createInverter(setter, toIteratee) {\n      return function(object, iteratee) {\n        return baseInverter(object, setter, toIteratee(iteratee), {});\n      };\n    }\n\n    /**\n     * Creates a function that performs a mathematical operation on two values.\n     *\n     * @private\n     * @param {Function} operator The function to perform the operation.\n     * @param {number} [defaultValue] The value used for `undefined` arguments.\n     * @returns {Function} Returns the new mathematical operation function.\n     */\n    function createMathOperation(operator, defaultValue) {\n      return function(value, other) {\n        var result;\n        if (value === undefined && other === undefined) {\n          return defaultValue;\n        }\n        if (value !== undefined) {\n          result = value;\n        }\n        if (other !== undefined) {\n          if (result === undefined) {\n            return other;\n          }\n          if (typeof value == 'string' || typeof other == 'string') {\n            value = baseToString(value);\n            other = baseToString(other);\n          } else {\n            value = baseToNumber(value);\n            other = baseToNumber(other);\n          }\n          result = operator(value, other);\n        }\n        return result;\n      };\n    }\n\n    /**\n     * Creates a function like `_.over`.\n     *\n     * @private\n     * @param {Function} arrayFunc The function to iterate over iteratees.\n     * @returns {Function} Returns the new over function.\n     */\n    function createOver(arrayFunc) {\n      return flatRest(function(iteratees) {\n        iteratees = arrayMap(iteratees, baseUnary(getIteratee()));\n        return baseRest(function(args) {\n          var thisArg = this;\n          return arrayFunc(iteratees, function(iteratee) {\n            return apply(iteratee, thisArg, args);\n          });\n        });\n      });\n    }\n\n    /**\n     * Creates the padding for `string` based on `length`. The `chars` string\n     * is truncated if the number of characters exceeds `length`.\n     *\n     * @private\n     * @param {number} length The padding length.\n     * @param {string} [chars=' '] The string used as padding.\n     * @returns {string} Returns the padding for `string`.\n     */\n    function createPadding(length, chars) {\n      chars = chars === undefined ? ' ' : baseToString(chars);\n\n      var charsLength = chars.length;\n      if (charsLength < 2) {\n        return charsLength ? baseRepeat(chars, length) : chars;\n      }\n      var result = baseRepeat(chars, nativeCeil(length / stringSize(chars)));\n      return hasUnicode(chars)\n        ? castSlice(stringToArray(result), 0, length).join('')\n        : result.slice(0, length);\n    }\n\n    /**\n     * Creates a function that wraps `func` to invoke it with the `this` binding\n     * of `thisArg` and `partials` prepended to the arguments it receives.\n     *\n     * @private\n     * @param {Function} func The function to wrap.\n     * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n     * @param {*} thisArg The `this` binding of `func`.\n     * @param {Array} partials The arguments to prepend to those provided to\n     *  the new function.\n     * @returns {Function} Returns the new wrapped function.\n     */\n    function createPartial(func, bitmask, thisArg, partials) {\n      var isBind = bitmask & WRAP_BIND_FLAG,\n          Ctor = createCtor(func);\n\n      function wrapper() {\n        var argsIndex = -1,\n            argsLength = arguments.length,\n            leftIndex = -1,\n            leftLength = partials.length,\n            args = Array(leftLength + argsLength),\n            fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;\n\n        while (++leftIndex < leftLength) {\n          args[leftIndex] = partials[leftIndex];\n        }\n        while (argsLength--) {\n          args[leftIndex++] = arguments[++argsIndex];\n        }\n        return apply(fn, isBind ? thisArg : this, args);\n      }\n      return wrapper;\n    }\n\n    /**\n     * Creates a `_.range` or `_.rangeRight` function.\n     *\n     * @private\n     * @param {boolean} [fromRight] Specify iterating from right to left.\n     * @returns {Function} Returns the new range function.\n     */\n    function createRange(fromRight) {\n      return function(start, end, step) {\n        if (step && typeof step != 'number' && isIterateeCall(start, end, step)) {\n          end = step = undefined;\n        }\n        // Ensure the sign of `-0` is preserved.\n        start = toFinite(start);\n        if (end === undefined) {\n          end = start;\n          start = 0;\n        } else {\n          end = toFinite(end);\n        }\n        step = step === undefined ? (start < end ? 1 : -1) : toFinite(step);\n        return baseRange(start, end, step, fromRight);\n      };\n    }\n\n    /**\n     * Creates a function that performs a relational operation on two values.\n     *\n     * @private\n     * @param {Function} operator The function to perform the operation.\n     * @returns {Function} Returns the new relational operation function.\n     */\n    function createRelationalOperation(operator) {\n      return function(value, other) {\n        if (!(typeof value == 'string' && typeof other == 'string')) {\n          value = toNumber(value);\n          other = toNumber(other);\n        }\n        return operator(value, other);\n      };\n    }\n\n    /**\n     * Creates a function that wraps `func` to continue currying.\n     *\n     * @private\n     * @param {Function} func The function to wrap.\n     * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n     * @param {Function} wrapFunc The function to create the `func` wrapper.\n     * @param {*} placeholder The placeholder value.\n     * @param {*} [thisArg] The `this` binding of `func`.\n     * @param {Array} [partials] The arguments to prepend to those provided to\n     *  the new function.\n     * @param {Array} [holders] The `partials` placeholder indexes.\n     * @param {Array} [argPos] The argument positions of the new function.\n     * @param {number} [ary] The arity cap of `func`.\n     * @param {number} [arity] The arity of `func`.\n     * @returns {Function} Returns the new wrapped function.\n     */\n    function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {\n      var isCurry = bitmask & WRAP_CURRY_FLAG,\n          newHolders = isCurry ? holders : undefined,\n          newHoldersRight = isCurry ? undefined : holders,\n          newPartials = isCurry ? partials : undefined,\n          newPartialsRight = isCurry ? undefined : partials;\n\n      bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG);\n      bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG);\n\n      if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) {\n        bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG);\n      }\n      var newData = [\n        func, bitmask, thisArg, newPartials, newHolders, newPartialsRight,\n        newHoldersRight, argPos, ary, arity\n      ];\n\n      var result = wrapFunc.apply(undefined, newData);\n      if (isLaziable(func)) {\n        setData(result, newData);\n      }\n      result.placeholder = placeholder;\n      return setWrapToString(result, func, bitmask);\n    }\n\n    /**\n     * Creates a function like `_.round`.\n     *\n     * @private\n     * @param {string} methodName The name of the `Math` method to use when rounding.\n     * @returns {Function} Returns the new round function.\n     */\n    function createRound(methodName) {\n      var func = Math[methodName];\n      return function(number, precision) {\n        number = toNumber(number);\n        precision = precision == null ? 0 : nativeMin(toInteger(precision), 292);\n        if (precision && nativeIsFinite(number)) {\n          // Shift with exponential notation to avoid floating-point issues.\n          // See [MDN](https://mdn.io/round#Examples) for more details.\n          var pair = (toString(number) + 'e').split('e'),\n              value = func(pair[0] + 'e' + (+pair[1] + precision));\n\n          pair = (toString(value) + 'e').split('e');\n          return +(pair[0] + 'e' + (+pair[1] - precision));\n        }\n        return func(number);\n      };\n    }\n\n    /**\n     * Creates a set object of `values`.\n     *\n     * @private\n     * @param {Array} values The values to add to the set.\n     * @returns {Object} Returns the new set.\n     */\n    var createSet = !(Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY) ? noop : function(values) {\n      return new Set(values);\n    };\n\n    /**\n     * Creates a `_.toPairs` or `_.toPairsIn` function.\n     *\n     * @private\n     * @param {Function} keysFunc The function to get the keys of a given object.\n     * @returns {Function} Returns the new pairs function.\n     */\n    function createToPairs(keysFunc) {\n      return function(object) {\n        var tag = getTag(object);\n        if (tag == mapTag) {\n          return mapToArray(object);\n        }\n        if (tag == setTag) {\n          return setToPairs(object);\n        }\n        return baseToPairs(object, keysFunc(object));\n      };\n    }\n\n    /**\n     * Creates a function that either curries or invokes `func` with optional\n     * `this` binding and partially applied arguments.\n     *\n     * @private\n     * @param {Function|string} func The function or method name to wrap.\n     * @param {number} bitmask The bitmask flags.\n     *    1 - `_.bind`\n     *    2 - `_.bindKey`\n     *    4 - `_.curry` or `_.curryRight` of a bound function\n     *    8 - `_.curry`\n     *   16 - `_.curryRight`\n     *   32 - `_.partial`\n     *   64 - `_.partialRight`\n     *  128 - `_.rearg`\n     *  256 - `_.ary`\n     *  512 - `_.flip`\n     * @param {*} [thisArg] The `this` binding of `func`.\n     * @param {Array} [partials] The arguments to be partially applied.\n     * @param {Array} [holders] The `partials` placeholder indexes.\n     * @param {Array} [argPos] The argument positions of the new function.\n     * @param {number} [ary] The arity cap of `func`.\n     * @param {number} [arity] The arity of `func`.\n     * @returns {Function} Returns the new wrapped function.\n     */\n    function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {\n      var isBindKey = bitmask & WRAP_BIND_KEY_FLAG;\n      if (!isBindKey && typeof func != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      var length = partials ? partials.length : 0;\n      if (!length) {\n        bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG);\n        partials = holders = undefined;\n      }\n      ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0);\n      arity = arity === undefined ? arity : toInteger(arity);\n      length -= holders ? holders.length : 0;\n\n      if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) {\n        var partialsRight = partials,\n            holdersRight = holders;\n\n        partials = holders = undefined;\n      }\n      var data = isBindKey ? undefined : getData(func);\n\n      var newData = [\n        func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,\n        argPos, ary, arity\n      ];\n\n      if (data) {\n        mergeData(newData, data);\n      }\n      func = newData[0];\n      bitmask = newData[1];\n      thisArg = newData[2];\n      partials = newData[3];\n      holders = newData[4];\n      arity = newData[9] = newData[9] === undefined\n        ? (isBindKey ? 0 : func.length)\n        : nativeMax(newData[9] - length, 0);\n\n      if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) {\n        bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG);\n      }\n      if (!bitmask || bitmask == WRAP_BIND_FLAG) {\n        var result = createBind(func, bitmask, thisArg);\n      } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) {\n        result = createCurry(func, bitmask, arity);\n      } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) {\n        result = createPartial(func, bitmask, thisArg, partials);\n      } else {\n        result = createHybrid.apply(undefined, newData);\n      }\n      var setter = data ? baseSetData : setData;\n      return setWrapToString(setter(result, newData), func, bitmask);\n    }\n\n    /**\n     * Used by `_.defaults` to customize its `_.assignIn` use to assign properties\n     * of source objects to the destination object for all destination properties\n     * that resolve to `undefined`.\n     *\n     * @private\n     * @param {*} objValue The destination value.\n     * @param {*} srcValue The source value.\n     * @param {string} key The key of the property to assign.\n     * @param {Object} object The parent object of `objValue`.\n     * @returns {*} Returns the value to assign.\n     */\n    function customDefaultsAssignIn(objValue, srcValue, key, object) {\n      if (objValue === undefined ||\n          (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) {\n        return srcValue;\n      }\n      return objValue;\n    }\n\n    /**\n     * Used by `_.defaultsDeep` to customize its `_.merge` use to merge source\n     * objects into destination objects that are passed thru.\n     *\n     * @private\n     * @param {*} objValue The destination value.\n     * @param {*} srcValue The source value.\n     * @param {string} key The key of the property to merge.\n     * @param {Object} object The parent object of `objValue`.\n     * @param {Object} source The parent object of `srcValue`.\n     * @param {Object} [stack] Tracks traversed source values and their merged\n     *  counterparts.\n     * @returns {*} Returns the value to assign.\n     */\n    function customDefaultsMerge(objValue, srcValue, key, object, source, stack) {\n      if (isObject(objValue) && isObject(srcValue)) {\n        // Recursively merge objects and arrays (susceptible to call stack limits).\n        stack.set(srcValue, objValue);\n        baseMerge(objValue, srcValue, undefined, customDefaultsMerge, stack);\n        stack['delete'](srcValue);\n      }\n      return objValue;\n    }\n\n    /**\n     * Used by `_.omit` to customize its `_.cloneDeep` use to only clone plain\n     * objects.\n     *\n     * @private\n     * @param {*} value The value to inspect.\n     * @param {string} key The key of the property to inspect.\n     * @returns {*} Returns the uncloned value or `undefined` to defer cloning to `_.cloneDeep`.\n     */\n    function customOmitClone(value) {\n      return isPlainObject(value) ? undefined : value;\n    }\n\n    /**\n     * A specialized version of `baseIsEqualDeep` for arrays with support for\n     * partial deep comparisons.\n     *\n     * @private\n     * @param {Array} array The array to compare.\n     * @param {Array} other The other array to compare.\n     * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.\n     * @param {Function} customizer The function to customize comparisons.\n     * @param {Function} equalFunc The function to determine equivalents of values.\n     * @param {Object} stack Tracks traversed `array` and `other` objects.\n     * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.\n     */\n    function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {\n      var isPartial = bitmask & COMPARE_PARTIAL_FLAG,\n          arrLength = array.length,\n          othLength = other.length;\n\n      if (arrLength != othLength && !(isPartial && othLength > arrLength)) {\n        return false;\n      }\n      // Check that cyclic values are equal.\n      var arrStacked = stack.get(array);\n      var othStacked = stack.get(other);\n      if (arrStacked && othStacked) {\n        return arrStacked == other && othStacked == array;\n      }\n      var index = -1,\n          result = true,\n          seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined;\n\n      stack.set(array, other);\n      stack.set(other, array);\n\n      // Ignore non-index properties.\n      while (++index < arrLength) {\n        var arrValue = array[index],\n            othValue = other[index];\n\n        if (customizer) {\n          var compared = isPartial\n            ? customizer(othValue, arrValue, index, other, array, stack)\n            : customizer(arrValue, othValue, index, array, other, stack);\n        }\n        if (compared !== undefined) {\n          if (compared) {\n            continue;\n          }\n          result = false;\n          break;\n        }\n        // Recursively compare arrays (susceptible to call stack limits).\n        if (seen) {\n          if (!arraySome(other, function(othValue, othIndex) {\n                if (!cacheHas(seen, othIndex) &&\n                    (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {\n                  return seen.push(othIndex);\n                }\n              })) {\n            result = false;\n            break;\n          }\n        } else if (!(\n              arrValue === othValue ||\n                equalFunc(arrValue, othValue, bitmask, customizer, stack)\n            )) {\n          result = false;\n          break;\n        }\n      }\n      stack['delete'](array);\n      stack['delete'](other);\n      return result;\n    }\n\n    /**\n     * A specialized version of `baseIsEqualDeep` for comparing objects of\n     * the same `toStringTag`.\n     *\n     * **Note:** This function only supports comparing values with tags of\n     * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.\n     *\n     * @private\n     * @param {Object} object The object to compare.\n     * @param {Object} other The other object to compare.\n     * @param {string} tag The `toStringTag` of the objects to compare.\n     * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.\n     * @param {Function} customizer The function to customize comparisons.\n     * @param {Function} equalFunc The function to determine equivalents of values.\n     * @param {Object} stack Tracks traversed `object` and `other` objects.\n     * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.\n     */\n    function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {\n      switch (tag) {\n        case dataViewTag:\n          if ((object.byteLength != other.byteLength) ||\n              (object.byteOffset != other.byteOffset)) {\n            return false;\n          }\n          object = object.buffer;\n          other = other.buffer;\n\n        case arrayBufferTag:\n          if ((object.byteLength != other.byteLength) ||\n              !equalFunc(new Uint8Array(object), new Uint8Array(other))) {\n            return false;\n          }\n          return true;\n\n        case boolTag:\n        case dateTag:\n        case numberTag:\n          // Coerce booleans to `1` or `0` and dates to milliseconds.\n          // Invalid dates are coerced to `NaN`.\n          return eq(+object, +other);\n\n        case errorTag:\n          return object.name == other.name && object.message == other.message;\n\n        case regexpTag:\n        case stringTag:\n          // Coerce regexes to strings and treat strings, primitives and objects,\n          // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring\n          // for more details.\n          return object == (other + '');\n\n        case mapTag:\n          var convert = mapToArray;\n\n        case setTag:\n          var isPartial = bitmask & COMPARE_PARTIAL_FLAG;\n          convert || (convert = setToArray);\n\n          if (object.size != other.size && !isPartial) {\n            return false;\n          }\n          // Assume cyclic values are equal.\n          var stacked = stack.get(object);\n          if (stacked) {\n            return stacked == other;\n          }\n          bitmask |= COMPARE_UNORDERED_FLAG;\n\n          // Recursively compare objects (susceptible to call stack limits).\n          stack.set(object, other);\n          var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);\n          stack['delete'](object);\n          return result;\n\n        case symbolTag:\n          if (symbolValueOf) {\n            return symbolValueOf.call(object) == symbolValueOf.call(other);\n          }\n      }\n      return false;\n    }\n\n    /**\n     * A specialized version of `baseIsEqualDeep` for objects with support for\n     * partial deep comparisons.\n     *\n     * @private\n     * @param {Object} object The object to compare.\n     * @param {Object} other The other object to compare.\n     * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.\n     * @param {Function} customizer The function to customize comparisons.\n     * @param {Function} equalFunc The function to determine equivalents of values.\n     * @param {Object} stack Tracks traversed `object` and `other` objects.\n     * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.\n     */\n    function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {\n      var isPartial = bitmask & COMPARE_PARTIAL_FLAG,\n          objProps = getAllKeys(object),\n          objLength = objProps.length,\n          othProps = getAllKeys(other),\n          othLength = othProps.length;\n\n      if (objLength != othLength && !isPartial) {\n        return false;\n      }\n      var index = objLength;\n      while (index--) {\n        var key = objProps[index];\n        if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {\n          return false;\n        }\n      }\n      // Check that cyclic values are equal.\n      var objStacked = stack.get(object);\n      var othStacked = stack.get(other);\n      if (objStacked && othStacked) {\n        return objStacked == other && othStacked == object;\n      }\n      var result = true;\n      stack.set(object, other);\n      stack.set(other, object);\n\n      var skipCtor = isPartial;\n      while (++index < objLength) {\n        key = objProps[index];\n        var objValue = object[key],\n            othValue = other[key];\n\n        if (customizer) {\n          var compared = isPartial\n            ? customizer(othValue, objValue, key, other, object, stack)\n            : customizer(objValue, othValue, key, object, other, stack);\n        }\n        // Recursively compare objects (susceptible to call stack limits).\n        if (!(compared === undefined\n              ? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack))\n              : compared\n            )) {\n          result = false;\n          break;\n        }\n        skipCtor || (skipCtor = key == 'constructor');\n      }\n      if (result && !skipCtor) {\n        var objCtor = object.constructor,\n            othCtor = other.constructor;\n\n        // Non `Object` object instances with different constructors are not equal.\n        if (objCtor != othCtor &&\n            ('constructor' in object && 'constructor' in other) &&\n            !(typeof objCtor == 'function' && objCtor instanceof objCtor &&\n              typeof othCtor == 'function' && othCtor instanceof othCtor)) {\n          result = false;\n        }\n      }\n      stack['delete'](object);\n      stack['delete'](other);\n      return result;\n    }\n\n    /**\n     * A specialized version of `baseRest` which flattens the rest array.\n     *\n     * @private\n     * @param {Function} func The function to apply a rest parameter to.\n     * @returns {Function} Returns the new function.\n     */\n    function flatRest(func) {\n      return setToString(overRest(func, undefined, flatten), func + '');\n    }\n\n    /**\n     * Creates an array of own enumerable property names and symbols of `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property names and symbols.\n     */\n    function getAllKeys(object) {\n      return baseGetAllKeys(object, keys, getSymbols);\n    }\n\n    /**\n     * Creates an array of own and inherited enumerable property names and\n     * symbols of `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property names and symbols.\n     */\n    function getAllKeysIn(object) {\n      return baseGetAllKeys(object, keysIn, getSymbolsIn);\n    }\n\n    /**\n     * Gets metadata for `func`.\n     *\n     * @private\n     * @param {Function} func The function to query.\n     * @returns {*} Returns the metadata for `func`.\n     */\n    var getData = !metaMap ? noop : function(func) {\n      return metaMap.get(func);\n    };\n\n    /**\n     * Gets the name of `func`.\n     *\n     * @private\n     * @param {Function} func The function to query.\n     * @returns {string} Returns the function name.\n     */\n    function getFuncName(func) {\n      var result = (func.name + ''),\n          array = realNames[result],\n          length = hasOwnProperty.call(realNames, result) ? array.length : 0;\n\n      while (length--) {\n        var data = array[length],\n            otherFunc = data.func;\n        if (otherFunc == null || otherFunc == func) {\n          return data.name;\n        }\n      }\n      return result;\n    }\n\n    /**\n     * Gets the argument placeholder value for `func`.\n     *\n     * @private\n     * @param {Function} func The function to inspect.\n     * @returns {*} Returns the placeholder value.\n     */\n    function getHolder(func) {\n      var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func;\n      return object.placeholder;\n    }\n\n    /**\n     * Gets the appropriate \"iteratee\" function. If `_.iteratee` is customized,\n     * this function returns the custom method, otherwise it returns `baseIteratee`.\n     * If arguments are provided, the chosen function is invoked with them and\n     * its result is returned.\n     *\n     * @private\n     * @param {*} [value] The value to convert to an iteratee.\n     * @param {number} [arity] The arity of the created iteratee.\n     * @returns {Function} Returns the chosen function or its result.\n     */\n    function getIteratee() {\n      var result = lodash.iteratee || iteratee;\n      result = result === iteratee ? baseIteratee : result;\n      return arguments.length ? result(arguments[0], arguments[1]) : result;\n    }\n\n    /**\n     * Gets the data for `map`.\n     *\n     * @private\n     * @param {Object} map The map to query.\n     * @param {string} key The reference key.\n     * @returns {*} Returns the map data.\n     */\n    function getMapData(map, key) {\n      var data = map.__data__;\n      return isKeyable(key)\n        ? data[typeof key == 'string' ? 'string' : 'hash']\n        : data.map;\n    }\n\n    /**\n     * Gets the property names, values, and compare flags of `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the match data of `object`.\n     */\n    function getMatchData(object) {\n      var result = keys(object),\n          length = result.length;\n\n      while (length--) {\n        var key = result[length],\n            value = object[key];\n\n        result[length] = [key, value, isStrictComparable(value)];\n      }\n      return result;\n    }\n\n    /**\n     * Gets the native function at `key` of `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @param {string} key The key of the method to get.\n     * @returns {*} Returns the function if it's native, else `undefined`.\n     */\n    function getNative(object, key) {\n      var value = getValue(object, key);\n      return baseIsNative(value) ? value : undefined;\n    }\n\n    /**\n     * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.\n     *\n     * @private\n     * @param {*} value The value to query.\n     * @returns {string} Returns the raw `toStringTag`.\n     */\n    function getRawTag(value) {\n      var isOwn = hasOwnProperty.call(value, symToStringTag),\n          tag = value[symToStringTag];\n\n      try {\n        value[symToStringTag] = undefined;\n        var unmasked = true;\n      } catch (e) {}\n\n      var result = nativeObjectToString.call(value);\n      if (unmasked) {\n        if (isOwn) {\n          value[symToStringTag] = tag;\n        } else {\n          delete value[symToStringTag];\n        }\n      }\n      return result;\n    }\n\n    /**\n     * Creates an array of the own enumerable symbols of `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of symbols.\n     */\n    var getSymbols = !nativeGetSymbols ? stubArray : function(object) {\n      if (object == null) {\n        return [];\n      }\n      object = Object(object);\n      return arrayFilter(nativeGetSymbols(object), function(symbol) {\n        return propertyIsEnumerable.call(object, symbol);\n      });\n    };\n\n    /**\n     * Creates an array of the own and inherited enumerable symbols of `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of symbols.\n     */\n    var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) {\n      var result = [];\n      while (object) {\n        arrayPush(result, getSymbols(object));\n        object = getPrototype(object);\n      }\n      return result;\n    };\n\n    /**\n     * Gets the `toStringTag` of `value`.\n     *\n     * @private\n     * @param {*} value The value to query.\n     * @returns {string} Returns the `toStringTag`.\n     */\n    var getTag = baseGetTag;\n\n    // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.\n    if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||\n        (Map && getTag(new Map) != mapTag) ||\n        (Promise && getTag(Promise.resolve()) != promiseTag) ||\n        (Set && getTag(new Set) != setTag) ||\n        (WeakMap && getTag(new WeakMap) != weakMapTag)) {\n      getTag = function(value) {\n        var result = baseGetTag(value),\n            Ctor = result == objectTag ? value.constructor : undefined,\n            ctorString = Ctor ? toSource(Ctor) : '';\n\n        if (ctorString) {\n          switch (ctorString) {\n            case dataViewCtorString: return dataViewTag;\n            case mapCtorString: return mapTag;\n            case promiseCtorString: return promiseTag;\n            case setCtorString: return setTag;\n            case weakMapCtorString: return weakMapTag;\n          }\n        }\n        return result;\n      };\n    }\n\n    /**\n     * Gets the view, applying any `transforms` to the `start` and `end` positions.\n     *\n     * @private\n     * @param {number} start The start of the view.\n     * @param {number} end The end of the view.\n     * @param {Array} transforms The transformations to apply to the view.\n     * @returns {Object} Returns an object containing the `start` and `end`\n     *  positions of the view.\n     */\n    function getView(start, end, transforms) {\n      var index = -1,\n          length = transforms.length;\n\n      while (++index < length) {\n        var data = transforms[index],\n            size = data.size;\n\n        switch (data.type) {\n          case 'drop':      start += size; break;\n          case 'dropRight': end -= size; break;\n          case 'take':      end = nativeMin(end, start + size); break;\n          case 'takeRight': start = nativeMax(start, end - size); break;\n        }\n      }\n      return { 'start': start, 'end': end };\n    }\n\n    /**\n     * Extracts wrapper details from the `source` body comment.\n     *\n     * @private\n     * @param {string} source The source to inspect.\n     * @returns {Array} Returns the wrapper details.\n     */\n    function getWrapDetails(source) {\n      var match = source.match(reWrapDetails);\n      return match ? match[1].split(reSplitDetails) : [];\n    }\n\n    /**\n     * Checks if `path` exists on `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @param {Array|string} path The path to check.\n     * @param {Function} hasFunc The function to check properties.\n     * @returns {boolean} Returns `true` if `path` exists, else `false`.\n     */\n    function hasPath(object, path, hasFunc) {\n      path = castPath(path, object);\n\n      var index = -1,\n          length = path.length,\n          result = false;\n\n      while (++index < length) {\n        var key = toKey(path[index]);\n        if (!(result = object != null && hasFunc(object, key))) {\n          break;\n        }\n        object = object[key];\n      }\n      if (result || ++index != length) {\n        return result;\n      }\n      length = object == null ? 0 : object.length;\n      return !!length && isLength(length) && isIndex(key, length) &&\n        (isArray(object) || isArguments(object));\n    }\n\n    /**\n     * Initializes an array clone.\n     *\n     * @private\n     * @param {Array} array The array to clone.\n     * @returns {Array} Returns the initialized clone.\n     */\n    function initCloneArray(array) {\n      var length = array.length,\n          result = new array.constructor(length);\n\n      // Add properties assigned by `RegExp#exec`.\n      if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {\n        result.index = array.index;\n        result.input = array.input;\n      }\n      return result;\n    }\n\n    /**\n     * Initializes an object clone.\n     *\n     * @private\n     * @param {Object} object The object to clone.\n     * @returns {Object} Returns the initialized clone.\n     */\n    function initCloneObject(object) {\n      return (typeof object.constructor == 'function' && !isPrototype(object))\n        ? baseCreate(getPrototype(object))\n        : {};\n    }\n\n    /**\n     * Initializes an object clone based on its `toStringTag`.\n     *\n     * **Note:** This function only supports cloning values with tags of\n     * `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`.\n     *\n     * @private\n     * @param {Object} object The object to clone.\n     * @param {string} tag The `toStringTag` of the object to clone.\n     * @param {boolean} [isDeep] Specify a deep clone.\n     * @returns {Object} Returns the initialized clone.\n     */\n    function initCloneByTag(object, tag, isDeep) {\n      var Ctor = object.constructor;\n      switch (tag) {\n        case arrayBufferTag:\n          return cloneArrayBuffer(object);\n\n        case boolTag:\n        case dateTag:\n          return new Ctor(+object);\n\n        case dataViewTag:\n          return cloneDataView(object, isDeep);\n\n        case float32Tag: case float64Tag:\n        case int8Tag: case int16Tag: case int32Tag:\n        case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:\n          return cloneTypedArray(object, isDeep);\n\n        case mapTag:\n          return new Ctor;\n\n        case numberTag:\n        case stringTag:\n          return new Ctor(object);\n\n        case regexpTag:\n          return cloneRegExp(object);\n\n        case setTag:\n          return new Ctor;\n\n        case symbolTag:\n          return cloneSymbol(object);\n      }\n    }\n\n    /**\n     * Inserts wrapper `details` in a comment at the top of the `source` body.\n     *\n     * @private\n     * @param {string} source The source to modify.\n     * @returns {Array} details The details to insert.\n     * @returns {string} Returns the modified source.\n     */\n    function insertWrapDetails(source, details) {\n      var length = details.length;\n      if (!length) {\n        return source;\n      }\n      var lastIndex = length - 1;\n      details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex];\n      details = details.join(length > 2 ? ', ' : ' ');\n      return source.replace(reWrapComment, '{\\n/* [wrapped with ' + details + '] */\\n');\n    }\n\n    /**\n     * Checks if `value` is a flattenable `arguments` object or array.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is flattenable, else `false`.\n     */\n    function isFlattenable(value) {\n      return isArray(value) || isArguments(value) ||\n        !!(spreadableSymbol && value && value[spreadableSymbol]);\n    }\n\n    /**\n     * Checks if `value` is a valid array-like index.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.\n     * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.\n     */\n    function isIndex(value, length) {\n      var type = typeof value;\n      length = length == null ? MAX_SAFE_INTEGER : length;\n\n      return !!length &&\n        (type == 'number' ||\n          (type != 'symbol' && reIsUint.test(value))) &&\n            (value > -1 && value % 1 == 0 && value < length);\n    }\n\n    /**\n     * Checks if the given arguments are from an iteratee call.\n     *\n     * @private\n     * @param {*} value The potential iteratee value argument.\n     * @param {*} index The potential iteratee index or key argument.\n     * @param {*} object The potential iteratee object argument.\n     * @returns {boolean} Returns `true` if the arguments are from an iteratee call,\n     *  else `false`.\n     */\n    function isIterateeCall(value, index, object) {\n      if (!isObject(object)) {\n        return false;\n      }\n      var type = typeof index;\n      if (type == 'number'\n            ? (isArrayLike(object) && isIndex(index, object.length))\n            : (type == 'string' && index in object)\n          ) {\n        return eq(object[index], value);\n      }\n      return false;\n    }\n\n    /**\n     * Checks if `value` is a property name and not a property path.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @param {Object} [object] The object to query keys on.\n     * @returns {boolean} Returns `true` if `value` is a property name, else `false`.\n     */\n    function isKey(value, object) {\n      if (isArray(value)) {\n        return false;\n      }\n      var type = typeof value;\n      if (type == 'number' || type == 'symbol' || type == 'boolean' ||\n          value == null || isSymbol(value)) {\n        return true;\n      }\n      return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||\n        (object != null && value in Object(object));\n    }\n\n    /**\n     * Checks if `value` is suitable for use as unique object key.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is suitable, else `false`.\n     */\n    function isKeyable(value) {\n      var type = typeof value;\n      return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')\n        ? (value !== '__proto__')\n        : (value === null);\n    }\n\n    /**\n     * Checks if `func` has a lazy counterpart.\n     *\n     * @private\n     * @param {Function} func The function to check.\n     * @returns {boolean} Returns `true` if `func` has a lazy counterpart,\n     *  else `false`.\n     */\n    function isLaziable(func) {\n      var funcName = getFuncName(func),\n          other = lodash[funcName];\n\n      if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) {\n        return false;\n      }\n      if (func === other) {\n        return true;\n      }\n      var data = getData(other);\n      return !!data && func === data[0];\n    }\n\n    /**\n     * Checks if `func` has its source masked.\n     *\n     * @private\n     * @param {Function} func The function to check.\n     * @returns {boolean} Returns `true` if `func` is masked, else `false`.\n     */\n    function isMasked(func) {\n      return !!maskSrcKey && (maskSrcKey in func);\n    }\n\n    /**\n     * Checks if `func` is capable of being masked.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `func` is maskable, else `false`.\n     */\n    var isMaskable = coreJsData ? isFunction : stubFalse;\n\n    /**\n     * Checks if `value` is likely a prototype object.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a prototype, else `false`.\n     */\n    function isPrototype(value) {\n      var Ctor = value && value.constructor,\n          proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;\n\n      return value === proto;\n    }\n\n    /**\n     * Checks if `value` is suitable for strict equality comparisons, i.e. `===`.\n     *\n     * @private\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` if suitable for strict\n     *  equality comparisons, else `false`.\n     */\n    function isStrictComparable(value) {\n      return value === value && !isObject(value);\n    }\n\n    /**\n     * A specialized version of `matchesProperty` for source values suitable\n     * for strict equality comparisons, i.e. `===`.\n     *\n     * @private\n     * @param {string} key The key of the property to get.\n     * @param {*} srcValue The value to match.\n     * @returns {Function} Returns the new spec function.\n     */\n    function matchesStrictComparable(key, srcValue) {\n      return function(object) {\n        if (object == null) {\n          return false;\n        }\n        return object[key] === srcValue &&\n          (srcValue !== undefined || (key in Object(object)));\n      };\n    }\n\n    /**\n     * A specialized version of `_.memoize` which clears the memoized function's\n     * cache when it exceeds `MAX_MEMOIZE_SIZE`.\n     *\n     * @private\n     * @param {Function} func The function to have its output memoized.\n     * @returns {Function} Returns the new memoized function.\n     */\n    function memoizeCapped(func) {\n      var result = memoize(func, function(key) {\n        if (cache.size === MAX_MEMOIZE_SIZE) {\n          cache.clear();\n        }\n        return key;\n      });\n\n      var cache = result.cache;\n      return result;\n    }\n\n    /**\n     * Merges the function metadata of `source` into `data`.\n     *\n     * Merging metadata reduces the number of wrappers used to invoke a function.\n     * This is possible because methods like `_.bind`, `_.curry`, and `_.partial`\n     * may be applied regardless of execution order. Methods like `_.ary` and\n     * `_.rearg` modify function arguments, making the order in which they are\n     * executed important, preventing the merging of metadata. However, we make\n     * an exception for a safe combined case where curried functions have `_.ary`\n     * and or `_.rearg` applied.\n     *\n     * @private\n     * @param {Array} data The destination metadata.\n     * @param {Array} source The source metadata.\n     * @returns {Array} Returns `data`.\n     */\n    function mergeData(data, source) {\n      var bitmask = data[1],\n          srcBitmask = source[1],\n          newBitmask = bitmask | srcBitmask,\n          isCommon = newBitmask < (WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG | WRAP_ARY_FLAG);\n\n      var isCombo =\n        ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_CURRY_FLAG)) ||\n        ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_REARG_FLAG) && (data[7].length <= source[8])) ||\n        ((srcBitmask == (WRAP_ARY_FLAG | WRAP_REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == WRAP_CURRY_FLAG));\n\n      // Exit early if metadata can't be merged.\n      if (!(isCommon || isCombo)) {\n        return data;\n      }\n      // Use source `thisArg` if available.\n      if (srcBitmask & WRAP_BIND_FLAG) {\n        data[2] = source[2];\n        // Set when currying a bound function.\n        newBitmask |= bitmask & WRAP_BIND_FLAG ? 0 : WRAP_CURRY_BOUND_FLAG;\n      }\n      // Compose partial arguments.\n      var value = source[3];\n      if (value) {\n        var partials = data[3];\n        data[3] = partials ? composeArgs(partials, value, source[4]) : value;\n        data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : source[4];\n      }\n      // Compose partial right arguments.\n      value = source[5];\n      if (value) {\n        partials = data[5];\n        data[5] = partials ? composeArgsRight(partials, value, source[6]) : value;\n        data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : source[6];\n      }\n      // Use source `argPos` if available.\n      value = source[7];\n      if (value) {\n        data[7] = value;\n      }\n      // Use source `ary` if it's smaller.\n      if (srcBitmask & WRAP_ARY_FLAG) {\n        data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]);\n      }\n      // Use source `arity` if one is not provided.\n      if (data[9] == null) {\n        data[9] = source[9];\n      }\n      // Use source `func` and merge bitmasks.\n      data[0] = source[0];\n      data[1] = newBitmask;\n\n      return data;\n    }\n\n    /**\n     * This function is like\n     * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)\n     * except that it includes inherited enumerable properties.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property names.\n     */\n    function nativeKeysIn(object) {\n      var result = [];\n      if (object != null) {\n        for (var key in Object(object)) {\n          result.push(key);\n        }\n      }\n      return result;\n    }\n\n    /**\n     * Converts `value` to a string using `Object.prototype.toString`.\n     *\n     * @private\n     * @param {*} value The value to convert.\n     * @returns {string} Returns the converted string.\n     */\n    function objectToString(value) {\n      return nativeObjectToString.call(value);\n    }\n\n    /**\n     * A specialized version of `baseRest` which transforms the rest array.\n     *\n     * @private\n     * @param {Function} func The function to apply a rest parameter to.\n     * @param {number} [start=func.length-1] The start position of the rest parameter.\n     * @param {Function} transform The rest array transform.\n     * @returns {Function} Returns the new function.\n     */\n    function overRest(func, start, transform) {\n      start = nativeMax(start === undefined ? (func.length - 1) : start, 0);\n      return function() {\n        var args = arguments,\n            index = -1,\n            length = nativeMax(args.length - start, 0),\n            array = Array(length);\n\n        while (++index < length) {\n          array[index] = args[start + index];\n        }\n        index = -1;\n        var otherArgs = Array(start + 1);\n        while (++index < start) {\n          otherArgs[index] = args[index];\n        }\n        otherArgs[start] = transform(array);\n        return apply(func, this, otherArgs);\n      };\n    }\n\n    /**\n     * Gets the parent value at `path` of `object`.\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @param {Array} path The path to get the parent value of.\n     * @returns {*} Returns the parent value.\n     */\n    function parent(object, path) {\n      return path.length < 2 ? object : baseGet(object, baseSlice(path, 0, -1));\n    }\n\n    /**\n     * Reorder `array` according to the specified indexes where the element at\n     * the first index is assigned as the first element, the element at\n     * the second index is assigned as the second element, and so on.\n     *\n     * @private\n     * @param {Array} array The array to reorder.\n     * @param {Array} indexes The arranged array indexes.\n     * @returns {Array} Returns `array`.\n     */\n    function reorder(array, indexes) {\n      var arrLength = array.length,\n          length = nativeMin(indexes.length, arrLength),\n          oldArray = copyArray(array);\n\n      while (length--) {\n        var index = indexes[length];\n        array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined;\n      }\n      return array;\n    }\n\n    /**\n     * Gets the value at `key`, unless `key` is \"__proto__\" or \"constructor\".\n     *\n     * @private\n     * @param {Object} object The object to query.\n     * @param {string} key The key of the property to get.\n     * @returns {*} Returns the property value.\n     */\n    function safeGet(object, key) {\n      if (key === 'constructor' && typeof object[key] === 'function') {\n        return;\n      }\n\n      if (key == '__proto__') {\n        return;\n      }\n\n      return object[key];\n    }\n\n    /**\n     * Sets metadata for `func`.\n     *\n     * **Note:** If this function becomes hot, i.e. is invoked a lot in a short\n     * period of time, it will trip its breaker and transition to an identity\n     * function to avoid garbage collection pauses in V8. See\n     * [V8 issue 2070](https://bugs.chromium.org/p/v8/issues/detail?id=2070)\n     * for more details.\n     *\n     * @private\n     * @param {Function} func The function to associate metadata with.\n     * @param {*} data The metadata.\n     * @returns {Function} Returns `func`.\n     */\n    var setData = shortOut(baseSetData);\n\n    /**\n     * A simple wrapper around the global [`setTimeout`](https://mdn.io/setTimeout).\n     *\n     * @private\n     * @param {Function} func The function to delay.\n     * @param {number} wait The number of milliseconds to delay invocation.\n     * @returns {number|Object} Returns the timer id or timeout object.\n     */\n    var setTimeout = ctxSetTimeout || function(func, wait) {\n      return root.setTimeout(func, wait);\n    };\n\n    /**\n     * Sets the `toString` method of `func` to return `string`.\n     *\n     * @private\n     * @param {Function} func The function to modify.\n     * @param {Function} string The `toString` result.\n     * @returns {Function} Returns `func`.\n     */\n    var setToString = shortOut(baseSetToString);\n\n    /**\n     * Sets the `toString` method of `wrapper` to mimic the source of `reference`\n     * with wrapper details in a comment at the top of the source body.\n     *\n     * @private\n     * @param {Function} wrapper The function to modify.\n     * @param {Function} reference The reference function.\n     * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n     * @returns {Function} Returns `wrapper`.\n     */\n    function setWrapToString(wrapper, reference, bitmask) {\n      var source = (reference + '');\n      return setToString(wrapper, insertWrapDetails(source, updateWrapDetails(getWrapDetails(source), bitmask)));\n    }\n\n    /**\n     * Creates a function that'll short out and invoke `identity` instead\n     * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN`\n     * milliseconds.\n     *\n     * @private\n     * @param {Function} func The function to restrict.\n     * @returns {Function} Returns the new shortable function.\n     */\n    function shortOut(func) {\n      var count = 0,\n          lastCalled = 0;\n\n      return function() {\n        var stamp = nativeNow(),\n            remaining = HOT_SPAN - (stamp - lastCalled);\n\n        lastCalled = stamp;\n        if (remaining > 0) {\n          if (++count >= HOT_COUNT) {\n            return arguments[0];\n          }\n        } else {\n          count = 0;\n        }\n        return func.apply(undefined, arguments);\n      };\n    }\n\n    /**\n     * A specialized version of `_.shuffle` which mutates and sets the size of `array`.\n     *\n     * @private\n     * @param {Array} array The array to shuffle.\n     * @param {number} [size=array.length] The size of `array`.\n     * @returns {Array} Returns `array`.\n     */\n    function shuffleSelf(array, size) {\n      var index = -1,\n          length = array.length,\n          lastIndex = length - 1;\n\n      size = size === undefined ? length : size;\n      while (++index < size) {\n        var rand = baseRandom(index, lastIndex),\n            value = array[rand];\n\n        array[rand] = array[index];\n        array[index] = value;\n      }\n      array.length = size;\n      return array;\n    }\n\n    /**\n     * Converts `string` to a property path array.\n     *\n     * @private\n     * @param {string} string The string to convert.\n     * @returns {Array} Returns the property path array.\n     */\n    var stringToPath = memoizeCapped(function(string) {\n      var result = [];\n      if (string.charCodeAt(0) === 46 /* . */) {\n        result.push('');\n      }\n      string.replace(rePropName, function(match, number, quote, subString) {\n        result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match));\n      });\n      return result;\n    });\n\n    /**\n     * Converts `value` to a string key if it's not a string or symbol.\n     *\n     * @private\n     * @param {*} value The value to inspect.\n     * @returns {string|symbol} Returns the key.\n     */\n    function toKey(value) {\n      if (typeof value == 'string' || isSymbol(value)) {\n        return value;\n      }\n      var result = (value + '');\n      return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;\n    }\n\n    /**\n     * Converts `func` to its source code.\n     *\n     * @private\n     * @param {Function} func The function to convert.\n     * @returns {string} Returns the source code.\n     */\n    function toSource(func) {\n      if (func != null) {\n        try {\n          return funcToString.call(func);\n        } catch (e) {}\n        try {\n          return (func + '');\n        } catch (e) {}\n      }\n      return '';\n    }\n\n    /**\n     * Updates wrapper `details` based on `bitmask` flags.\n     *\n     * @private\n     * @returns {Array} details The details to modify.\n     * @param {number} bitmask The bitmask flags. See `createWrap` for more details.\n     * @returns {Array} Returns `details`.\n     */\n    function updateWrapDetails(details, bitmask) {\n      arrayEach(wrapFlags, function(pair) {\n        var value = '_.' + pair[0];\n        if ((bitmask & pair[1]) && !arrayIncludes(details, value)) {\n          details.push(value);\n        }\n      });\n      return details.sort();\n    }\n\n    /**\n     * Creates a clone of `wrapper`.\n     *\n     * @private\n     * @param {Object} wrapper The wrapper to clone.\n     * @returns {Object} Returns the cloned wrapper.\n     */\n    function wrapperClone(wrapper) {\n      if (wrapper instanceof LazyWrapper) {\n        return wrapper.clone();\n      }\n      var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__);\n      result.__actions__ = copyArray(wrapper.__actions__);\n      result.__index__  = wrapper.__index__;\n      result.__values__ = wrapper.__values__;\n      return result;\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates an array of elements split into groups the length of `size`.\n     * If `array` can't be split evenly, the final chunk will be the remaining\n     * elements.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to process.\n     * @param {number} [size=1] The length of each chunk\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Array} Returns the new array of chunks.\n     * @example\n     *\n     * _.chunk(['a', 'b', 'c', 'd'], 2);\n     * // => [['a', 'b'], ['c', 'd']]\n     *\n     * _.chunk(['a', 'b', 'c', 'd'], 3);\n     * // => [['a', 'b', 'c'], ['d']]\n     */\n    function chunk(array, size, guard) {\n      if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) {\n        size = 1;\n      } else {\n        size = nativeMax(toInteger(size), 0);\n      }\n      var length = array == null ? 0 : array.length;\n      if (!length || size < 1) {\n        return [];\n      }\n      var index = 0,\n          resIndex = 0,\n          result = Array(nativeCeil(length / size));\n\n      while (index < length) {\n        result[resIndex++] = baseSlice(array, index, (index += size));\n      }\n      return result;\n    }\n\n    /**\n     * Creates an array with all falsey values removed. The values `false`, `null`,\n     * `0`, `\"\"`, `undefined`, and `NaN` are falsey.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to compact.\n     * @returns {Array} Returns the new array of filtered values.\n     * @example\n     *\n     * _.compact([0, 1, false, 2, '', 3]);\n     * // => [1, 2, 3]\n     */\n    function compact(array) {\n      var index = -1,\n          length = array == null ? 0 : array.length,\n          resIndex = 0,\n          result = [];\n\n      while (++index < length) {\n        var value = array[index];\n        if (value) {\n          result[resIndex++] = value;\n        }\n      }\n      return result;\n    }\n\n    /**\n     * Creates a new array concatenating `array` with any additional arrays\n     * and/or values.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to concatenate.\n     * @param {...*} [values] The values to concatenate.\n     * @returns {Array} Returns the new concatenated array.\n     * @example\n     *\n     * var array = [1];\n     * var other = _.concat(array, 2, [3], [[4]]);\n     *\n     * console.log(other);\n     * // => [1, 2, 3, [4]]\n     *\n     * console.log(array);\n     * // => [1]\n     */\n    function concat() {\n      var length = arguments.length;\n      if (!length) {\n        return [];\n      }\n      var args = Array(length - 1),\n          array = arguments[0],\n          index = length;\n\n      while (index--) {\n        args[index - 1] = arguments[index];\n      }\n      return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1));\n    }\n\n    /**\n     * Creates an array of `array` values not included in the other given arrays\n     * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * for equality comparisons. The order and references of result values are\n     * determined by the first array.\n     *\n     * **Note:** Unlike `_.pullAll`, this method returns a new array.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {...Array} [values] The values to exclude.\n     * @returns {Array} Returns the new array of filtered values.\n     * @see _.without, _.xor\n     * @example\n     *\n     * _.difference([2, 1], [2, 3]);\n     * // => [1]\n     */\n    var difference = baseRest(function(array, values) {\n      return isArrayLikeObject(array)\n        ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true))\n        : [];\n    });\n\n    /**\n     * This method is like `_.difference` except that it accepts `iteratee` which\n     * is invoked for each element of `array` and `values` to generate the criterion\n     * by which they're compared. The order and references of result values are\n     * determined by the first array. The iteratee is invoked with one argument:\n     * (value).\n     *\n     * **Note:** Unlike `_.pullAllBy`, this method returns a new array.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {...Array} [values] The values to exclude.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {Array} Returns the new array of filtered values.\n     * @example\n     *\n     * _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);\n     * // => [1.2]\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');\n     * // => [{ 'x': 2 }]\n     */\n    var differenceBy = baseRest(function(array, values) {\n      var iteratee = last(values);\n      if (isArrayLikeObject(iteratee)) {\n        iteratee = undefined;\n      }\n      return isArrayLikeObject(array)\n        ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2))\n        : [];\n    });\n\n    /**\n     * This method is like `_.difference` except that it accepts `comparator`\n     * which is invoked to compare elements of `array` to `values`. The order and\n     * references of result values are determined by the first array. The comparator\n     * is invoked with two arguments: (arrVal, othVal).\n     *\n     * **Note:** Unlike `_.pullAllWith`, this method returns a new array.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {...Array} [values] The values to exclude.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new array of filtered values.\n     * @example\n     *\n     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n     *\n     * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual);\n     * // => [{ 'x': 2, 'y': 1 }]\n     */\n    var differenceWith = baseRest(function(array, values) {\n      var comparator = last(values);\n      if (isArrayLikeObject(comparator)) {\n        comparator = undefined;\n      }\n      return isArrayLikeObject(array)\n        ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator)\n        : [];\n    });\n\n    /**\n     * Creates a slice of `array` with `n` elements dropped from the beginning.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.5.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {number} [n=1] The number of elements to drop.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * _.drop([1, 2, 3]);\n     * // => [2, 3]\n     *\n     * _.drop([1, 2, 3], 2);\n     * // => [3]\n     *\n     * _.drop([1, 2, 3], 5);\n     * // => []\n     *\n     * _.drop([1, 2, 3], 0);\n     * // => [1, 2, 3]\n     */\n    function drop(array, n, guard) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return [];\n      }\n      n = (guard || n === undefined) ? 1 : toInteger(n);\n      return baseSlice(array, n < 0 ? 0 : n, length);\n    }\n\n    /**\n     * Creates a slice of `array` with `n` elements dropped from the end.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {number} [n=1] The number of elements to drop.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * _.dropRight([1, 2, 3]);\n     * // => [1, 2]\n     *\n     * _.dropRight([1, 2, 3], 2);\n     * // => [1]\n     *\n     * _.dropRight([1, 2, 3], 5);\n     * // => []\n     *\n     * _.dropRight([1, 2, 3], 0);\n     * // => [1, 2, 3]\n     */\n    function dropRight(array, n, guard) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return [];\n      }\n      n = (guard || n === undefined) ? 1 : toInteger(n);\n      n = length - n;\n      return baseSlice(array, 0, n < 0 ? 0 : n);\n    }\n\n    /**\n     * Creates a slice of `array` excluding elements dropped from the end.\n     * Elements are dropped until `predicate` returns falsey. The predicate is\n     * invoked with three arguments: (value, index, array).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'active': true },\n     *   { 'user': 'fred',    'active': false },\n     *   { 'user': 'pebbles', 'active': false }\n     * ];\n     *\n     * _.dropRightWhile(users, function(o) { return !o.active; });\n     * // => objects for ['barney']\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false });\n     * // => objects for ['barney', 'fred']\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.dropRightWhile(users, ['active', false]);\n     * // => objects for ['barney']\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.dropRightWhile(users, 'active');\n     * // => objects for ['barney', 'fred', 'pebbles']\n     */\n    function dropRightWhile(array, predicate) {\n      return (array && array.length)\n        ? baseWhile(array, getIteratee(predicate, 3), true, true)\n        : [];\n    }\n\n    /**\n     * Creates a slice of `array` excluding elements dropped from the beginning.\n     * Elements are dropped until `predicate` returns falsey. The predicate is\n     * invoked with three arguments: (value, index, array).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'active': false },\n     *   { 'user': 'fred',    'active': false },\n     *   { 'user': 'pebbles', 'active': true }\n     * ];\n     *\n     * _.dropWhile(users, function(o) { return !o.active; });\n     * // => objects for ['pebbles']\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.dropWhile(users, { 'user': 'barney', 'active': false });\n     * // => objects for ['fred', 'pebbles']\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.dropWhile(users, ['active', false]);\n     * // => objects for ['pebbles']\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.dropWhile(users, 'active');\n     * // => objects for ['barney', 'fred', 'pebbles']\n     */\n    function dropWhile(array, predicate) {\n      return (array && array.length)\n        ? baseWhile(array, getIteratee(predicate, 3), true)\n        : [];\n    }\n\n    /**\n     * Fills elements of `array` with `value` from `start` up to, but not\n     * including, `end`.\n     *\n     * **Note:** This method mutates `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.2.0\n     * @category Array\n     * @param {Array} array The array to fill.\n     * @param {*} value The value to fill `array` with.\n     * @param {number} [start=0] The start position.\n     * @param {number} [end=array.length] The end position.\n     * @returns {Array} Returns `array`.\n     * @example\n     *\n     * var array = [1, 2, 3];\n     *\n     * _.fill(array, 'a');\n     * console.log(array);\n     * // => ['a', 'a', 'a']\n     *\n     * _.fill(Array(3), 2);\n     * // => [2, 2, 2]\n     *\n     * _.fill([4, 6, 8, 10], '*', 1, 3);\n     * // => [4, '*', '*', 10]\n     */\n    function fill(array, value, start, end) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return [];\n      }\n      if (start && typeof start != 'number' && isIterateeCall(array, value, start)) {\n        start = 0;\n        end = length;\n      }\n      return baseFill(array, value, start, end);\n    }\n\n    /**\n     * This method is like `_.find` except that it returns the index of the first\n     * element `predicate` returns truthy for instead of the element itself.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.1.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @param {number} [fromIndex=0] The index to search from.\n     * @returns {number} Returns the index of the found element, else `-1`.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'active': false },\n     *   { 'user': 'fred',    'active': false },\n     *   { 'user': 'pebbles', 'active': true }\n     * ];\n     *\n     * _.findIndex(users, function(o) { return o.user == 'barney'; });\n     * // => 0\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.findIndex(users, { 'user': 'fred', 'active': false });\n     * // => 1\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.findIndex(users, ['active', false]);\n     * // => 0\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.findIndex(users, 'active');\n     * // => 2\n     */\n    function findIndex(array, predicate, fromIndex) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return -1;\n      }\n      var index = fromIndex == null ? 0 : toInteger(fromIndex);\n      if (index < 0) {\n        index = nativeMax(length + index, 0);\n      }\n      return baseFindIndex(array, getIteratee(predicate, 3), index);\n    }\n\n    /**\n     * This method is like `_.findIndex` except that it iterates over elements\n     * of `collection` from right to left.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @param {number} [fromIndex=array.length-1] The index to search from.\n     * @returns {number} Returns the index of the found element, else `-1`.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'active': true },\n     *   { 'user': 'fred',    'active': false },\n     *   { 'user': 'pebbles', 'active': false }\n     * ];\n     *\n     * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; });\n     * // => 2\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.findLastIndex(users, { 'user': 'barney', 'active': true });\n     * // => 0\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.findLastIndex(users, ['active', false]);\n     * // => 2\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.findLastIndex(users, 'active');\n     * // => 0\n     */\n    function findLastIndex(array, predicate, fromIndex) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return -1;\n      }\n      var index = length - 1;\n      if (fromIndex !== undefined) {\n        index = toInteger(fromIndex);\n        index = fromIndex < 0\n          ? nativeMax(length + index, 0)\n          : nativeMin(index, length - 1);\n      }\n      return baseFindIndex(array, getIteratee(predicate, 3), index, true);\n    }\n\n    /**\n     * Flattens `array` a single level deep.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to flatten.\n     * @returns {Array} Returns the new flattened array.\n     * @example\n     *\n     * _.flatten([1, [2, [3, [4]], 5]]);\n     * // => [1, 2, [3, [4]], 5]\n     */\n    function flatten(array) {\n      var length = array == null ? 0 : array.length;\n      return length ? baseFlatten(array, 1) : [];\n    }\n\n    /**\n     * Recursively flattens `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to flatten.\n     * @returns {Array} Returns the new flattened array.\n     * @example\n     *\n     * _.flattenDeep([1, [2, [3, [4]], 5]]);\n     * // => [1, 2, 3, 4, 5]\n     */\n    function flattenDeep(array) {\n      var length = array == null ? 0 : array.length;\n      return length ? baseFlatten(array, INFINITY) : [];\n    }\n\n    /**\n     * Recursively flatten `array` up to `depth` times.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.4.0\n     * @category Array\n     * @param {Array} array The array to flatten.\n     * @param {number} [depth=1] The maximum recursion depth.\n     * @returns {Array} Returns the new flattened array.\n     * @example\n     *\n     * var array = [1, [2, [3, [4]], 5]];\n     *\n     * _.flattenDepth(array, 1);\n     * // => [1, 2, [3, [4]], 5]\n     *\n     * _.flattenDepth(array, 2);\n     * // => [1, 2, 3, [4], 5]\n     */\n    function flattenDepth(array, depth) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return [];\n      }\n      depth = depth === undefined ? 1 : toInteger(depth);\n      return baseFlatten(array, depth);\n    }\n\n    /**\n     * The inverse of `_.toPairs`; this method returns an object composed\n     * from key-value `pairs`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} pairs The key-value pairs.\n     * @returns {Object} Returns the new object.\n     * @example\n     *\n     * _.fromPairs([['a', 1], ['b', 2]]);\n     * // => { 'a': 1, 'b': 2 }\n     */\n    function fromPairs(pairs) {\n      var index = -1,\n          length = pairs == null ? 0 : pairs.length,\n          result = {};\n\n      while (++index < length) {\n        var pair = pairs[index];\n        result[pair[0]] = pair[1];\n      }\n      return result;\n    }\n\n    /**\n     * Gets the first element of `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @alias first\n     * @category Array\n     * @param {Array} array The array to query.\n     * @returns {*} Returns the first element of `array`.\n     * @example\n     *\n     * _.head([1, 2, 3]);\n     * // => 1\n     *\n     * _.head([]);\n     * // => undefined\n     */\n    function head(array) {\n      return (array && array.length) ? array[0] : undefined;\n    }\n\n    /**\n     * Gets the index at which the first occurrence of `value` is found in `array`\n     * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * for equality comparisons. If `fromIndex` is negative, it's used as the\n     * offset from the end of `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {*} value The value to search for.\n     * @param {number} [fromIndex=0] The index to search from.\n     * @returns {number} Returns the index of the matched value, else `-1`.\n     * @example\n     *\n     * _.indexOf([1, 2, 1, 2], 2);\n     * // => 1\n     *\n     * // Search from the `fromIndex`.\n     * _.indexOf([1, 2, 1, 2], 2, 2);\n     * // => 3\n     */\n    function indexOf(array, value, fromIndex) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return -1;\n      }\n      var index = fromIndex == null ? 0 : toInteger(fromIndex);\n      if (index < 0) {\n        index = nativeMax(length + index, 0);\n      }\n      return baseIndexOf(array, value, index);\n    }\n\n    /**\n     * Gets all but the last element of `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * _.initial([1, 2, 3]);\n     * // => [1, 2]\n     */\n    function initial(array) {\n      var length = array == null ? 0 : array.length;\n      return length ? baseSlice(array, 0, -1) : [];\n    }\n\n    /**\n     * Creates an array of unique values that are included in all given arrays\n     * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * for equality comparisons. The order and references of result values are\n     * determined by the first array.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @returns {Array} Returns the new array of intersecting values.\n     * @example\n     *\n     * _.intersection([2, 1], [2, 3]);\n     * // => [2]\n     */\n    var intersection = baseRest(function(arrays) {\n      var mapped = arrayMap(arrays, castArrayLikeObject);\n      return (mapped.length && mapped[0] === arrays[0])\n        ? baseIntersection(mapped)\n        : [];\n    });\n\n    /**\n     * This method is like `_.intersection` except that it accepts `iteratee`\n     * which is invoked for each element of each `arrays` to generate the criterion\n     * by which they're compared. The order and references of result values are\n     * determined by the first array. The iteratee is invoked with one argument:\n     * (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {Array} Returns the new array of intersecting values.\n     * @example\n     *\n     * _.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor);\n     * // => [2.1]\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n     * // => [{ 'x': 1 }]\n     */\n    var intersectionBy = baseRest(function(arrays) {\n      var iteratee = last(arrays),\n          mapped = arrayMap(arrays, castArrayLikeObject);\n\n      if (iteratee === last(mapped)) {\n        iteratee = undefined;\n      } else {\n        mapped.pop();\n      }\n      return (mapped.length && mapped[0] === arrays[0])\n        ? baseIntersection(mapped, getIteratee(iteratee, 2))\n        : [];\n    });\n\n    /**\n     * This method is like `_.intersection` except that it accepts `comparator`\n     * which is invoked to compare elements of `arrays`. The order and references\n     * of result values are determined by the first array. The comparator is\n     * invoked with two arguments: (arrVal, othVal).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new array of intersecting values.\n     * @example\n     *\n     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n     * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n     *\n     * _.intersectionWith(objects, others, _.isEqual);\n     * // => [{ 'x': 1, 'y': 2 }]\n     */\n    var intersectionWith = baseRest(function(arrays) {\n      var comparator = last(arrays),\n          mapped = arrayMap(arrays, castArrayLikeObject);\n\n      comparator = typeof comparator == 'function' ? comparator : undefined;\n      if (comparator) {\n        mapped.pop();\n      }\n      return (mapped.length && mapped[0] === arrays[0])\n        ? baseIntersection(mapped, undefined, comparator)\n        : [];\n    });\n\n    /**\n     * Converts all elements in `array` into a string separated by `separator`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to convert.\n     * @param {string} [separator=','] The element separator.\n     * @returns {string} Returns the joined string.\n     * @example\n     *\n     * _.join(['a', 'b', 'c'], '~');\n     * // => 'a~b~c'\n     */\n    function join(array, separator) {\n      return array == null ? '' : nativeJoin.call(array, separator);\n    }\n\n    /**\n     * Gets the last element of `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @returns {*} Returns the last element of `array`.\n     * @example\n     *\n     * _.last([1, 2, 3]);\n     * // => 3\n     */\n    function last(array) {\n      var length = array == null ? 0 : array.length;\n      return length ? array[length - 1] : undefined;\n    }\n\n    /**\n     * This method is like `_.indexOf` except that it iterates over elements of\n     * `array` from right to left.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {*} value The value to search for.\n     * @param {number} [fromIndex=array.length-1] The index to search from.\n     * @returns {number} Returns the index of the matched value, else `-1`.\n     * @example\n     *\n     * _.lastIndexOf([1, 2, 1, 2], 2);\n     * // => 3\n     *\n     * // Search from the `fromIndex`.\n     * _.lastIndexOf([1, 2, 1, 2], 2, 2);\n     * // => 1\n     */\n    function lastIndexOf(array, value, fromIndex) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return -1;\n      }\n      var index = length;\n      if (fromIndex !== undefined) {\n        index = toInteger(fromIndex);\n        index = index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1);\n      }\n      return value === value\n        ? strictLastIndexOf(array, value, index)\n        : baseFindIndex(array, baseIsNaN, index, true);\n    }\n\n    /**\n     * Gets the element at index `n` of `array`. If `n` is negative, the nth\n     * element from the end is returned.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.11.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {number} [n=0] The index of the element to return.\n     * @returns {*} Returns the nth element of `array`.\n     * @example\n     *\n     * var array = ['a', 'b', 'c', 'd'];\n     *\n     * _.nth(array, 1);\n     * // => 'b'\n     *\n     * _.nth(array, -2);\n     * // => 'c';\n     */\n    function nth(array, n) {\n      return (array && array.length) ? baseNth(array, toInteger(n)) : undefined;\n    }\n\n    /**\n     * Removes all given values from `array` using\n     * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * for equality comparisons.\n     *\n     * **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove`\n     * to remove elements from an array by predicate.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Array\n     * @param {Array} array The array to modify.\n     * @param {...*} [values] The values to remove.\n     * @returns {Array} Returns `array`.\n     * @example\n     *\n     * var array = ['a', 'b', 'c', 'a', 'b', 'c'];\n     *\n     * _.pull(array, 'a', 'c');\n     * console.log(array);\n     * // => ['b', 'b']\n     */\n    var pull = baseRest(pullAll);\n\n    /**\n     * This method is like `_.pull` except that it accepts an array of values to remove.\n     *\n     * **Note:** Unlike `_.difference`, this method mutates `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to modify.\n     * @param {Array} values The values to remove.\n     * @returns {Array} Returns `array`.\n     * @example\n     *\n     * var array = ['a', 'b', 'c', 'a', 'b', 'c'];\n     *\n     * _.pullAll(array, ['a', 'c']);\n     * console.log(array);\n     * // => ['b', 'b']\n     */\n    function pullAll(array, values) {\n      return (array && array.length && values && values.length)\n        ? basePullAll(array, values)\n        : array;\n    }\n\n    /**\n     * This method is like `_.pullAll` except that it accepts `iteratee` which is\n     * invoked for each element of `array` and `values` to generate the criterion\n     * by which they're compared. The iteratee is invoked with one argument: (value).\n     *\n     * **Note:** Unlike `_.differenceBy`, this method mutates `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to modify.\n     * @param {Array} values The values to remove.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {Array} Returns `array`.\n     * @example\n     *\n     * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }];\n     *\n     * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x');\n     * console.log(array);\n     * // => [{ 'x': 2 }]\n     */\n    function pullAllBy(array, values, iteratee) {\n      return (array && array.length && values && values.length)\n        ? basePullAll(array, values, getIteratee(iteratee, 2))\n        : array;\n    }\n\n    /**\n     * This method is like `_.pullAll` except that it accepts `comparator` which\n     * is invoked to compare elements of `array` to `values`. The comparator is\n     * invoked with two arguments: (arrVal, othVal).\n     *\n     * **Note:** Unlike `_.differenceWith`, this method mutates `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.6.0\n     * @category Array\n     * @param {Array} array The array to modify.\n     * @param {Array} values The values to remove.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns `array`.\n     * @example\n     *\n     * var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }];\n     *\n     * _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual);\n     * console.log(array);\n     * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }]\n     */\n    function pullAllWith(array, values, comparator) {\n      return (array && array.length && values && values.length)\n        ? basePullAll(array, values, undefined, comparator)\n        : array;\n    }\n\n    /**\n     * Removes elements from `array` corresponding to `indexes` and returns an\n     * array of removed elements.\n     *\n     * **Note:** Unlike `_.at`, this method mutates `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to modify.\n     * @param {...(number|number[])} [indexes] The indexes of elements to remove.\n     * @returns {Array} Returns the new array of removed elements.\n     * @example\n     *\n     * var array = ['a', 'b', 'c', 'd'];\n     * var pulled = _.pullAt(array, [1, 3]);\n     *\n     * console.log(array);\n     * // => ['a', 'c']\n     *\n     * console.log(pulled);\n     * // => ['b', 'd']\n     */\n    var pullAt = flatRest(function(array, indexes) {\n      var length = array == null ? 0 : array.length,\n          result = baseAt(array, indexes);\n\n      basePullAt(array, arrayMap(indexes, function(index) {\n        return isIndex(index, length) ? +index : index;\n      }).sort(compareAscending));\n\n      return result;\n    });\n\n    /**\n     * Removes all elements from `array` that `predicate` returns truthy for\n     * and returns an array of the removed elements. The predicate is invoked\n     * with three arguments: (value, index, array).\n     *\n     * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull`\n     * to pull elements from an array by value.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Array\n     * @param {Array} array The array to modify.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the new array of removed elements.\n     * @example\n     *\n     * var array = [1, 2, 3, 4];\n     * var evens = _.remove(array, function(n) {\n     *   return n % 2 == 0;\n     * });\n     *\n     * console.log(array);\n     * // => [1, 3]\n     *\n     * console.log(evens);\n     * // => [2, 4]\n     */\n    function remove(array, predicate) {\n      var result = [];\n      if (!(array && array.length)) {\n        return result;\n      }\n      var index = -1,\n          indexes = [],\n          length = array.length;\n\n      predicate = getIteratee(predicate, 3);\n      while (++index < length) {\n        var value = array[index];\n        if (predicate(value, index, array)) {\n          result.push(value);\n          indexes.push(index);\n        }\n      }\n      basePullAt(array, indexes);\n      return result;\n    }\n\n    /**\n     * Reverses `array` so that the first element becomes the last, the second\n     * element becomes the second to last, and so on.\n     *\n     * **Note:** This method mutates `array` and is based on\n     * [`Array#reverse`](https://mdn.io/Array/reverse).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to modify.\n     * @returns {Array} Returns `array`.\n     * @example\n     *\n     * var array = [1, 2, 3];\n     *\n     * _.reverse(array);\n     * // => [3, 2, 1]\n     *\n     * console.log(array);\n     * // => [3, 2, 1]\n     */\n    function reverse(array) {\n      return array == null ? array : nativeReverse.call(array);\n    }\n\n    /**\n     * Creates a slice of `array` from `start` up to, but not including, `end`.\n     *\n     * **Note:** This method is used instead of\n     * [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are\n     * returned.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to slice.\n     * @param {number} [start=0] The start position.\n     * @param {number} [end=array.length] The end position.\n     * @returns {Array} Returns the slice of `array`.\n     */\n    function slice(array, start, end) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return [];\n      }\n      if (end && typeof end != 'number' && isIterateeCall(array, start, end)) {\n        start = 0;\n        end = length;\n      }\n      else {\n        start = start == null ? 0 : toInteger(start);\n        end = end === undefined ? length : toInteger(end);\n      }\n      return baseSlice(array, start, end);\n    }\n\n    /**\n     * Uses a binary search to determine the lowest index at which `value`\n     * should be inserted into `array` in order to maintain its sort order.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The sorted array to inspect.\n     * @param {*} value The value to evaluate.\n     * @returns {number} Returns the index at which `value` should be inserted\n     *  into `array`.\n     * @example\n     *\n     * _.sortedIndex([30, 50], 40);\n     * // => 1\n     */\n    function sortedIndex(array, value) {\n      return baseSortedIndex(array, value);\n    }\n\n    /**\n     * This method is like `_.sortedIndex` except that it accepts `iteratee`\n     * which is invoked for `value` and each element of `array` to compute their\n     * sort ranking. The iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The sorted array to inspect.\n     * @param {*} value The value to evaluate.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {number} Returns the index at which `value` should be inserted\n     *  into `array`.\n     * @example\n     *\n     * var objects = [{ 'x': 4 }, { 'x': 5 }];\n     *\n     * _.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });\n     * // => 0\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.sortedIndexBy(objects, { 'x': 4 }, 'x');\n     * // => 0\n     */\n    function sortedIndexBy(array, value, iteratee) {\n      return baseSortedIndexBy(array, value, getIteratee(iteratee, 2));\n    }\n\n    /**\n     * This method is like `_.indexOf` except that it performs a binary\n     * search on a sorted `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {*} value The value to search for.\n     * @returns {number} Returns the index of the matched value, else `-1`.\n     * @example\n     *\n     * _.sortedIndexOf([4, 5, 5, 5, 6], 5);\n     * // => 1\n     */\n    function sortedIndexOf(array, value) {\n      var length = array == null ? 0 : array.length;\n      if (length) {\n        var index = baseSortedIndex(array, value);\n        if (index < length && eq(array[index], value)) {\n          return index;\n        }\n      }\n      return -1;\n    }\n\n    /**\n     * This method is like `_.sortedIndex` except that it returns the highest\n     * index at which `value` should be inserted into `array` in order to\n     * maintain its sort order.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The sorted array to inspect.\n     * @param {*} value The value to evaluate.\n     * @returns {number} Returns the index at which `value` should be inserted\n     *  into `array`.\n     * @example\n     *\n     * _.sortedLastIndex([4, 5, 5, 5, 6], 5);\n     * // => 4\n     */\n    function sortedLastIndex(array, value) {\n      return baseSortedIndex(array, value, true);\n    }\n\n    /**\n     * This method is like `_.sortedLastIndex` except that it accepts `iteratee`\n     * which is invoked for `value` and each element of `array` to compute their\n     * sort ranking. The iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The sorted array to inspect.\n     * @param {*} value The value to evaluate.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {number} Returns the index at which `value` should be inserted\n     *  into `array`.\n     * @example\n     *\n     * var objects = [{ 'x': 4 }, { 'x': 5 }];\n     *\n     * _.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });\n     * // => 1\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.sortedLastIndexBy(objects, { 'x': 4 }, 'x');\n     * // => 1\n     */\n    function sortedLastIndexBy(array, value, iteratee) {\n      return baseSortedIndexBy(array, value, getIteratee(iteratee, 2), true);\n    }\n\n    /**\n     * This method is like `_.lastIndexOf` except that it performs a binary\n     * search on a sorted `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {*} value The value to search for.\n     * @returns {number} Returns the index of the matched value, else `-1`.\n     * @example\n     *\n     * _.sortedLastIndexOf([4, 5, 5, 5, 6], 5);\n     * // => 3\n     */\n    function sortedLastIndexOf(array, value) {\n      var length = array == null ? 0 : array.length;\n      if (length) {\n        var index = baseSortedIndex(array, value, true) - 1;\n        if (eq(array[index], value)) {\n          return index;\n        }\n      }\n      return -1;\n    }\n\n    /**\n     * This method is like `_.uniq` except that it's designed and optimized\n     * for sorted arrays.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @returns {Array} Returns the new duplicate free array.\n     * @example\n     *\n     * _.sortedUniq([1, 1, 2]);\n     * // => [1, 2]\n     */\n    function sortedUniq(array) {\n      return (array && array.length)\n        ? baseSortedUniq(array)\n        : [];\n    }\n\n    /**\n     * This method is like `_.uniqBy` except that it's designed and optimized\n     * for sorted arrays.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {Function} [iteratee] The iteratee invoked per element.\n     * @returns {Array} Returns the new duplicate free array.\n     * @example\n     *\n     * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor);\n     * // => [1.1, 2.3]\n     */\n    function sortedUniqBy(array, iteratee) {\n      return (array && array.length)\n        ? baseSortedUniq(array, getIteratee(iteratee, 2))\n        : [];\n    }\n\n    /**\n     * Gets all but the first element of `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * _.tail([1, 2, 3]);\n     * // => [2, 3]\n     */\n    function tail(array) {\n      var length = array == null ? 0 : array.length;\n      return length ? baseSlice(array, 1, length) : [];\n    }\n\n    /**\n     * Creates a slice of `array` with `n` elements taken from the beginning.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {number} [n=1] The number of elements to take.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * _.take([1, 2, 3]);\n     * // => [1]\n     *\n     * _.take([1, 2, 3], 2);\n     * // => [1, 2]\n     *\n     * _.take([1, 2, 3], 5);\n     * // => [1, 2, 3]\n     *\n     * _.take([1, 2, 3], 0);\n     * // => []\n     */\n    function take(array, n, guard) {\n      if (!(array && array.length)) {\n        return [];\n      }\n      n = (guard || n === undefined) ? 1 : toInteger(n);\n      return baseSlice(array, 0, n < 0 ? 0 : n);\n    }\n\n    /**\n     * Creates a slice of `array` with `n` elements taken from the end.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {number} [n=1] The number of elements to take.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * _.takeRight([1, 2, 3]);\n     * // => [3]\n     *\n     * _.takeRight([1, 2, 3], 2);\n     * // => [2, 3]\n     *\n     * _.takeRight([1, 2, 3], 5);\n     * // => [1, 2, 3]\n     *\n     * _.takeRight([1, 2, 3], 0);\n     * // => []\n     */\n    function takeRight(array, n, guard) {\n      var length = array == null ? 0 : array.length;\n      if (!length) {\n        return [];\n      }\n      n = (guard || n === undefined) ? 1 : toInteger(n);\n      n = length - n;\n      return baseSlice(array, n < 0 ? 0 : n, length);\n    }\n\n    /**\n     * Creates a slice of `array` with elements taken from the end. Elements are\n     * taken until `predicate` returns falsey. The predicate is invoked with\n     * three arguments: (value, index, array).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'active': true },\n     *   { 'user': 'fred',    'active': false },\n     *   { 'user': 'pebbles', 'active': false }\n     * ];\n     *\n     * _.takeRightWhile(users, function(o) { return !o.active; });\n     * // => objects for ['fred', 'pebbles']\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false });\n     * // => objects for ['pebbles']\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.takeRightWhile(users, ['active', false]);\n     * // => objects for ['fred', 'pebbles']\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.takeRightWhile(users, 'active');\n     * // => []\n     */\n    function takeRightWhile(array, predicate) {\n      return (array && array.length)\n        ? baseWhile(array, getIteratee(predicate, 3), false, true)\n        : [];\n    }\n\n    /**\n     * Creates a slice of `array` with elements taken from the beginning. Elements\n     * are taken until `predicate` returns falsey. The predicate is invoked with\n     * three arguments: (value, index, array).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Array\n     * @param {Array} array The array to query.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the slice of `array`.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'active': false },\n     *   { 'user': 'fred',    'active': false },\n     *   { 'user': 'pebbles', 'active': true }\n     * ];\n     *\n     * _.takeWhile(users, function(o) { return !o.active; });\n     * // => objects for ['barney', 'fred']\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.takeWhile(users, { 'user': 'barney', 'active': false });\n     * // => objects for ['barney']\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.takeWhile(users, ['active', false]);\n     * // => objects for ['barney', 'fred']\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.takeWhile(users, 'active');\n     * // => []\n     */\n    function takeWhile(array, predicate) {\n      return (array && array.length)\n        ? baseWhile(array, getIteratee(predicate, 3))\n        : [];\n    }\n\n    /**\n     * Creates an array of unique values, in order, from all given arrays using\n     * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * for equality comparisons.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @returns {Array} Returns the new array of combined values.\n     * @example\n     *\n     * _.union([2], [1, 2]);\n     * // => [2, 1]\n     */\n    var union = baseRest(function(arrays) {\n      return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true));\n    });\n\n    /**\n     * This method is like `_.union` except that it accepts `iteratee` which is\n     * invoked for each element of each `arrays` to generate the criterion by\n     * which uniqueness is computed. Result values are chosen from the first\n     * array in which the value occurs. The iteratee is invoked with one argument:\n     * (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {Array} Returns the new array of combined values.\n     * @example\n     *\n     * _.unionBy([2.1], [1.2, 2.3], Math.floor);\n     * // => [2.1, 1.2]\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n     * // => [{ 'x': 1 }, { 'x': 2 }]\n     */\n    var unionBy = baseRest(function(arrays) {\n      var iteratee = last(arrays);\n      if (isArrayLikeObject(iteratee)) {\n        iteratee = undefined;\n      }\n      return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), getIteratee(iteratee, 2));\n    });\n\n    /**\n     * This method is like `_.union` except that it accepts `comparator` which\n     * is invoked to compare elements of `arrays`. Result values are chosen from\n     * the first array in which the value occurs. The comparator is invoked\n     * with two arguments: (arrVal, othVal).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new array of combined values.\n     * @example\n     *\n     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n     * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n     *\n     * _.unionWith(objects, others, _.isEqual);\n     * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]\n     */\n    var unionWith = baseRest(function(arrays) {\n      var comparator = last(arrays);\n      comparator = typeof comparator == 'function' ? comparator : undefined;\n      return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator);\n    });\n\n    /**\n     * Creates a duplicate-free version of an array, using\n     * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * for equality comparisons, in which only the first occurrence of each element\n     * is kept. The order of result values is determined by the order they occur\n     * in the array.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @returns {Array} Returns the new duplicate free array.\n     * @example\n     *\n     * _.uniq([2, 1, 2]);\n     * // => [2, 1]\n     */\n    function uniq(array) {\n      return (array && array.length) ? baseUniq(array) : [];\n    }\n\n    /**\n     * This method is like `_.uniq` except that it accepts `iteratee` which is\n     * invoked for each element in `array` to generate the criterion by which\n     * uniqueness is computed. The order of result values is determined by the\n     * order they occur in the array. The iteratee is invoked with one argument:\n     * (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {Array} Returns the new duplicate free array.\n     * @example\n     *\n     * _.uniqBy([2.1, 1.2, 2.3], Math.floor);\n     * // => [2.1, 1.2]\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');\n     * // => [{ 'x': 1 }, { 'x': 2 }]\n     */\n    function uniqBy(array, iteratee) {\n      return (array && array.length) ? baseUniq(array, getIteratee(iteratee, 2)) : [];\n    }\n\n    /**\n     * This method is like `_.uniq` except that it accepts `comparator` which\n     * is invoked to compare elements of `array`. The order of result values is\n     * determined by the order they occur in the array.The comparator is invoked\n     * with two arguments: (arrVal, othVal).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new duplicate free array.\n     * @example\n     *\n     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];\n     *\n     * _.uniqWith(objects, _.isEqual);\n     * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]\n     */\n    function uniqWith(array, comparator) {\n      comparator = typeof comparator == 'function' ? comparator : undefined;\n      return (array && array.length) ? baseUniq(array, undefined, comparator) : [];\n    }\n\n    /**\n     * This method is like `_.zip` except that it accepts an array of grouped\n     * elements and creates an array regrouping the elements to their pre-zip\n     * configuration.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.2.0\n     * @category Array\n     * @param {Array} array The array of grouped elements to process.\n     * @returns {Array} Returns the new array of regrouped elements.\n     * @example\n     *\n     * var zipped = _.zip(['a', 'b'], [1, 2], [true, false]);\n     * // => [['a', 1, true], ['b', 2, false]]\n     *\n     * _.unzip(zipped);\n     * // => [['a', 'b'], [1, 2], [true, false]]\n     */\n    function unzip(array) {\n      if (!(array && array.length)) {\n        return [];\n      }\n      var length = 0;\n      array = arrayFilter(array, function(group) {\n        if (isArrayLikeObject(group)) {\n          length = nativeMax(group.length, length);\n          return true;\n        }\n      });\n      return baseTimes(length, function(index) {\n        return arrayMap(array, baseProperty(index));\n      });\n    }\n\n    /**\n     * This method is like `_.unzip` except that it accepts `iteratee` to specify\n     * how regrouped values should be combined. The iteratee is invoked with the\n     * elements of each group: (...group).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.8.0\n     * @category Array\n     * @param {Array} array The array of grouped elements to process.\n     * @param {Function} [iteratee=_.identity] The function to combine\n     *  regrouped values.\n     * @returns {Array} Returns the new array of regrouped elements.\n     * @example\n     *\n     * var zipped = _.zip([1, 2], [10, 20], [100, 200]);\n     * // => [[1, 10, 100], [2, 20, 200]]\n     *\n     * _.unzipWith(zipped, _.add);\n     * // => [3, 30, 300]\n     */\n    function unzipWith(array, iteratee) {\n      if (!(array && array.length)) {\n        return [];\n      }\n      var result = unzip(array);\n      if (iteratee == null) {\n        return result;\n      }\n      return arrayMap(result, function(group) {\n        return apply(iteratee, undefined, group);\n      });\n    }\n\n    /**\n     * Creates an array excluding all given values using\n     * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * for equality comparisons.\n     *\n     * **Note:** Unlike `_.pull`, this method returns a new array.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {Array} array The array to inspect.\n     * @param {...*} [values] The values to exclude.\n     * @returns {Array} Returns the new array of filtered values.\n     * @see _.difference, _.xor\n     * @example\n     *\n     * _.without([2, 1, 2, 3], 1, 2);\n     * // => [3]\n     */\n    var without = baseRest(function(array, values) {\n      return isArrayLikeObject(array)\n        ? baseDifference(array, values)\n        : [];\n    });\n\n    /**\n     * Creates an array of unique values that is the\n     * [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)\n     * of the given arrays. The order of result values is determined by the order\n     * they occur in the arrays.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.4.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @returns {Array} Returns the new array of filtered values.\n     * @see _.difference, _.without\n     * @example\n     *\n     * _.xor([2, 1], [2, 3]);\n     * // => [1, 3]\n     */\n    var xor = baseRest(function(arrays) {\n      return baseXor(arrayFilter(arrays, isArrayLikeObject));\n    });\n\n    /**\n     * This method is like `_.xor` except that it accepts `iteratee` which is\n     * invoked for each element of each `arrays` to generate the criterion by\n     * which by which they're compared. The order of result values is determined\n     * by the order they occur in the arrays. The iteratee is invoked with one\n     * argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {Array} Returns the new array of filtered values.\n     * @example\n     *\n     * _.xorBy([2.1, 1.2], [2.3, 3.4], Math.floor);\n     * // => [1.2, 3.4]\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n     * // => [{ 'x': 2 }]\n     */\n    var xorBy = baseRest(function(arrays) {\n      var iteratee = last(arrays);\n      if (isArrayLikeObject(iteratee)) {\n        iteratee = undefined;\n      }\n      return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee, 2));\n    });\n\n    /**\n     * This method is like `_.xor` except that it accepts `comparator` which is\n     * invoked to compare elements of `arrays`. The order of result values is\n     * determined by the order they occur in the arrays. The comparator is invoked\n     * with two arguments: (arrVal, othVal).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to inspect.\n     * @param {Function} [comparator] The comparator invoked per element.\n     * @returns {Array} Returns the new array of filtered values.\n     * @example\n     *\n     * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n     * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n     *\n     * _.xorWith(objects, others, _.isEqual);\n     * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]\n     */\n    var xorWith = baseRest(function(arrays) {\n      var comparator = last(arrays);\n      comparator = typeof comparator == 'function' ? comparator : undefined;\n      return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator);\n    });\n\n    /**\n     * Creates an array of grouped elements, the first of which contains the\n     * first elements of the given arrays, the second of which contains the\n     * second elements of the given arrays, and so on.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to process.\n     * @returns {Array} Returns the new array of grouped elements.\n     * @example\n     *\n     * _.zip(['a', 'b'], [1, 2], [true, false]);\n     * // => [['a', 1, true], ['b', 2, false]]\n     */\n    var zip = baseRest(unzip);\n\n    /**\n     * This method is like `_.fromPairs` except that it accepts two arrays,\n     * one of property identifiers and one of corresponding values.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.4.0\n     * @category Array\n     * @param {Array} [props=[]] The property identifiers.\n     * @param {Array} [values=[]] The property values.\n     * @returns {Object} Returns the new object.\n     * @example\n     *\n     * _.zipObject(['a', 'b'], [1, 2]);\n     * // => { 'a': 1, 'b': 2 }\n     */\n    function zipObject(props, values) {\n      return baseZipObject(props || [], values || [], assignValue);\n    }\n\n    /**\n     * This method is like `_.zipObject` except that it supports property paths.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.1.0\n     * @category Array\n     * @param {Array} [props=[]] The property identifiers.\n     * @param {Array} [values=[]] The property values.\n     * @returns {Object} Returns the new object.\n     * @example\n     *\n     * _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]);\n     * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } }\n     */\n    function zipObjectDeep(props, values) {\n      return baseZipObject(props || [], values || [], baseSet);\n    }\n\n    /**\n     * This method is like `_.zip` except that it accepts `iteratee` to specify\n     * how grouped values should be combined. The iteratee is invoked with the\n     * elements of each group: (...group).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.8.0\n     * @category Array\n     * @param {...Array} [arrays] The arrays to process.\n     * @param {Function} [iteratee=_.identity] The function to combine\n     *  grouped values.\n     * @returns {Array} Returns the new array of grouped elements.\n     * @example\n     *\n     * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) {\n     *   return a + b + c;\n     * });\n     * // => [111, 222]\n     */\n    var zipWith = baseRest(function(arrays) {\n      var length = arrays.length,\n          iteratee = length > 1 ? arrays[length - 1] : undefined;\n\n      iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined;\n      return unzipWith(arrays, iteratee);\n    });\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates a `lodash` wrapper instance that wraps `value` with explicit method\n     * chain sequences enabled. The result of such sequences must be unwrapped\n     * with `_#value`.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.3.0\n     * @category Seq\n     * @param {*} value The value to wrap.\n     * @returns {Object} Returns the new `lodash` wrapper instance.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'age': 36 },\n     *   { 'user': 'fred',    'age': 40 },\n     *   { 'user': 'pebbles', 'age': 1 }\n     * ];\n     *\n     * var youngest = _\n     *   .chain(users)\n     *   .sortBy('age')\n     *   .map(function(o) {\n     *     return o.user + ' is ' + o.age;\n     *   })\n     *   .head()\n     *   .value();\n     * // => 'pebbles is 1'\n     */\n    function chain(value) {\n      var result = lodash(value);\n      result.__chain__ = true;\n      return result;\n    }\n\n    /**\n     * This method invokes `interceptor` and returns `value`. The interceptor\n     * is invoked with one argument; (value). The purpose of this method is to\n     * \"tap into\" a method chain sequence in order to modify intermediate results.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Seq\n     * @param {*} value The value to provide to `interceptor`.\n     * @param {Function} interceptor The function to invoke.\n     * @returns {*} Returns `value`.\n     * @example\n     *\n     * _([1, 2, 3])\n     *  .tap(function(array) {\n     *    // Mutate input array.\n     *    array.pop();\n     *  })\n     *  .reverse()\n     *  .value();\n     * // => [2, 1]\n     */\n    function tap(value, interceptor) {\n      interceptor(value);\n      return value;\n    }\n\n    /**\n     * This method is like `_.tap` except that it returns the result of `interceptor`.\n     * The purpose of this method is to \"pass thru\" values replacing intermediate\n     * results in a method chain sequence.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Seq\n     * @param {*} value The value to provide to `interceptor`.\n     * @param {Function} interceptor The function to invoke.\n     * @returns {*} Returns the result of `interceptor`.\n     * @example\n     *\n     * _('  abc  ')\n     *  .chain()\n     *  .trim()\n     *  .thru(function(value) {\n     *    return [value];\n     *  })\n     *  .value();\n     * // => ['abc']\n     */\n    function thru(value, interceptor) {\n      return interceptor(value);\n    }\n\n    /**\n     * This method is the wrapper version of `_.at`.\n     *\n     * @name at\n     * @memberOf _\n     * @since 1.0.0\n     * @category Seq\n     * @param {...(string|string[])} [paths] The property paths to pick.\n     * @returns {Object} Returns the new `lodash` wrapper instance.\n     * @example\n     *\n     * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };\n     *\n     * _(object).at(['a[0].b.c', 'a[1]']).value();\n     * // => [3, 4]\n     */\n    var wrapperAt = flatRest(function(paths) {\n      var length = paths.length,\n          start = length ? paths[0] : 0,\n          value = this.__wrapped__,\n          interceptor = function(object) { return baseAt(object, paths); };\n\n      if (length > 1 || this.__actions__.length ||\n          !(value instanceof LazyWrapper) || !isIndex(start)) {\n        return this.thru(interceptor);\n      }\n      value = value.slice(start, +start + (length ? 1 : 0));\n      value.__actions__.push({\n        'func': thru,\n        'args': [interceptor],\n        'thisArg': undefined\n      });\n      return new LodashWrapper(value, this.__chain__).thru(function(array) {\n        if (length && !array.length) {\n          array.push(undefined);\n        }\n        return array;\n      });\n    });\n\n    /**\n     * Creates a `lodash` wrapper instance with explicit method chain sequences enabled.\n     *\n     * @name chain\n     * @memberOf _\n     * @since 0.1.0\n     * @category Seq\n     * @returns {Object} Returns the new `lodash` wrapper instance.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney', 'age': 36 },\n     *   { 'user': 'fred',   'age': 40 }\n     * ];\n     *\n     * // A sequence without explicit chaining.\n     * _(users).head();\n     * // => { 'user': 'barney', 'age': 36 }\n     *\n     * // A sequence with explicit chaining.\n     * _(users)\n     *   .chain()\n     *   .head()\n     *   .pick('user')\n     *   .value();\n     * // => { 'user': 'barney' }\n     */\n    function wrapperChain() {\n      return chain(this);\n    }\n\n    /**\n     * Executes the chain sequence and returns the wrapped result.\n     *\n     * @name commit\n     * @memberOf _\n     * @since 3.2.0\n     * @category Seq\n     * @returns {Object} Returns the new `lodash` wrapper instance.\n     * @example\n     *\n     * var array = [1, 2];\n     * var wrapped = _(array).push(3);\n     *\n     * console.log(array);\n     * // => [1, 2]\n     *\n     * wrapped = wrapped.commit();\n     * console.log(array);\n     * // => [1, 2, 3]\n     *\n     * wrapped.last();\n     * // => 3\n     *\n     * console.log(array);\n     * // => [1, 2, 3]\n     */\n    function wrapperCommit() {\n      return new LodashWrapper(this.value(), this.__chain__);\n    }\n\n    /**\n     * Gets the next value on a wrapped object following the\n     * [iterator protocol](https://mdn.io/iteration_protocols#iterator).\n     *\n     * @name next\n     * @memberOf _\n     * @since 4.0.0\n     * @category Seq\n     * @returns {Object} Returns the next iterator value.\n     * @example\n     *\n     * var wrapped = _([1, 2]);\n     *\n     * wrapped.next();\n     * // => { 'done': false, 'value': 1 }\n     *\n     * wrapped.next();\n     * // => { 'done': false, 'value': 2 }\n     *\n     * wrapped.next();\n     * // => { 'done': true, 'value': undefined }\n     */\n    function wrapperNext() {\n      if (this.__values__ === undefined) {\n        this.__values__ = toArray(this.value());\n      }\n      var done = this.__index__ >= this.__values__.length,\n          value = done ? undefined : this.__values__[this.__index__++];\n\n      return { 'done': done, 'value': value };\n    }\n\n    /**\n     * Enables the wrapper to be iterable.\n     *\n     * @name Symbol.iterator\n     * @memberOf _\n     * @since 4.0.0\n     * @category Seq\n     * @returns {Object} Returns the wrapper object.\n     * @example\n     *\n     * var wrapped = _([1, 2]);\n     *\n     * wrapped[Symbol.iterator]() === wrapped;\n     * // => true\n     *\n     * Array.from(wrapped);\n     * // => [1, 2]\n     */\n    function wrapperToIterator() {\n      return this;\n    }\n\n    /**\n     * Creates a clone of the chain sequence planting `value` as the wrapped value.\n     *\n     * @name plant\n     * @memberOf _\n     * @since 3.2.0\n     * @category Seq\n     * @param {*} value The value to plant.\n     * @returns {Object} Returns the new `lodash` wrapper instance.\n     * @example\n     *\n     * function square(n) {\n     *   return n * n;\n     * }\n     *\n     * var wrapped = _([1, 2]).map(square);\n     * var other = wrapped.plant([3, 4]);\n     *\n     * other.value();\n     * // => [9, 16]\n     *\n     * wrapped.value();\n     * // => [1, 4]\n     */\n    function wrapperPlant(value) {\n      var result,\n          parent = this;\n\n      while (parent instanceof baseLodash) {\n        var clone = wrapperClone(parent);\n        clone.__index__ = 0;\n        clone.__values__ = undefined;\n        if (result) {\n          previous.__wrapped__ = clone;\n        } else {\n          result = clone;\n        }\n        var previous = clone;\n        parent = parent.__wrapped__;\n      }\n      previous.__wrapped__ = value;\n      return result;\n    }\n\n    /**\n     * This method is the wrapper version of `_.reverse`.\n     *\n     * **Note:** This method mutates the wrapped array.\n     *\n     * @name reverse\n     * @memberOf _\n     * @since 0.1.0\n     * @category Seq\n     * @returns {Object} Returns the new `lodash` wrapper instance.\n     * @example\n     *\n     * var array = [1, 2, 3];\n     *\n     * _(array).reverse().value()\n     * // => [3, 2, 1]\n     *\n     * console.log(array);\n     * // => [3, 2, 1]\n     */\n    function wrapperReverse() {\n      var value = this.__wrapped__;\n      if (value instanceof LazyWrapper) {\n        var wrapped = value;\n        if (this.__actions__.length) {\n          wrapped = new LazyWrapper(this);\n        }\n        wrapped = wrapped.reverse();\n        wrapped.__actions__.push({\n          'func': thru,\n          'args': [reverse],\n          'thisArg': undefined\n        });\n        return new LodashWrapper(wrapped, this.__chain__);\n      }\n      return this.thru(reverse);\n    }\n\n    /**\n     * Executes the chain sequence to resolve the unwrapped value.\n     *\n     * @name value\n     * @memberOf _\n     * @since 0.1.0\n     * @alias toJSON, valueOf\n     * @category Seq\n     * @returns {*} Returns the resolved unwrapped value.\n     * @example\n     *\n     * _([1, 2, 3]).value();\n     * // => [1, 2, 3]\n     */\n    function wrapperValue() {\n      return baseWrapperValue(this.__wrapped__, this.__actions__);\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Creates an object composed of keys generated from the results of running\n     * each element of `collection` thru `iteratee`. The corresponding value of\n     * each key is the number of times the key was returned by `iteratee`. The\n     * iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.5.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The iteratee to transform keys.\n     * @returns {Object} Returns the composed aggregate object.\n     * @example\n     *\n     * _.countBy([6.1, 4.2, 6.3], Math.floor);\n     * // => { '4': 1, '6': 2 }\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.countBy(['one', 'two', 'three'], 'length');\n     * // => { '3': 2, '5': 1 }\n     */\n    var countBy = createAggregator(function(result, value, key) {\n      if (hasOwnProperty.call(result, key)) {\n        ++result[key];\n      } else {\n        baseAssignValue(result, key, 1);\n      }\n    });\n\n    /**\n     * Checks if `predicate` returns truthy for **all** elements of `collection`.\n     * Iteration is stopped once `predicate` returns falsey. The predicate is\n     * invoked with three arguments: (value, index|key, collection).\n     *\n     * **Note:** This method returns `true` for\n     * [empty collections](https://en.wikipedia.org/wiki/Empty_set) because\n     * [everything is true](https://en.wikipedia.org/wiki/Vacuous_truth) of\n     * elements of empty collections.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {boolean} Returns `true` if all elements pass the predicate check,\n     *  else `false`.\n     * @example\n     *\n     * _.every([true, 1, null, 'yes'], Boolean);\n     * // => false\n     *\n     * var users = [\n     *   { 'user': 'barney', 'age': 36, 'active': false },\n     *   { 'user': 'fred',   'age': 40, 'active': false }\n     * ];\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.every(users, { 'user': 'barney', 'active': false });\n     * // => false\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.every(users, ['active', false]);\n     * // => true\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.every(users, 'active');\n     * // => false\n     */\n    function every(collection, predicate, guard) {\n      var func = isArray(collection) ? arrayEvery : baseEvery;\n      if (guard && isIterateeCall(collection, predicate, guard)) {\n        predicate = undefined;\n      }\n      return func(collection, getIteratee(predicate, 3));\n    }\n\n    /**\n     * Iterates over elements of `collection`, returning an array of all elements\n     * `predicate` returns truthy for. The predicate is invoked with three\n     * arguments: (value, index|key, collection).\n     *\n     * **Note:** Unlike `_.remove`, this method returns a new array.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the new filtered array.\n     * @see _.reject\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney', 'age': 36, 'active': true },\n     *   { 'user': 'fred',   'age': 40, 'active': false }\n     * ];\n     *\n     * _.filter(users, function(o) { return !o.active; });\n     * // => objects for ['fred']\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.filter(users, { 'age': 36, 'active': true });\n     * // => objects for ['barney']\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.filter(users, ['active', false]);\n     * // => objects for ['fred']\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.filter(users, 'active');\n     * // => objects for ['barney']\n     *\n     * // Combining several predicates using `_.overEvery` or `_.overSome`.\n     * _.filter(users, _.overSome([{ 'age': 36 }, ['age', 40]]));\n     * // => objects for ['fred', 'barney']\n     */\n    function filter(collection, predicate) {\n      var func = isArray(collection) ? arrayFilter : baseFilter;\n      return func(collection, getIteratee(predicate, 3));\n    }\n\n    /**\n     * Iterates over elements of `collection`, returning the first element\n     * `predicate` returns truthy for. The predicate is invoked with three\n     * arguments: (value, index|key, collection).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to inspect.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @param {number} [fromIndex=0] The index to search from.\n     * @returns {*} Returns the matched element, else `undefined`.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'age': 36, 'active': true },\n     *   { 'user': 'fred',    'age': 40, 'active': false },\n     *   { 'user': 'pebbles', 'age': 1,  'active': true }\n     * ];\n     *\n     * _.find(users, function(o) { return o.age < 40; });\n     * // => object for 'barney'\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.find(users, { 'age': 1, 'active': true });\n     * // => object for 'pebbles'\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.find(users, ['active', false]);\n     * // => object for 'fred'\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.find(users, 'active');\n     * // => object for 'barney'\n     */\n    var find = createFind(findIndex);\n\n    /**\n     * This method is like `_.find` except that it iterates over elements of\n     * `collection` from right to left.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to inspect.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @param {number} [fromIndex=collection.length-1] The index to search from.\n     * @returns {*} Returns the matched element, else `undefined`.\n     * @example\n     *\n     * _.findLast([1, 2, 3, 4], function(n) {\n     *   return n % 2 == 1;\n     * });\n     * // => 3\n     */\n    var findLast = createFind(findLastIndex);\n\n    /**\n     * Creates a flattened array of values by running each element in `collection`\n     * thru `iteratee` and flattening the mapped results. The iteratee is invoked\n     * with three arguments: (value, index|key, collection).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the new flattened array.\n     * @example\n     *\n     * function duplicate(n) {\n     *   return [n, n];\n     * }\n     *\n     * _.flatMap([1, 2], duplicate);\n     * // => [1, 1, 2, 2]\n     */\n    function flatMap(collection, iteratee) {\n      return baseFlatten(map(collection, iteratee), 1);\n    }\n\n    /**\n     * This method is like `_.flatMap` except that it recursively flattens the\n     * mapped results.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.7.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the new flattened array.\n     * @example\n     *\n     * function duplicate(n) {\n     *   return [[[n, n]]];\n     * }\n     *\n     * _.flatMapDeep([1, 2], duplicate);\n     * // => [1, 1, 2, 2]\n     */\n    function flatMapDeep(collection, iteratee) {\n      return baseFlatten(map(collection, iteratee), INFINITY);\n    }\n\n    /**\n     * This method is like `_.flatMap` except that it recursively flattens the\n     * mapped results up to `depth` times.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.7.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @param {number} [depth=1] The maximum recursion depth.\n     * @returns {Array} Returns the new flattened array.\n     * @example\n     *\n     * function duplicate(n) {\n     *   return [[[n, n]]];\n     * }\n     *\n     * _.flatMapDepth([1, 2], duplicate, 2);\n     * // => [[1, 1], [2, 2]]\n     */\n    function flatMapDepth(collection, iteratee, depth) {\n      depth = depth === undefined ? 1 : toInteger(depth);\n      return baseFlatten(map(collection, iteratee), depth);\n    }\n\n    /**\n     * Iterates over elements of `collection` and invokes `iteratee` for each element.\n     * The iteratee is invoked with three arguments: (value, index|key, collection).\n     * Iteratee functions may exit iteration early by explicitly returning `false`.\n     *\n     * **Note:** As with other \"Collections\" methods, objects with a \"length\"\n     * property are iterated like arrays. To avoid this behavior use `_.forIn`\n     * or `_.forOwn` for object iteration.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @alias each\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Array|Object} Returns `collection`.\n     * @see _.forEachRight\n     * @example\n     *\n     * _.forEach([1, 2], function(value) {\n     *   console.log(value);\n     * });\n     * // => Logs `1` then `2`.\n     *\n     * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) {\n     *   console.log(key);\n     * });\n     * // => Logs 'a' then 'b' (iteration order is not guaranteed).\n     */\n    function forEach(collection, iteratee) {\n      var func = isArray(collection) ? arrayEach : baseEach;\n      return func(collection, getIteratee(iteratee, 3));\n    }\n\n    /**\n     * This method is like `_.forEach` except that it iterates over elements of\n     * `collection` from right to left.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @alias eachRight\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Array|Object} Returns `collection`.\n     * @see _.forEach\n     * @example\n     *\n     * _.forEachRight([1, 2], function(value) {\n     *   console.log(value);\n     * });\n     * // => Logs `2` then `1`.\n     */\n    function forEachRight(collection, iteratee) {\n      var func = isArray(collection) ? arrayEachRight : baseEachRight;\n      return func(collection, getIteratee(iteratee, 3));\n    }\n\n    /**\n     * Creates an object composed of keys generated from the results of running\n     * each element of `collection` thru `iteratee`. The order of grouped values\n     * is determined by the order they occur in `collection`. The corresponding\n     * value of each key is an array of elements responsible for generating the\n     * key. The iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The iteratee to transform keys.\n     * @returns {Object} Returns the composed aggregate object.\n     * @example\n     *\n     * _.groupBy([6.1, 4.2, 6.3], Math.floor);\n     * // => { '4': [4.2], '6': [6.1, 6.3] }\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.groupBy(['one', 'two', 'three'], 'length');\n     * // => { '3': ['one', 'two'], '5': ['three'] }\n     */\n    var groupBy = createAggregator(function(result, value, key) {\n      if (hasOwnProperty.call(result, key)) {\n        result[key].push(value);\n      } else {\n        baseAssignValue(result, key, [value]);\n      }\n    });\n\n    /**\n     * Checks if `value` is in `collection`. If `collection` is a string, it's\n     * checked for a substring of `value`, otherwise\n     * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * is used for equality comparisons. If `fromIndex` is negative, it's used as\n     * the offset from the end of `collection`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object|string} collection The collection to inspect.\n     * @param {*} value The value to search for.\n     * @param {number} [fromIndex=0] The index to search from.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`.\n     * @returns {boolean} Returns `true` if `value` is found, else `false`.\n     * @example\n     *\n     * _.includes([1, 2, 3], 1);\n     * // => true\n     *\n     * _.includes([1, 2, 3], 1, 2);\n     * // => false\n     *\n     * _.includes({ 'a': 1, 'b': 2 }, 1);\n     * // => true\n     *\n     * _.includes('abcd', 'bc');\n     * // => true\n     */\n    function includes(collection, value, fromIndex, guard) {\n      collection = isArrayLike(collection) ? collection : values(collection);\n      fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0;\n\n      var length = collection.length;\n      if (fromIndex < 0) {\n        fromIndex = nativeMax(length + fromIndex, 0);\n      }\n      return isString(collection)\n        ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1)\n        : (!!length && baseIndexOf(collection, value, fromIndex) > -1);\n    }\n\n    /**\n     * Invokes the method at `path` of each element in `collection`, returning\n     * an array of the results of each invoked method. Any additional arguments\n     * are provided to each invoked method. If `path` is a function, it's invoked\n     * for, and `this` bound to, each element in `collection`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Array|Function|string} path The path of the method to invoke or\n     *  the function invoked per iteration.\n     * @param {...*} [args] The arguments to invoke each method with.\n     * @returns {Array} Returns the array of results.\n     * @example\n     *\n     * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort');\n     * // => [[1, 5, 7], [1, 2, 3]]\n     *\n     * _.invokeMap([123, 456], String.prototype.split, '');\n     * // => [['1', '2', '3'], ['4', '5', '6']]\n     */\n    var invokeMap = baseRest(function(collection, path, args) {\n      var index = -1,\n          isFunc = typeof path == 'function',\n          result = isArrayLike(collection) ? Array(collection.length) : [];\n\n      baseEach(collection, function(value) {\n        result[++index] = isFunc ? apply(path, value, args) : baseInvoke(value, path, args);\n      });\n      return result;\n    });\n\n    /**\n     * Creates an object composed of keys generated from the results of running\n     * each element of `collection` thru `iteratee`. The corresponding value of\n     * each key is the last element responsible for generating the key. The\n     * iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The iteratee to transform keys.\n     * @returns {Object} Returns the composed aggregate object.\n     * @example\n     *\n     * var array = [\n     *   { 'dir': 'left', 'code': 97 },\n     *   { 'dir': 'right', 'code': 100 }\n     * ];\n     *\n     * _.keyBy(array, function(o) {\n     *   return String.fromCharCode(o.code);\n     * });\n     * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } }\n     *\n     * _.keyBy(array, 'dir');\n     * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } }\n     */\n    var keyBy = createAggregator(function(result, value, key) {\n      baseAssignValue(result, key, value);\n    });\n\n    /**\n     * Creates an array of values by running each element in `collection` thru\n     * `iteratee`. The iteratee is invoked with three arguments:\n     * (value, index|key, collection).\n     *\n     * Many lodash methods are guarded to work as iteratees for methods like\n     * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`.\n     *\n     * The guarded methods are:\n     * `ary`, `chunk`, `curry`, `curryRight`, `drop`, `dropRight`, `every`,\n     * `fill`, `invert`, `parseInt`, `random`, `range`, `rangeRight`, `repeat`,\n     * `sampleSize`, `slice`, `some`, `sortBy`, `split`, `take`, `takeRight`,\n     * `template`, `trim`, `trimEnd`, `trimStart`, and `words`\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the new mapped array.\n     * @example\n     *\n     * function square(n) {\n     *   return n * n;\n     * }\n     *\n     * _.map([4, 8], square);\n     * // => [16, 64]\n     *\n     * _.map({ 'a': 4, 'b': 8 }, square);\n     * // => [16, 64] (iteration order is not guaranteed)\n     *\n     * var users = [\n     *   { 'user': 'barney' },\n     *   { 'user': 'fred' }\n     * ];\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.map(users, 'user');\n     * // => ['barney', 'fred']\n     */\n    function map(collection, iteratee) {\n      var func = isArray(collection) ? arrayMap : baseMap;\n      return func(collection, getIteratee(iteratee, 3));\n    }\n\n    /**\n     * This method is like `_.sortBy` except that it allows specifying the sort\n     * orders of the iteratees to sort by. If `orders` is unspecified, all values\n     * are sorted in ascending order. Otherwise, specify an order of \"desc\" for\n     * descending or \"asc\" for ascending sort order of corresponding values.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Array[]|Function[]|Object[]|string[]} [iteratees=[_.identity]]\n     *  The iteratees to sort by.\n     * @param {string[]} [orders] The sort orders of `iteratees`.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`.\n     * @returns {Array} Returns the new sorted array.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'fred',   'age': 48 },\n     *   { 'user': 'barney', 'age': 34 },\n     *   { 'user': 'fred',   'age': 40 },\n     *   { 'user': 'barney', 'age': 36 }\n     * ];\n     *\n     * // Sort by `user` in ascending order and by `age` in descending order.\n     * _.orderBy(users, ['user', 'age'], ['asc', 'desc']);\n     * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]\n     */\n    function orderBy(collection, iteratees, orders, guard) {\n      if (collection == null) {\n        return [];\n      }\n      if (!isArray(iteratees)) {\n        iteratees = iteratees == null ? [] : [iteratees];\n      }\n      orders = guard ? undefined : orders;\n      if (!isArray(orders)) {\n        orders = orders == null ? [] : [orders];\n      }\n      return baseOrderBy(collection, iteratees, orders);\n    }\n\n    /**\n     * Creates an array of elements split into two groups, the first of which\n     * contains elements `predicate` returns truthy for, the second of which\n     * contains elements `predicate` returns falsey for. The predicate is\n     * invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the array of grouped elements.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney',  'age': 36, 'active': false },\n     *   { 'user': 'fred',    'age': 40, 'active': true },\n     *   { 'user': 'pebbles', 'age': 1,  'active': false }\n     * ];\n     *\n     * _.partition(users, function(o) { return o.active; });\n     * // => objects for [['fred'], ['barney', 'pebbles']]\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.partition(users, { 'age': 1, 'active': false });\n     * // => objects for [['pebbles'], ['barney', 'fred']]\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.partition(users, ['active', false]);\n     * // => objects for [['barney', 'pebbles'], ['fred']]\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.partition(users, 'active');\n     * // => objects for [['fred'], ['barney', 'pebbles']]\n     */\n    var partition = createAggregator(function(result, value, key) {\n      result[key ? 0 : 1].push(value);\n    }, function() { return [[], []]; });\n\n    /**\n     * Reduces `collection` to a value which is the accumulated result of running\n     * each element in `collection` thru `iteratee`, where each successive\n     * invocation is supplied the return value of the previous. If `accumulator`\n     * is not given, the first element of `collection` is used as the initial\n     * value. The iteratee is invoked with four arguments:\n     * (accumulator, value, index|key, collection).\n     *\n     * Many lodash methods are guarded to work as iteratees for methods like\n     * `_.reduce`, `_.reduceRight`, and `_.transform`.\n     *\n     * The guarded methods are:\n     * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`,\n     * and `sortBy`\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @param {*} [accumulator] The initial value.\n     * @returns {*} Returns the accumulated value.\n     * @see _.reduceRight\n     * @example\n     *\n     * _.reduce([1, 2], function(sum, n) {\n     *   return sum + n;\n     * }, 0);\n     * // => 3\n     *\n     * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {\n     *   (result[value] || (result[value] = [])).push(key);\n     *   return result;\n     * }, {});\n     * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed)\n     */\n    function reduce(collection, iteratee, accumulator) {\n      var func = isArray(collection) ? arrayReduce : baseReduce,\n          initAccum = arguments.length < 3;\n\n      return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach);\n    }\n\n    /**\n     * This method is like `_.reduce` except that it iterates over elements of\n     * `collection` from right to left.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @param {*} [accumulator] The initial value.\n     * @returns {*} Returns the accumulated value.\n     * @see _.reduce\n     * @example\n     *\n     * var array = [[0, 1], [2, 3], [4, 5]];\n     *\n     * _.reduceRight(array, function(flattened, other) {\n     *   return flattened.concat(other);\n     * }, []);\n     * // => [4, 5, 2, 3, 0, 1]\n     */\n    function reduceRight(collection, iteratee, accumulator) {\n      var func = isArray(collection) ? arrayReduceRight : baseReduce,\n          initAccum = arguments.length < 3;\n\n      return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight);\n    }\n\n    /**\n     * The opposite of `_.filter`; this method returns the elements of `collection`\n     * that `predicate` does **not** return truthy for.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the new filtered array.\n     * @see _.filter\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney', 'age': 36, 'active': false },\n     *   { 'user': 'fred',   'age': 40, 'active': true }\n     * ];\n     *\n     * _.reject(users, function(o) { return !o.active; });\n     * // => objects for ['fred']\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.reject(users, { 'age': 40, 'active': true });\n     * // => objects for ['barney']\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.reject(users, ['active', false]);\n     * // => objects for ['fred']\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.reject(users, 'active');\n     * // => objects for ['barney']\n     */\n    function reject(collection, predicate) {\n      var func = isArray(collection) ? arrayFilter : baseFilter;\n      return func(collection, negate(getIteratee(predicate, 3)));\n    }\n\n    /**\n     * Gets a random element from `collection`.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to sample.\n     * @returns {*} Returns the random element.\n     * @example\n     *\n     * _.sample([1, 2, 3, 4]);\n     * // => 2\n     */\n    function sample(collection) {\n      var func = isArray(collection) ? arraySample : baseSample;\n      return func(collection);\n    }\n\n    /**\n     * Gets `n` random elements at unique keys from `collection` up to the\n     * size of `collection`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to sample.\n     * @param {number} [n=1] The number of elements to sample.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Array} Returns the random elements.\n     * @example\n     *\n     * _.sampleSize([1, 2, 3], 2);\n     * // => [3, 1]\n     *\n     * _.sampleSize([1, 2, 3], 4);\n     * // => [2, 3, 1]\n     */\n    function sampleSize(collection, n, guard) {\n      if ((guard ? isIterateeCall(collection, n, guard) : n === undefined)) {\n        n = 1;\n      } else {\n        n = toInteger(n);\n      }\n      var func = isArray(collection) ? arraySampleSize : baseSampleSize;\n      return func(collection, n);\n    }\n\n    /**\n     * Creates an array of shuffled values, using a version of the\n     * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to shuffle.\n     * @returns {Array} Returns the new shuffled array.\n     * @example\n     *\n     * _.shuffle([1, 2, 3, 4]);\n     * // => [4, 1, 3, 2]\n     */\n    function shuffle(collection) {\n      var func = isArray(collection) ? arrayShuffle : baseShuffle;\n      return func(collection);\n    }\n\n    /**\n     * Gets the size of `collection` by returning its length for array-like\n     * values or the number of own enumerable string keyed properties for objects.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object|string} collection The collection to inspect.\n     * @returns {number} Returns the collection size.\n     * @example\n     *\n     * _.size([1, 2, 3]);\n     * // => 3\n     *\n     * _.size({ 'a': 1, 'b': 2 });\n     * // => 2\n     *\n     * _.size('pebbles');\n     * // => 7\n     */\n    function size(collection) {\n      if (collection == null) {\n        return 0;\n      }\n      if (isArrayLike(collection)) {\n        return isString(collection) ? stringSize(collection) : collection.length;\n      }\n      var tag = getTag(collection);\n      if (tag == mapTag || tag == setTag) {\n        return collection.size;\n      }\n      return baseKeys(collection).length;\n    }\n\n    /**\n     * Checks if `predicate` returns truthy for **any** element of `collection`.\n     * Iteration is stopped once `predicate` returns truthy. The predicate is\n     * invoked with three arguments: (value, index|key, collection).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {boolean} Returns `true` if any element passes the predicate check,\n     *  else `false`.\n     * @example\n     *\n     * _.some([null, 0, 'yes', false], Boolean);\n     * // => true\n     *\n     * var users = [\n     *   { 'user': 'barney', 'active': true },\n     *   { 'user': 'fred',   'active': false }\n     * ];\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.some(users, { 'user': 'barney', 'active': false });\n     * // => false\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.some(users, ['active', false]);\n     * // => true\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.some(users, 'active');\n     * // => true\n     */\n    function some(collection, predicate, guard) {\n      var func = isArray(collection) ? arraySome : baseSome;\n      if (guard && isIterateeCall(collection, predicate, guard)) {\n        predicate = undefined;\n      }\n      return func(collection, getIteratee(predicate, 3));\n    }\n\n    /**\n     * Creates an array of elements, sorted in ascending order by the results of\n     * running each element in a collection thru each iteratee. This method\n     * performs a stable sort, that is, it preserves the original sort order of\n     * equal elements. The iteratees are invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Collection\n     * @param {Array|Object} collection The collection to iterate over.\n     * @param {...(Function|Function[])} [iteratees=[_.identity]]\n     *  The iteratees to sort by.\n     * @returns {Array} Returns the new sorted array.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'fred',   'age': 48 },\n     *   { 'user': 'barney', 'age': 36 },\n     *   { 'user': 'fred',   'age': 30 },\n     *   { 'user': 'barney', 'age': 34 }\n     * ];\n     *\n     * _.sortBy(users, [function(o) { return o.user; }]);\n     * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 30]]\n     *\n     * _.sortBy(users, ['user', 'age']);\n     * // => objects for [['barney', 34], ['barney', 36], ['fred', 30], ['fred', 48]]\n     */\n    var sortBy = baseRest(function(collection, iteratees) {\n      if (collection == null) {\n        return [];\n      }\n      var length = iteratees.length;\n      if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) {\n        iteratees = [];\n      } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) {\n        iteratees = [iteratees[0]];\n      }\n      return baseOrderBy(collection, baseFlatten(iteratees, 1), []);\n    });\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Gets the timestamp of the number of milliseconds that have elapsed since\n     * the Unix epoch (1 January 1970 00:00:00 UTC).\n     *\n     * @static\n     * @memberOf _\n     * @since 2.4.0\n     * @category Date\n     * @returns {number} Returns the timestamp.\n     * @example\n     *\n     * _.defer(function(stamp) {\n     *   console.log(_.now() - stamp);\n     * }, _.now());\n     * // => Logs the number of milliseconds it took for the deferred invocation.\n     */\n    var now = ctxNow || function() {\n      return root.Date.now();\n    };\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * The opposite of `_.before`; this method creates a function that invokes\n     * `func` once it's called `n` or more times.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {number} n The number of calls before `func` is invoked.\n     * @param {Function} func The function to restrict.\n     * @returns {Function} Returns the new restricted function.\n     * @example\n     *\n     * var saves = ['profile', 'settings'];\n     *\n     * var done = _.after(saves.length, function() {\n     *   console.log('done saving!');\n     * });\n     *\n     * _.forEach(saves, function(type) {\n     *   asyncSave({ 'type': type, 'complete': done });\n     * });\n     * // => Logs 'done saving!' after the two async saves have completed.\n     */\n    function after(n, func) {\n      if (typeof func != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      n = toInteger(n);\n      return function() {\n        if (--n < 1) {\n          return func.apply(this, arguments);\n        }\n      };\n    }\n\n    /**\n     * Creates a function that invokes `func`, with up to `n` arguments,\n     * ignoring any additional arguments.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Function\n     * @param {Function} func The function to cap arguments for.\n     * @param {number} [n=func.length] The arity cap.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Function} Returns the new capped function.\n     * @example\n     *\n     * _.map(['6', '8', '10'], _.ary(parseInt, 1));\n     * // => [6, 8, 10]\n     */\n    function ary(func, n, guard) {\n      n = guard ? undefined : n;\n      n = (func && n == null) ? func.length : n;\n      return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n);\n    }\n\n    /**\n     * Creates a function that invokes `func`, with the `this` binding and arguments\n     * of the created function, while it's called less than `n` times. Subsequent\n     * calls to the created function return the result of the last `func` invocation.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Function\n     * @param {number} n The number of calls at which `func` is no longer invoked.\n     * @param {Function} func The function to restrict.\n     * @returns {Function} Returns the new restricted function.\n     * @example\n     *\n     * jQuery(element).on('click', _.before(5, addContactToList));\n     * // => Allows adding up to 4 contacts to the list.\n     */\n    function before(n, func) {\n      var result;\n      if (typeof func != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      n = toInteger(n);\n      return function() {\n        if (--n > 0) {\n          result = func.apply(this, arguments);\n        }\n        if (n <= 1) {\n          func = undefined;\n        }\n        return result;\n      };\n    }\n\n    /**\n     * Creates a function that invokes `func` with the `this` binding of `thisArg`\n     * and `partials` prepended to the arguments it receives.\n     *\n     * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds,\n     * may be used as a placeholder for partially applied arguments.\n     *\n     * **Note:** Unlike native `Function#bind`, this method doesn't set the \"length\"\n     * property of bound functions.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {Function} func The function to bind.\n     * @param {*} thisArg The `this` binding of `func`.\n     * @param {...*} [partials] The arguments to be partially applied.\n     * @returns {Function} Returns the new bound function.\n     * @example\n     *\n     * function greet(greeting, punctuation) {\n     *   return greeting + ' ' + this.user + punctuation;\n     * }\n     *\n     * var object = { 'user': 'fred' };\n     *\n     * var bound = _.bind(greet, object, 'hi');\n     * bound('!');\n     * // => 'hi fred!'\n     *\n     * // Bound with placeholders.\n     * var bound = _.bind(greet, object, _, '!');\n     * bound('hi');\n     * // => 'hi fred!'\n     */\n    var bind = baseRest(function(func, thisArg, partials) {\n      var bitmask = WRAP_BIND_FLAG;\n      if (partials.length) {\n        var holders = replaceHolders(partials, getHolder(bind));\n        bitmask |= WRAP_PARTIAL_FLAG;\n      }\n      return createWrap(func, bitmask, thisArg, partials, holders);\n    });\n\n    /**\n     * Creates a function that invokes the method at `object[key]` with `partials`\n     * prepended to the arguments it receives.\n     *\n     * This method differs from `_.bind` by allowing bound functions to reference\n     * methods that may be redefined or don't yet exist. See\n     * [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern)\n     * for more details.\n     *\n     * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic\n     * builds, may be used as a placeholder for partially applied arguments.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.10.0\n     * @category Function\n     * @param {Object} object The object to invoke the method on.\n     * @param {string} key The key of the method.\n     * @param {...*} [partials] The arguments to be partially applied.\n     * @returns {Function} Returns the new bound function.\n     * @example\n     *\n     * var object = {\n     *   'user': 'fred',\n     *   'greet': function(greeting, punctuation) {\n     *     return greeting + ' ' + this.user + punctuation;\n     *   }\n     * };\n     *\n     * var bound = _.bindKey(object, 'greet', 'hi');\n     * bound('!');\n     * // => 'hi fred!'\n     *\n     * object.greet = function(greeting, punctuation) {\n     *   return greeting + 'ya ' + this.user + punctuation;\n     * };\n     *\n     * bound('!');\n     * // => 'hiya fred!'\n     *\n     * // Bound with placeholders.\n     * var bound = _.bindKey(object, 'greet', _, '!');\n     * bound('hi');\n     * // => 'hiya fred!'\n     */\n    var bindKey = baseRest(function(object, key, partials) {\n      var bitmask = WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG;\n      if (partials.length) {\n        var holders = replaceHolders(partials, getHolder(bindKey));\n        bitmask |= WRAP_PARTIAL_FLAG;\n      }\n      return createWrap(key, bitmask, object, partials, holders);\n    });\n\n    /**\n     * Creates a function that accepts arguments of `func` and either invokes\n     * `func` returning its result, if at least `arity` number of arguments have\n     * been provided, or returns a function that accepts the remaining `func`\n     * arguments, and so on. The arity of `func` may be specified if `func.length`\n     * is not sufficient.\n     *\n     * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds,\n     * may be used as a placeholder for provided arguments.\n     *\n     * **Note:** This method doesn't set the \"length\" property of curried functions.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Function\n     * @param {Function} func The function to curry.\n     * @param {number} [arity=func.length] The arity of `func`.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Function} Returns the new curried function.\n     * @example\n     *\n     * var abc = function(a, b, c) {\n     *   return [a, b, c];\n     * };\n     *\n     * var curried = _.curry(abc);\n     *\n     * curried(1)(2)(3);\n     * // => [1, 2, 3]\n     *\n     * curried(1, 2)(3);\n     * // => [1, 2, 3]\n     *\n     * curried(1, 2, 3);\n     * // => [1, 2, 3]\n     *\n     * // Curried with placeholders.\n     * curried(1)(_, 3)(2);\n     * // => [1, 2, 3]\n     */\n    function curry(func, arity, guard) {\n      arity = guard ? undefined : arity;\n      var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity);\n      result.placeholder = curry.placeholder;\n      return result;\n    }\n\n    /**\n     * This method is like `_.curry` except that arguments are applied to `func`\n     * in the manner of `_.partialRight` instead of `_.partial`.\n     *\n     * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic\n     * builds, may be used as a placeholder for provided arguments.\n     *\n     * **Note:** This method doesn't set the \"length\" property of curried functions.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Function\n     * @param {Function} func The function to curry.\n     * @param {number} [arity=func.length] The arity of `func`.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Function} Returns the new curried function.\n     * @example\n     *\n     * var abc = function(a, b, c) {\n     *   return [a, b, c];\n     * };\n     *\n     * var curried = _.curryRight(abc);\n     *\n     * curried(3)(2)(1);\n     * // => [1, 2, 3]\n     *\n     * curried(2, 3)(1);\n     * // => [1, 2, 3]\n     *\n     * curried(1, 2, 3);\n     * // => [1, 2, 3]\n     *\n     * // Curried with placeholders.\n     * curried(3)(1, _)(2);\n     * // => [1, 2, 3]\n     */\n    function curryRight(func, arity, guard) {\n      arity = guard ? undefined : arity;\n      var result = createWrap(func, WRAP_CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity);\n      result.placeholder = curryRight.placeholder;\n      return result;\n    }\n\n    /**\n     * Creates a debounced function that delays invoking `func` until after `wait`\n     * milliseconds have elapsed since the last time the debounced function was\n     * invoked. The debounced function comes with a `cancel` method to cancel\n     * delayed `func` invocations and a `flush` method to immediately invoke them.\n     * Provide `options` to indicate whether `func` should be invoked on the\n     * leading and/or trailing edge of the `wait` timeout. The `func` is invoked\n     * with the last arguments provided to the debounced function. Subsequent\n     * calls to the debounced function return the result of the last `func`\n     * invocation.\n     *\n     * **Note:** If `leading` and `trailing` options are `true`, `func` is\n     * invoked on the trailing edge of the timeout only if the debounced function\n     * is invoked more than once during the `wait` timeout.\n     *\n     * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred\n     * until to the next tick, similar to `setTimeout` with a timeout of `0`.\n     *\n     * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)\n     * for details over the differences between `_.debounce` and `_.throttle`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {Function} func The function to debounce.\n     * @param {number} [wait=0] The number of milliseconds to delay.\n     * @param {Object} [options={}] The options object.\n     * @param {boolean} [options.leading=false]\n     *  Specify invoking on the leading edge of the timeout.\n     * @param {number} [options.maxWait]\n     *  The maximum time `func` is allowed to be delayed before it's invoked.\n     * @param {boolean} [options.trailing=true]\n     *  Specify invoking on the trailing edge of the timeout.\n     * @returns {Function} Returns the new debounced function.\n     * @example\n     *\n     * // Avoid costly calculations while the window size is in flux.\n     * jQuery(window).on('resize', _.debounce(calculateLayout, 150));\n     *\n     * // Invoke `sendMail` when clicked, debouncing subsequent calls.\n     * jQuery(element).on('click', _.debounce(sendMail, 300, {\n     *   'leading': true,\n     *   'trailing': false\n     * }));\n     *\n     * // Ensure `batchLog` is invoked once after 1 second of debounced calls.\n     * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });\n     * var source = new EventSource('/stream');\n     * jQuery(source).on('message', debounced);\n     *\n     * // Cancel the trailing debounced invocation.\n     * jQuery(window).on('popstate', debounced.cancel);\n     */\n    function debounce(func, wait, options) {\n      var lastArgs,\n          lastThis,\n          maxWait,\n          result,\n          timerId,\n          lastCallTime,\n          lastInvokeTime = 0,\n          leading = false,\n          maxing = false,\n          trailing = true;\n\n      if (typeof func != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      wait = toNumber(wait) || 0;\n      if (isObject(options)) {\n        leading = !!options.leading;\n        maxing = 'maxWait' in options;\n        maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;\n        trailing = 'trailing' in options ? !!options.trailing : trailing;\n      }\n\n      function invokeFunc(time) {\n        var args = lastArgs,\n            thisArg = lastThis;\n\n        lastArgs = lastThis = undefined;\n        lastInvokeTime = time;\n        result = func.apply(thisArg, args);\n        return result;\n      }\n\n      function leadingEdge(time) {\n        // Reset any `maxWait` timer.\n        lastInvokeTime = time;\n        // Start the timer for the trailing edge.\n        timerId = setTimeout(timerExpired, wait);\n        // Invoke the leading edge.\n        return leading ? invokeFunc(time) : result;\n      }\n\n      function remainingWait(time) {\n        var timeSinceLastCall = time - lastCallTime,\n            timeSinceLastInvoke = time - lastInvokeTime,\n            timeWaiting = wait - timeSinceLastCall;\n\n        return maxing\n          ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)\n          : timeWaiting;\n      }\n\n      function shouldInvoke(time) {\n        var timeSinceLastCall = time - lastCallTime,\n            timeSinceLastInvoke = time - lastInvokeTime;\n\n        // Either this is the first call, activity has stopped and we're at the\n        // trailing edge, the system time has gone backwards and we're treating\n        // it as the trailing edge, or we've hit the `maxWait` limit.\n        return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||\n          (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));\n      }\n\n      function timerExpired() {\n        var time = now();\n        if (shouldInvoke(time)) {\n          return trailingEdge(time);\n        }\n        // Restart the timer.\n        timerId = setTimeout(timerExpired, remainingWait(time));\n      }\n\n      function trailingEdge(time) {\n        timerId = undefined;\n\n        // Only invoke if we have `lastArgs` which means `func` has been\n        // debounced at least once.\n        if (trailing && lastArgs) {\n          return invokeFunc(time);\n        }\n        lastArgs = lastThis = undefined;\n        return result;\n      }\n\n      function cancel() {\n        if (timerId !== undefined) {\n          clearTimeout(timerId);\n        }\n        lastInvokeTime = 0;\n        lastArgs = lastCallTime = lastThis = timerId = undefined;\n      }\n\n      function flush() {\n        return timerId === undefined ? result : trailingEdge(now());\n      }\n\n      function debounced() {\n        var time = now(),\n            isInvoking = shouldInvoke(time);\n\n        lastArgs = arguments;\n        lastThis = this;\n        lastCallTime = time;\n\n        if (isInvoking) {\n          if (timerId === undefined) {\n            return leadingEdge(lastCallTime);\n          }\n          if (maxing) {\n            // Handle invocations in a tight loop.\n            clearTimeout(timerId);\n            timerId = setTimeout(timerExpired, wait);\n            return invokeFunc(lastCallTime);\n          }\n        }\n        if (timerId === undefined) {\n          timerId = setTimeout(timerExpired, wait);\n        }\n        return result;\n      }\n      debounced.cancel = cancel;\n      debounced.flush = flush;\n      return debounced;\n    }\n\n    /**\n     * Defers invoking the `func` until the current call stack has cleared. Any\n     * additional arguments are provided to `func` when it's invoked.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {Function} func The function to defer.\n     * @param {...*} [args] The arguments to invoke `func` with.\n     * @returns {number} Returns the timer id.\n     * @example\n     *\n     * _.defer(function(text) {\n     *   console.log(text);\n     * }, 'deferred');\n     * // => Logs 'deferred' after one millisecond.\n     */\n    var defer = baseRest(function(func, args) {\n      return baseDelay(func, 1, args);\n    });\n\n    /**\n     * Invokes `func` after `wait` milliseconds. Any additional arguments are\n     * provided to `func` when it's invoked.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {Function} func The function to delay.\n     * @param {number} wait The number of milliseconds to delay invocation.\n     * @param {...*} [args] The arguments to invoke `func` with.\n     * @returns {number} Returns the timer id.\n     * @example\n     *\n     * _.delay(function(text) {\n     *   console.log(text);\n     * }, 1000, 'later');\n     * // => Logs 'later' after one second.\n     */\n    var delay = baseRest(function(func, wait, args) {\n      return baseDelay(func, toNumber(wait) || 0, args);\n    });\n\n    /**\n     * Creates a function that invokes `func` with arguments reversed.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Function\n     * @param {Function} func The function to flip arguments for.\n     * @returns {Function} Returns the new flipped function.\n     * @example\n     *\n     * var flipped = _.flip(function() {\n     *   return _.toArray(arguments);\n     * });\n     *\n     * flipped('a', 'b', 'c', 'd');\n     * // => ['d', 'c', 'b', 'a']\n     */\n    function flip(func) {\n      return createWrap(func, WRAP_FLIP_FLAG);\n    }\n\n    /**\n     * Creates a function that memoizes the result of `func`. If `resolver` is\n     * provided, it determines the cache key for storing the result based on the\n     * arguments provided to the memoized function. By default, the first argument\n     * provided to the memoized function is used as the map cache key. The `func`\n     * is invoked with the `this` binding of the memoized function.\n     *\n     * **Note:** The cache is exposed as the `cache` property on the memoized\n     * function. Its creation may be customized by replacing the `_.memoize.Cache`\n     * constructor with one whose instances implement the\n     * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)\n     * method interface of `clear`, `delete`, `get`, `has`, and `set`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {Function} func The function to have its output memoized.\n     * @param {Function} [resolver] The function to resolve the cache key.\n     * @returns {Function} Returns the new memoized function.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': 2 };\n     * var other = { 'c': 3, 'd': 4 };\n     *\n     * var values = _.memoize(_.values);\n     * values(object);\n     * // => [1, 2]\n     *\n     * values(other);\n     * // => [3, 4]\n     *\n     * object.a = 2;\n     * values(object);\n     * // => [1, 2]\n     *\n     * // Modify the result cache.\n     * values.cache.set(object, ['a', 'b']);\n     * values(object);\n     * // => ['a', 'b']\n     *\n     * // Replace `_.memoize.Cache`.\n     * _.memoize.Cache = WeakMap;\n     */\n    function memoize(func, resolver) {\n      if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      var memoized = function() {\n        var args = arguments,\n            key = resolver ? resolver.apply(this, args) : args[0],\n            cache = memoized.cache;\n\n        if (cache.has(key)) {\n          return cache.get(key);\n        }\n        var result = func.apply(this, args);\n        memoized.cache = cache.set(key, result) || cache;\n        return result;\n      };\n      memoized.cache = new (memoize.Cache || MapCache);\n      return memoized;\n    }\n\n    // Expose `MapCache`.\n    memoize.Cache = MapCache;\n\n    /**\n     * Creates a function that negates the result of the predicate `func`. The\n     * `func` predicate is invoked with the `this` binding and arguments of the\n     * created function.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Function\n     * @param {Function} predicate The predicate to negate.\n     * @returns {Function} Returns the new negated function.\n     * @example\n     *\n     * function isEven(n) {\n     *   return n % 2 == 0;\n     * }\n     *\n     * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven));\n     * // => [1, 3, 5]\n     */\n    function negate(predicate) {\n      if (typeof predicate != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      return function() {\n        var args = arguments;\n        switch (args.length) {\n          case 0: return !predicate.call(this);\n          case 1: return !predicate.call(this, args[0]);\n          case 2: return !predicate.call(this, args[0], args[1]);\n          case 3: return !predicate.call(this, args[0], args[1], args[2]);\n        }\n        return !predicate.apply(this, args);\n      };\n    }\n\n    /**\n     * Creates a function that is restricted to invoking `func` once. Repeat calls\n     * to the function return the value of the first invocation. The `func` is\n     * invoked with the `this` binding and arguments of the created function.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {Function} func The function to restrict.\n     * @returns {Function} Returns the new restricted function.\n     * @example\n     *\n     * var initialize = _.once(createApplication);\n     * initialize();\n     * initialize();\n     * // => `createApplication` is invoked once\n     */\n    function once(func) {\n      return before(2, func);\n    }\n\n    /**\n     * Creates a function that invokes `func` with its arguments transformed.\n     *\n     * @static\n     * @since 4.0.0\n     * @memberOf _\n     * @category Function\n     * @param {Function} func The function to wrap.\n     * @param {...(Function|Function[])} [transforms=[_.identity]]\n     *  The argument transforms.\n     * @returns {Function} Returns the new function.\n     * @example\n     *\n     * function doubled(n) {\n     *   return n * 2;\n     * }\n     *\n     * function square(n) {\n     *   return n * n;\n     * }\n     *\n     * var func = _.overArgs(function(x, y) {\n     *   return [x, y];\n     * }, [square, doubled]);\n     *\n     * func(9, 3);\n     * // => [81, 6]\n     *\n     * func(10, 5);\n     * // => [100, 10]\n     */\n    var overArgs = castRest(function(func, transforms) {\n      transforms = (transforms.length == 1 && isArray(transforms[0]))\n        ? arrayMap(transforms[0], baseUnary(getIteratee()))\n        : arrayMap(baseFlatten(transforms, 1), baseUnary(getIteratee()));\n\n      var funcsLength = transforms.length;\n      return baseRest(function(args) {\n        var index = -1,\n            length = nativeMin(args.length, funcsLength);\n\n        while (++index < length) {\n          args[index] = transforms[index].call(this, args[index]);\n        }\n        return apply(func, this, args);\n      });\n    });\n\n    /**\n     * Creates a function that invokes `func` with `partials` prepended to the\n     * arguments it receives. This method is like `_.bind` except it does **not**\n     * alter the `this` binding.\n     *\n     * The `_.partial.placeholder` value, which defaults to `_` in monolithic\n     * builds, may be used as a placeholder for partially applied arguments.\n     *\n     * **Note:** This method doesn't set the \"length\" property of partially\n     * applied functions.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.2.0\n     * @category Function\n     * @param {Function} func The function to partially apply arguments to.\n     * @param {...*} [partials] The arguments to be partially applied.\n     * @returns {Function} Returns the new partially applied function.\n     * @example\n     *\n     * function greet(greeting, name) {\n     *   return greeting + ' ' + name;\n     * }\n     *\n     * var sayHelloTo = _.partial(greet, 'hello');\n     * sayHelloTo('fred');\n     * // => 'hello fred'\n     *\n     * // Partially applied with placeholders.\n     * var greetFred = _.partial(greet, _, 'fred');\n     * greetFred('hi');\n     * // => 'hi fred'\n     */\n    var partial = baseRest(function(func, partials) {\n      var holders = replaceHolders(partials, getHolder(partial));\n      return createWrap(func, WRAP_PARTIAL_FLAG, undefined, partials, holders);\n    });\n\n    /**\n     * This method is like `_.partial` except that partially applied arguments\n     * are appended to the arguments it receives.\n     *\n     * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic\n     * builds, may be used as a placeholder for partially applied arguments.\n     *\n     * **Note:** This method doesn't set the \"length\" property of partially\n     * applied functions.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.0.0\n     * @category Function\n     * @param {Function} func The function to partially apply arguments to.\n     * @param {...*} [partials] The arguments to be partially applied.\n     * @returns {Function} Returns the new partially applied function.\n     * @example\n     *\n     * function greet(greeting, name) {\n     *   return greeting + ' ' + name;\n     * }\n     *\n     * var greetFred = _.partialRight(greet, 'fred');\n     * greetFred('hi');\n     * // => 'hi fred'\n     *\n     * // Partially applied with placeholders.\n     * var sayHelloTo = _.partialRight(greet, 'hello', _);\n     * sayHelloTo('fred');\n     * // => 'hello fred'\n     */\n    var partialRight = baseRest(function(func, partials) {\n      var holders = replaceHolders(partials, getHolder(partialRight));\n      return createWrap(func, WRAP_PARTIAL_RIGHT_FLAG, undefined, partials, holders);\n    });\n\n    /**\n     * Creates a function that invokes `func` with arguments arranged according\n     * to the specified `indexes` where the argument value at the first index is\n     * provided as the first argument, the argument value at the second index is\n     * provided as the second argument, and so on.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Function\n     * @param {Function} func The function to rearrange arguments for.\n     * @param {...(number|number[])} indexes The arranged argument indexes.\n     * @returns {Function} Returns the new function.\n     * @example\n     *\n     * var rearged = _.rearg(function(a, b, c) {\n     *   return [a, b, c];\n     * }, [2, 0, 1]);\n     *\n     * rearged('b', 'c', 'a')\n     * // => ['a', 'b', 'c']\n     */\n    var rearg = flatRest(function(func, indexes) {\n      return createWrap(func, WRAP_REARG_FLAG, undefined, undefined, undefined, indexes);\n    });\n\n    /**\n     * Creates a function that invokes `func` with the `this` binding of the\n     * created function and arguments from `start` and beyond provided as\n     * an array.\n     *\n     * **Note:** This method is based on the\n     * [rest parameter](https://mdn.io/rest_parameters).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Function\n     * @param {Function} func The function to apply a rest parameter to.\n     * @param {number} [start=func.length-1] The start position of the rest parameter.\n     * @returns {Function} Returns the new function.\n     * @example\n     *\n     * var say = _.rest(function(what, names) {\n     *   return what + ' ' + _.initial(names).join(', ') +\n     *     (_.size(names) > 1 ? ', & ' : '') + _.last(names);\n     * });\n     *\n     * say('hello', 'fred', 'barney', 'pebbles');\n     * // => 'hello fred, barney, & pebbles'\n     */\n    function rest(func, start) {\n      if (typeof func != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      start = start === undefined ? start : toInteger(start);\n      return baseRest(func, start);\n    }\n\n    /**\n     * Creates a function that invokes `func` with the `this` binding of the\n     * create function and an array of arguments much like\n     * [`Function#apply`](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply).\n     *\n     * **Note:** This method is based on the\n     * [spread operator](https://mdn.io/spread_operator).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.2.0\n     * @category Function\n     * @param {Function} func The function to spread arguments over.\n     * @param {number} [start=0] The start position of the spread.\n     * @returns {Function} Returns the new function.\n     * @example\n     *\n     * var say = _.spread(function(who, what) {\n     *   return who + ' says ' + what;\n     * });\n     *\n     * say(['fred', 'hello']);\n     * // => 'fred says hello'\n     *\n     * var numbers = Promise.all([\n     *   Promise.resolve(40),\n     *   Promise.resolve(36)\n     * ]);\n     *\n     * numbers.then(_.spread(function(x, y) {\n     *   return x + y;\n     * }));\n     * // => a Promise of 76\n     */\n    function spread(func, start) {\n      if (typeof func != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      start = start == null ? 0 : nativeMax(toInteger(start), 0);\n      return baseRest(function(args) {\n        var array = args[start],\n            otherArgs = castSlice(args, 0, start);\n\n        if (array) {\n          arrayPush(otherArgs, array);\n        }\n        return apply(func, this, otherArgs);\n      });\n    }\n\n    /**\n     * Creates a throttled function that only invokes `func` at most once per\n     * every `wait` milliseconds. The throttled function comes with a `cancel`\n     * method to cancel delayed `func` invocations and a `flush` method to\n     * immediately invoke them. Provide `options` to indicate whether `func`\n     * should be invoked on the leading and/or trailing edge of the `wait`\n     * timeout. The `func` is invoked with the last arguments provided to the\n     * throttled function. Subsequent calls to the throttled function return the\n     * result of the last `func` invocation.\n     *\n     * **Note:** If `leading` and `trailing` options are `true`, `func` is\n     * invoked on the trailing edge of the timeout only if the throttled function\n     * is invoked more than once during the `wait` timeout.\n     *\n     * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred\n     * until to the next tick, similar to `setTimeout` with a timeout of `0`.\n     *\n     * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)\n     * for details over the differences between `_.throttle` and `_.debounce`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {Function} func The function to throttle.\n     * @param {number} [wait=0] The number of milliseconds to throttle invocations to.\n     * @param {Object} [options={}] The options object.\n     * @param {boolean} [options.leading=true]\n     *  Specify invoking on the leading edge of the timeout.\n     * @param {boolean} [options.trailing=true]\n     *  Specify invoking on the trailing edge of the timeout.\n     * @returns {Function} Returns the new throttled function.\n     * @example\n     *\n     * // Avoid excessively updating the position while scrolling.\n     * jQuery(window).on('scroll', _.throttle(updatePosition, 100));\n     *\n     * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.\n     * var throttled = _.throttle(renewToken, 300000, { 'trailing': false });\n     * jQuery(element).on('click', throttled);\n     *\n     * // Cancel the trailing throttled invocation.\n     * jQuery(window).on('popstate', throttled.cancel);\n     */\n    function throttle(func, wait, options) {\n      var leading = true,\n          trailing = true;\n\n      if (typeof func != 'function') {\n        throw new TypeError(FUNC_ERROR_TEXT);\n      }\n      if (isObject(options)) {\n        leading = 'leading' in options ? !!options.leading : leading;\n        trailing = 'trailing' in options ? !!options.trailing : trailing;\n      }\n      return debounce(func, wait, {\n        'leading': leading,\n        'maxWait': wait,\n        'trailing': trailing\n      });\n    }\n\n    /**\n     * Creates a function that accepts up to one argument, ignoring any\n     * additional arguments.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Function\n     * @param {Function} func The function to cap arguments for.\n     * @returns {Function} Returns the new capped function.\n     * @example\n     *\n     * _.map(['6', '8', '10'], _.unary(parseInt));\n     * // => [6, 8, 10]\n     */\n    function unary(func) {\n      return ary(func, 1);\n    }\n\n    /**\n     * Creates a function that provides `value` to `wrapper` as its first\n     * argument. Any additional arguments provided to the function are appended\n     * to those provided to the `wrapper`. The wrapper is invoked with the `this`\n     * binding of the created function.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Function\n     * @param {*} value The value to wrap.\n     * @param {Function} [wrapper=identity] The wrapper function.\n     * @returns {Function} Returns the new function.\n     * @example\n     *\n     * var p = _.wrap(_.escape, function(func, text) {\n     *   return '<p>' + func(text) + '</p>';\n     * });\n     *\n     * p('fred, barney, & pebbles');\n     * // => '<p>fred, barney, &amp; pebbles</p>'\n     */\n    function wrap(value, wrapper) {\n      return partial(castFunction(wrapper), value);\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Casts `value` as an array if it's not one.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.4.0\n     * @category Lang\n     * @param {*} value The value to inspect.\n     * @returns {Array} Returns the cast array.\n     * @example\n     *\n     * _.castArray(1);\n     * // => [1]\n     *\n     * _.castArray({ 'a': 1 });\n     * // => [{ 'a': 1 }]\n     *\n     * _.castArray('abc');\n     * // => ['abc']\n     *\n     * _.castArray(null);\n     * // => [null]\n     *\n     * _.castArray(undefined);\n     * // => [undefined]\n     *\n     * _.castArray();\n     * // => []\n     *\n     * var array = [1, 2, 3];\n     * console.log(_.castArray(array) === array);\n     * // => true\n     */\n    function castArray() {\n      if (!arguments.length) {\n        return [];\n      }\n      var value = arguments[0];\n      return isArray(value) ? value : [value];\n    }\n\n    /**\n     * Creates a shallow clone of `value`.\n     *\n     * **Note:** This method is loosely based on the\n     * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm)\n     * and supports cloning arrays, array buffers, booleans, date objects, maps,\n     * numbers, `Object` objects, regexes, sets, strings, symbols, and typed\n     * arrays. The own enumerable properties of `arguments` objects are cloned\n     * as plain objects. An empty object is returned for uncloneable values such\n     * as error objects, functions, DOM nodes, and WeakMaps.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to clone.\n     * @returns {*} Returns the cloned value.\n     * @see _.cloneDeep\n     * @example\n     *\n     * var objects = [{ 'a': 1 }, { 'b': 2 }];\n     *\n     * var shallow = _.clone(objects);\n     * console.log(shallow[0] === objects[0]);\n     * // => true\n     */\n    function clone(value) {\n      return baseClone(value, CLONE_SYMBOLS_FLAG);\n    }\n\n    /**\n     * This method is like `_.clone` except that it accepts `customizer` which\n     * is invoked to produce the cloned value. If `customizer` returns `undefined`,\n     * cloning is handled by the method instead. The `customizer` is invoked with\n     * up to four arguments; (value [, index|key, object, stack]).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to clone.\n     * @param {Function} [customizer] The function to customize cloning.\n     * @returns {*} Returns the cloned value.\n     * @see _.cloneDeepWith\n     * @example\n     *\n     * function customizer(value) {\n     *   if (_.isElement(value)) {\n     *     return value.cloneNode(false);\n     *   }\n     * }\n     *\n     * var el = _.cloneWith(document.body, customizer);\n     *\n     * console.log(el === document.body);\n     * // => false\n     * console.log(el.nodeName);\n     * // => 'BODY'\n     * console.log(el.childNodes.length);\n     * // => 0\n     */\n    function cloneWith(value, customizer) {\n      customizer = typeof customizer == 'function' ? customizer : undefined;\n      return baseClone(value, CLONE_SYMBOLS_FLAG, customizer);\n    }\n\n    /**\n     * This method is like `_.clone` except that it recursively clones `value`.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.0.0\n     * @category Lang\n     * @param {*} value The value to recursively clone.\n     * @returns {*} Returns the deep cloned value.\n     * @see _.clone\n     * @example\n     *\n     * var objects = [{ 'a': 1 }, { 'b': 2 }];\n     *\n     * var deep = _.cloneDeep(objects);\n     * console.log(deep[0] === objects[0]);\n     * // => false\n     */\n    function cloneDeep(value) {\n      return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);\n    }\n\n    /**\n     * This method is like `_.cloneWith` except that it recursively clones `value`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to recursively clone.\n     * @param {Function} [customizer] The function to customize cloning.\n     * @returns {*} Returns the deep cloned value.\n     * @see _.cloneWith\n     * @example\n     *\n     * function customizer(value) {\n     *   if (_.isElement(value)) {\n     *     return value.cloneNode(true);\n     *   }\n     * }\n     *\n     * var el = _.cloneDeepWith(document.body, customizer);\n     *\n     * console.log(el === document.body);\n     * // => false\n     * console.log(el.nodeName);\n     * // => 'BODY'\n     * console.log(el.childNodes.length);\n     * // => 20\n     */\n    function cloneDeepWith(value, customizer) {\n      customizer = typeof customizer == 'function' ? customizer : undefined;\n      return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer);\n    }\n\n    /**\n     * Checks if `object` conforms to `source` by invoking the predicate\n     * properties of `source` with the corresponding property values of `object`.\n     *\n     * **Note:** This method is equivalent to `_.conforms` when `source` is\n     * partially applied.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.14.0\n     * @category Lang\n     * @param {Object} object The object to inspect.\n     * @param {Object} source The object of property predicates to conform to.\n     * @returns {boolean} Returns `true` if `object` conforms, else `false`.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': 2 };\n     *\n     * _.conformsTo(object, { 'b': function(n) { return n > 1; } });\n     * // => true\n     *\n     * _.conformsTo(object, { 'b': function(n) { return n > 2; } });\n     * // => false\n     */\n    function conformsTo(object, source) {\n      return source == null || baseConformsTo(object, source, keys(source));\n    }\n\n    /**\n     * Performs a\n     * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n     * comparison between two values to determine if they are equivalent.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n     * @example\n     *\n     * var object = { 'a': 1 };\n     * var other = { 'a': 1 };\n     *\n     * _.eq(object, object);\n     * // => true\n     *\n     * _.eq(object, other);\n     * // => false\n     *\n     * _.eq('a', 'a');\n     * // => true\n     *\n     * _.eq('a', Object('a'));\n     * // => false\n     *\n     * _.eq(NaN, NaN);\n     * // => true\n     */\n    function eq(value, other) {\n      return value === other || (value !== value && other !== other);\n    }\n\n    /**\n     * Checks if `value` is greater than `other`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.9.0\n     * @category Lang\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {boolean} Returns `true` if `value` is greater than `other`,\n     *  else `false`.\n     * @see _.lt\n     * @example\n     *\n     * _.gt(3, 1);\n     * // => true\n     *\n     * _.gt(3, 3);\n     * // => false\n     *\n     * _.gt(1, 3);\n     * // => false\n     */\n    var gt = createRelationalOperation(baseGt);\n\n    /**\n     * Checks if `value` is greater than or equal to `other`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.9.0\n     * @category Lang\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {boolean} Returns `true` if `value` is greater than or equal to\n     *  `other`, else `false`.\n     * @see _.lte\n     * @example\n     *\n     * _.gte(3, 1);\n     * // => true\n     *\n     * _.gte(3, 3);\n     * // => true\n     *\n     * _.gte(1, 3);\n     * // => false\n     */\n    var gte = createRelationalOperation(function(value, other) {\n      return value >= other;\n    });\n\n    /**\n     * Checks if `value` is likely an `arguments` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an `arguments` object,\n     *  else `false`.\n     * @example\n     *\n     * _.isArguments(function() { return arguments; }());\n     * // => true\n     *\n     * _.isArguments([1, 2, 3]);\n     * // => false\n     */\n    var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {\n      return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&\n        !propertyIsEnumerable.call(value, 'callee');\n    };\n\n    /**\n     * Checks if `value` is classified as an `Array` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an array, else `false`.\n     * @example\n     *\n     * _.isArray([1, 2, 3]);\n     * // => true\n     *\n     * _.isArray(document.body.children);\n     * // => false\n     *\n     * _.isArray('abc');\n     * // => false\n     *\n     * _.isArray(_.noop);\n     * // => false\n     */\n    var isArray = Array.isArray;\n\n    /**\n     * Checks if `value` is classified as an `ArrayBuffer` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.3.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`.\n     * @example\n     *\n     * _.isArrayBuffer(new ArrayBuffer(2));\n     * // => true\n     *\n     * _.isArrayBuffer(new Array(2));\n     * // => false\n     */\n    var isArrayBuffer = nodeIsArrayBuffer ? baseUnary(nodeIsArrayBuffer) : baseIsArrayBuffer;\n\n    /**\n     * Checks if `value` is array-like. A value is considered array-like if it's\n     * not a function and has a `value.length` that's an integer greater than or\n     * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is array-like, else `false`.\n     * @example\n     *\n     * _.isArrayLike([1, 2, 3]);\n     * // => true\n     *\n     * _.isArrayLike(document.body.children);\n     * // => true\n     *\n     * _.isArrayLike('abc');\n     * // => true\n     *\n     * _.isArrayLike(_.noop);\n     * // => false\n     */\n    function isArrayLike(value) {\n      return value != null && isLength(value.length) && !isFunction(value);\n    }\n\n    /**\n     * This method is like `_.isArrayLike` except that it also checks if `value`\n     * is an object.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an array-like object,\n     *  else `false`.\n     * @example\n     *\n     * _.isArrayLikeObject([1, 2, 3]);\n     * // => true\n     *\n     * _.isArrayLikeObject(document.body.children);\n     * // => true\n     *\n     * _.isArrayLikeObject('abc');\n     * // => false\n     *\n     * _.isArrayLikeObject(_.noop);\n     * // => false\n     */\n    function isArrayLikeObject(value) {\n      return isObjectLike(value) && isArrayLike(value);\n    }\n\n    /**\n     * Checks if `value` is classified as a boolean primitive or object.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a boolean, else `false`.\n     * @example\n     *\n     * _.isBoolean(false);\n     * // => true\n     *\n     * _.isBoolean(null);\n     * // => false\n     */\n    function isBoolean(value) {\n      return value === true || value === false ||\n        (isObjectLike(value) && baseGetTag(value) == boolTag);\n    }\n\n    /**\n     * Checks if `value` is a buffer.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.3.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a buffer, else `false`.\n     * @example\n     *\n     * _.isBuffer(new Buffer(2));\n     * // => true\n     *\n     * _.isBuffer(new Uint8Array(2));\n     * // => false\n     */\n    var isBuffer = nativeIsBuffer || stubFalse;\n\n    /**\n     * Checks if `value` is classified as a `Date` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a date object, else `false`.\n     * @example\n     *\n     * _.isDate(new Date);\n     * // => true\n     *\n     * _.isDate('Mon April 23 2012');\n     * // => false\n     */\n    var isDate = nodeIsDate ? baseUnary(nodeIsDate) : baseIsDate;\n\n    /**\n     * Checks if `value` is likely a DOM element.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`.\n     * @example\n     *\n     * _.isElement(document.body);\n     * // => true\n     *\n     * _.isElement('<body>');\n     * // => false\n     */\n    function isElement(value) {\n      return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value);\n    }\n\n    /**\n     * Checks if `value` is an empty object, collection, map, or set.\n     *\n     * Objects are considered empty if they have no own enumerable string keyed\n     * properties.\n     *\n     * Array-like values such as `arguments` objects, arrays, buffers, strings, or\n     * jQuery-like collections are considered empty if they have a `length` of `0`.\n     * Similarly, maps and sets are considered empty if they have a `size` of `0`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is empty, else `false`.\n     * @example\n     *\n     * _.isEmpty(null);\n     * // => true\n     *\n     * _.isEmpty(true);\n     * // => true\n     *\n     * _.isEmpty(1);\n     * // => true\n     *\n     * _.isEmpty([1, 2, 3]);\n     * // => false\n     *\n     * _.isEmpty({ 'a': 1 });\n     * // => false\n     */\n    function isEmpty(value) {\n      if (value == null) {\n        return true;\n      }\n      if (isArrayLike(value) &&\n          (isArray(value) || typeof value == 'string' || typeof value.splice == 'function' ||\n            isBuffer(value) || isTypedArray(value) || isArguments(value))) {\n        return !value.length;\n      }\n      var tag = getTag(value);\n      if (tag == mapTag || tag == setTag) {\n        return !value.size;\n      }\n      if (isPrototype(value)) {\n        return !baseKeys(value).length;\n      }\n      for (var key in value) {\n        if (hasOwnProperty.call(value, key)) {\n          return false;\n        }\n      }\n      return true;\n    }\n\n    /**\n     * Performs a deep comparison between two values to determine if they are\n     * equivalent.\n     *\n     * **Note:** This method supports comparing arrays, array buffers, booleans,\n     * date objects, error objects, maps, numbers, `Object` objects, regexes,\n     * sets, strings, symbols, and typed arrays. `Object` objects are compared\n     * by their own, not inherited, enumerable properties. Functions and DOM\n     * nodes are compared by strict equality, i.e. `===`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n     * @example\n     *\n     * var object = { 'a': 1 };\n     * var other = { 'a': 1 };\n     *\n     * _.isEqual(object, other);\n     * // => true\n     *\n     * object === other;\n     * // => false\n     */\n    function isEqual(value, other) {\n      return baseIsEqual(value, other);\n    }\n\n    /**\n     * This method is like `_.isEqual` except that it accepts `customizer` which\n     * is invoked to compare values. If `customizer` returns `undefined`, comparisons\n     * are handled by the method instead. The `customizer` is invoked with up to\n     * six arguments: (objValue, othValue [, index|key, object, other, stack]).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @param {Function} [customizer] The function to customize comparisons.\n     * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n     * @example\n     *\n     * function isGreeting(value) {\n     *   return /^h(?:i|ello)$/.test(value);\n     * }\n     *\n     * function customizer(objValue, othValue) {\n     *   if (isGreeting(objValue) && isGreeting(othValue)) {\n     *     return true;\n     *   }\n     * }\n     *\n     * var array = ['hello', 'goodbye'];\n     * var other = ['hi', 'goodbye'];\n     *\n     * _.isEqualWith(array, other, customizer);\n     * // => true\n     */\n    function isEqualWith(value, other, customizer) {\n      customizer = typeof customizer == 'function' ? customizer : undefined;\n      var result = customizer ? customizer(value, other) : undefined;\n      return result === undefined ? baseIsEqual(value, other, undefined, customizer) : !!result;\n    }\n\n    /**\n     * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`,\n     * `SyntaxError`, `TypeError`, or `URIError` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an error object, else `false`.\n     * @example\n     *\n     * _.isError(new Error);\n     * // => true\n     *\n     * _.isError(Error);\n     * // => false\n     */\n    function isError(value) {\n      if (!isObjectLike(value)) {\n        return false;\n      }\n      var tag = baseGetTag(value);\n      return tag == errorTag || tag == domExcTag ||\n        (typeof value.message == 'string' && typeof value.name == 'string' && !isPlainObject(value));\n    }\n\n    /**\n     * Checks if `value` is a finite primitive number.\n     *\n     * **Note:** This method is based on\n     * [`Number.isFinite`](https://mdn.io/Number/isFinite).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a finite number, else `false`.\n     * @example\n     *\n     * _.isFinite(3);\n     * // => true\n     *\n     * _.isFinite(Number.MIN_VALUE);\n     * // => true\n     *\n     * _.isFinite(Infinity);\n     * // => false\n     *\n     * _.isFinite('3');\n     * // => false\n     */\n    function isFinite(value) {\n      return typeof value == 'number' && nativeIsFinite(value);\n    }\n\n    /**\n     * Checks if `value` is classified as a `Function` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a function, else `false`.\n     * @example\n     *\n     * _.isFunction(_);\n     * // => true\n     *\n     * _.isFunction(/abc/);\n     * // => false\n     */\n    function isFunction(value) {\n      if (!isObject(value)) {\n        return false;\n      }\n      // The use of `Object#toString` avoids issues with the `typeof` operator\n      // in Safari 9 which returns 'object' for typed arrays and other constructors.\n      var tag = baseGetTag(value);\n      return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;\n    }\n\n    /**\n     * Checks if `value` is an integer.\n     *\n     * **Note:** This method is based on\n     * [`Number.isInteger`](https://mdn.io/Number/isInteger).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an integer, else `false`.\n     * @example\n     *\n     * _.isInteger(3);\n     * // => true\n     *\n     * _.isInteger(Number.MIN_VALUE);\n     * // => false\n     *\n     * _.isInteger(Infinity);\n     * // => false\n     *\n     * _.isInteger('3');\n     * // => false\n     */\n    function isInteger(value) {\n      return typeof value == 'number' && value == toInteger(value);\n    }\n\n    /**\n     * Checks if `value` is a valid array-like length.\n     *\n     * **Note:** This method is loosely based on\n     * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.\n     * @example\n     *\n     * _.isLength(3);\n     * // => true\n     *\n     * _.isLength(Number.MIN_VALUE);\n     * // => false\n     *\n     * _.isLength(Infinity);\n     * // => false\n     *\n     * _.isLength('3');\n     * // => false\n     */\n    function isLength(value) {\n      return typeof value == 'number' &&\n        value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;\n    }\n\n    /**\n     * Checks if `value` is the\n     * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)\n     * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is an object, else `false`.\n     * @example\n     *\n     * _.isObject({});\n     * // => true\n     *\n     * _.isObject([1, 2, 3]);\n     * // => true\n     *\n     * _.isObject(_.noop);\n     * // => true\n     *\n     * _.isObject(null);\n     * // => false\n     */\n    function isObject(value) {\n      var type = typeof value;\n      return value != null && (type == 'object' || type == 'function');\n    }\n\n    /**\n     * Checks if `value` is object-like. A value is object-like if it's not `null`\n     * and has a `typeof` result of \"object\".\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is object-like, else `false`.\n     * @example\n     *\n     * _.isObjectLike({});\n     * // => true\n     *\n     * _.isObjectLike([1, 2, 3]);\n     * // => true\n     *\n     * _.isObjectLike(_.noop);\n     * // => false\n     *\n     * _.isObjectLike(null);\n     * // => false\n     */\n    function isObjectLike(value) {\n      return value != null && typeof value == 'object';\n    }\n\n    /**\n     * Checks if `value` is classified as a `Map` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.3.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a map, else `false`.\n     * @example\n     *\n     * _.isMap(new Map);\n     * // => true\n     *\n     * _.isMap(new WeakMap);\n     * // => false\n     */\n    var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap;\n\n    /**\n     * Performs a partial deep comparison between `object` and `source` to\n     * determine if `object` contains equivalent property values.\n     *\n     * **Note:** This method is equivalent to `_.matches` when `source` is\n     * partially applied.\n     *\n     * Partial comparisons will match empty array and empty object `source`\n     * values against any array or object value, respectively. See `_.isEqual`\n     * for a list of supported value comparisons.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Lang\n     * @param {Object} object The object to inspect.\n     * @param {Object} source The object of property values to match.\n     * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': 2 };\n     *\n     * _.isMatch(object, { 'b': 2 });\n     * // => true\n     *\n     * _.isMatch(object, { 'b': 1 });\n     * // => false\n     */\n    function isMatch(object, source) {\n      return object === source || baseIsMatch(object, source, getMatchData(source));\n    }\n\n    /**\n     * This method is like `_.isMatch` except that it accepts `customizer` which\n     * is invoked to compare values. If `customizer` returns `undefined`, comparisons\n     * are handled by the method instead. The `customizer` is invoked with five\n     * arguments: (objValue, srcValue, index|key, object, source).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {Object} object The object to inspect.\n     * @param {Object} source The object of property values to match.\n     * @param {Function} [customizer] The function to customize comparisons.\n     * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n     * @example\n     *\n     * function isGreeting(value) {\n     *   return /^h(?:i|ello)$/.test(value);\n     * }\n     *\n     * function customizer(objValue, srcValue) {\n     *   if (isGreeting(objValue) && isGreeting(srcValue)) {\n     *     return true;\n     *   }\n     * }\n     *\n     * var object = { 'greeting': 'hello' };\n     * var source = { 'greeting': 'hi' };\n     *\n     * _.isMatchWith(object, source, customizer);\n     * // => true\n     */\n    function isMatchWith(object, source, customizer) {\n      customizer = typeof customizer == 'function' ? customizer : undefined;\n      return baseIsMatch(object, source, getMatchData(source), customizer);\n    }\n\n    /**\n     * Checks if `value` is `NaN`.\n     *\n     * **Note:** This method is based on\n     * [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as\n     * global [`isNaN`](https://mdn.io/isNaN) which returns `true` for\n     * `undefined` and other non-number values.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.\n     * @example\n     *\n     * _.isNaN(NaN);\n     * // => true\n     *\n     * _.isNaN(new Number(NaN));\n     * // => true\n     *\n     * isNaN(undefined);\n     * // => true\n     *\n     * _.isNaN(undefined);\n     * // => false\n     */\n    function isNaN(value) {\n      // An `NaN` primitive is the only value that is not equal to itself.\n      // Perform the `toStringTag` check first to avoid errors with some\n      // ActiveX objects in IE.\n      return isNumber(value) && value != +value;\n    }\n\n    /**\n     * Checks if `value` is a pristine native function.\n     *\n     * **Note:** This method can't reliably detect native functions in the presence\n     * of the core-js package because core-js circumvents this kind of detection.\n     * Despite multiple requests, the core-js maintainer has made it clear: any\n     * attempt to fix the detection will be obstructed. As a result, we're left\n     * with little choice but to throw an error. Unfortunately, this also affects\n     * packages, like [babel-polyfill](https://www.npmjs.com/package/babel-polyfill),\n     * which rely on core-js.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a native function,\n     *  else `false`.\n     * @example\n     *\n     * _.isNative(Array.prototype.push);\n     * // => true\n     *\n     * _.isNative(_);\n     * // => false\n     */\n    function isNative(value) {\n      if (isMaskable(value)) {\n        throw new Error(CORE_ERROR_TEXT);\n      }\n      return baseIsNative(value);\n    }\n\n    /**\n     * Checks if `value` is `null`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is `null`, else `false`.\n     * @example\n     *\n     * _.isNull(null);\n     * // => true\n     *\n     * _.isNull(void 0);\n     * // => false\n     */\n    function isNull(value) {\n      return value === null;\n    }\n\n    /**\n     * Checks if `value` is `null` or `undefined`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is nullish, else `false`.\n     * @example\n     *\n     * _.isNil(null);\n     * // => true\n     *\n     * _.isNil(void 0);\n     * // => true\n     *\n     * _.isNil(NaN);\n     * // => false\n     */\n    function isNil(value) {\n      return value == null;\n    }\n\n    /**\n     * Checks if `value` is classified as a `Number` primitive or object.\n     *\n     * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are\n     * classified as numbers, use the `_.isFinite` method.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a number, else `false`.\n     * @example\n     *\n     * _.isNumber(3);\n     * // => true\n     *\n     * _.isNumber(Number.MIN_VALUE);\n     * // => true\n     *\n     * _.isNumber(Infinity);\n     * // => true\n     *\n     * _.isNumber('3');\n     * // => false\n     */\n    function isNumber(value) {\n      return typeof value == 'number' ||\n        (isObjectLike(value) && baseGetTag(value) == numberTag);\n    }\n\n    /**\n     * Checks if `value` is a plain object, that is, an object created by the\n     * `Object` constructor or one with a `[[Prototype]]` of `null`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.8.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     * }\n     *\n     * _.isPlainObject(new Foo);\n     * // => false\n     *\n     * _.isPlainObject([1, 2, 3]);\n     * // => false\n     *\n     * _.isPlainObject({ 'x': 0, 'y': 0 });\n     * // => true\n     *\n     * _.isPlainObject(Object.create(null));\n     * // => true\n     */\n    function isPlainObject(value) {\n      if (!isObjectLike(value) || baseGetTag(value) != objectTag) {\n        return false;\n      }\n      var proto = getPrototype(value);\n      if (proto === null) {\n        return true;\n      }\n      var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;\n      return typeof Ctor == 'function' && Ctor instanceof Ctor &&\n        funcToString.call(Ctor) == objectCtorString;\n    }\n\n    /**\n     * Checks if `value` is classified as a `RegExp` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.1.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a regexp, else `false`.\n     * @example\n     *\n     * _.isRegExp(/abc/);\n     * // => true\n     *\n     * _.isRegExp('/abc/');\n     * // => false\n     */\n    var isRegExp = nodeIsRegExp ? baseUnary(nodeIsRegExp) : baseIsRegExp;\n\n    /**\n     * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754\n     * double precision number which isn't the result of a rounded unsafe integer.\n     *\n     * **Note:** This method is based on\n     * [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`.\n     * @example\n     *\n     * _.isSafeInteger(3);\n     * // => true\n     *\n     * _.isSafeInteger(Number.MIN_VALUE);\n     * // => false\n     *\n     * _.isSafeInteger(Infinity);\n     * // => false\n     *\n     * _.isSafeInteger('3');\n     * // => false\n     */\n    function isSafeInteger(value) {\n      return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER;\n    }\n\n    /**\n     * Checks if `value` is classified as a `Set` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.3.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a set, else `false`.\n     * @example\n     *\n     * _.isSet(new Set);\n     * // => true\n     *\n     * _.isSet(new WeakSet);\n     * // => false\n     */\n    var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet;\n\n    /**\n     * Checks if `value` is classified as a `String` primitive or object.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a string, else `false`.\n     * @example\n     *\n     * _.isString('abc');\n     * // => true\n     *\n     * _.isString(1);\n     * // => false\n     */\n    function isString(value) {\n      return typeof value == 'string' ||\n        (!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag);\n    }\n\n    /**\n     * Checks if `value` is classified as a `Symbol` primitive or object.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.\n     * @example\n     *\n     * _.isSymbol(Symbol.iterator);\n     * // => true\n     *\n     * _.isSymbol('abc');\n     * // => false\n     */\n    function isSymbol(value) {\n      return typeof value == 'symbol' ||\n        (isObjectLike(value) && baseGetTag(value) == symbolTag);\n    }\n\n    /**\n     * Checks if `value` is classified as a typed array.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.\n     * @example\n     *\n     * _.isTypedArray(new Uint8Array);\n     * // => true\n     *\n     * _.isTypedArray([]);\n     * // => false\n     */\n    var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;\n\n    /**\n     * Checks if `value` is `undefined`.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`.\n     * @example\n     *\n     * _.isUndefined(void 0);\n     * // => true\n     *\n     * _.isUndefined(null);\n     * // => false\n     */\n    function isUndefined(value) {\n      return value === undefined;\n    }\n\n    /**\n     * Checks if `value` is classified as a `WeakMap` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.3.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a weak map, else `false`.\n     * @example\n     *\n     * _.isWeakMap(new WeakMap);\n     * // => true\n     *\n     * _.isWeakMap(new Map);\n     * // => false\n     */\n    function isWeakMap(value) {\n      return isObjectLike(value) && getTag(value) == weakMapTag;\n    }\n\n    /**\n     * Checks if `value` is classified as a `WeakSet` object.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.3.0\n     * @category Lang\n     * @param {*} value The value to check.\n     * @returns {boolean} Returns `true` if `value` is a weak set, else `false`.\n     * @example\n     *\n     * _.isWeakSet(new WeakSet);\n     * // => true\n     *\n     * _.isWeakSet(new Set);\n     * // => false\n     */\n    function isWeakSet(value) {\n      return isObjectLike(value) && baseGetTag(value) == weakSetTag;\n    }\n\n    /**\n     * Checks if `value` is less than `other`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.9.0\n     * @category Lang\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {boolean} Returns `true` if `value` is less than `other`,\n     *  else `false`.\n     * @see _.gt\n     * @example\n     *\n     * _.lt(1, 3);\n     * // => true\n     *\n     * _.lt(3, 3);\n     * // => false\n     *\n     * _.lt(3, 1);\n     * // => false\n     */\n    var lt = createRelationalOperation(baseLt);\n\n    /**\n     * Checks if `value` is less than or equal to `other`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.9.0\n     * @category Lang\n     * @param {*} value The value to compare.\n     * @param {*} other The other value to compare.\n     * @returns {boolean} Returns `true` if `value` is less than or equal to\n     *  `other`, else `false`.\n     * @see _.gte\n     * @example\n     *\n     * _.lte(1, 3);\n     * // => true\n     *\n     * _.lte(3, 3);\n     * // => true\n     *\n     * _.lte(3, 1);\n     * // => false\n     */\n    var lte = createRelationalOperation(function(value, other) {\n      return value <= other;\n    });\n\n    /**\n     * Converts `value` to an array.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Lang\n     * @param {*} value The value to convert.\n     * @returns {Array} Returns the converted array.\n     * @example\n     *\n     * _.toArray({ 'a': 1, 'b': 2 });\n     * // => [1, 2]\n     *\n     * _.toArray('abc');\n     * // => ['a', 'b', 'c']\n     *\n     * _.toArray(1);\n     * // => []\n     *\n     * _.toArray(null);\n     * // => []\n     */\n    function toArray(value) {\n      if (!value) {\n        return [];\n      }\n      if (isArrayLike(value)) {\n        return isString(value) ? stringToArray(value) : copyArray(value);\n      }\n      if (symIterator && value[symIterator]) {\n        return iteratorToArray(value[symIterator]());\n      }\n      var tag = getTag(value),\n          func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values);\n\n      return func(value);\n    }\n\n    /**\n     * Converts `value` to a finite number.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.12.0\n     * @category Lang\n     * @param {*} value The value to convert.\n     * @returns {number} Returns the converted number.\n     * @example\n     *\n     * _.toFinite(3.2);\n     * // => 3.2\n     *\n     * _.toFinite(Number.MIN_VALUE);\n     * // => 5e-324\n     *\n     * _.toFinite(Infinity);\n     * // => 1.7976931348623157e+308\n     *\n     * _.toFinite('3.2');\n     * // => 3.2\n     */\n    function toFinite(value) {\n      if (!value) {\n        return value === 0 ? value : 0;\n      }\n      value = toNumber(value);\n      if (value === INFINITY || value === -INFINITY) {\n        var sign = (value < 0 ? -1 : 1);\n        return sign * MAX_INTEGER;\n      }\n      return value === value ? value : 0;\n    }\n\n    /**\n     * Converts `value` to an integer.\n     *\n     * **Note:** This method is loosely based on\n     * [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to convert.\n     * @returns {number} Returns the converted integer.\n     * @example\n     *\n     * _.toInteger(3.2);\n     * // => 3\n     *\n     * _.toInteger(Number.MIN_VALUE);\n     * // => 0\n     *\n     * _.toInteger(Infinity);\n     * // => 1.7976931348623157e+308\n     *\n     * _.toInteger('3.2');\n     * // => 3\n     */\n    function toInteger(value) {\n      var result = toFinite(value),\n          remainder = result % 1;\n\n      return result === result ? (remainder ? result - remainder : result) : 0;\n    }\n\n    /**\n     * Converts `value` to an integer suitable for use as the length of an\n     * array-like object.\n     *\n     * **Note:** This method is based on\n     * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to convert.\n     * @returns {number} Returns the converted integer.\n     * @example\n     *\n     * _.toLength(3.2);\n     * // => 3\n     *\n     * _.toLength(Number.MIN_VALUE);\n     * // => 0\n     *\n     * _.toLength(Infinity);\n     * // => 4294967295\n     *\n     * _.toLength('3.2');\n     * // => 3\n     */\n    function toLength(value) {\n      return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0;\n    }\n\n    /**\n     * Converts `value` to a number.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to process.\n     * @returns {number} Returns the number.\n     * @example\n     *\n     * _.toNumber(3.2);\n     * // => 3.2\n     *\n     * _.toNumber(Number.MIN_VALUE);\n     * // => 5e-324\n     *\n     * _.toNumber(Infinity);\n     * // => Infinity\n     *\n     * _.toNumber('3.2');\n     * // => 3.2\n     */\n    function toNumber(value) {\n      if (typeof value == 'number') {\n        return value;\n      }\n      if (isSymbol(value)) {\n        return NAN;\n      }\n      if (isObject(value)) {\n        var other = typeof value.valueOf == 'function' ? value.valueOf() : value;\n        value = isObject(other) ? (other + '') : other;\n      }\n      if (typeof value != 'string') {\n        return value === 0 ? value : +value;\n      }\n      value = value.replace(reTrim, '');\n      var isBinary = reIsBinary.test(value);\n      return (isBinary || reIsOctal.test(value))\n        ? freeParseInt(value.slice(2), isBinary ? 2 : 8)\n        : (reIsBadHex.test(value) ? NAN : +value);\n    }\n\n    /**\n     * Converts `value` to a plain object flattening inherited enumerable string\n     * keyed properties of `value` to own properties of the plain object.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Lang\n     * @param {*} value The value to convert.\n     * @returns {Object} Returns the converted plain object.\n     * @example\n     *\n     * function Foo() {\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.assign({ 'a': 1 }, new Foo);\n     * // => { 'a': 1, 'b': 2 }\n     *\n     * _.assign({ 'a': 1 }, _.toPlainObject(new Foo));\n     * // => { 'a': 1, 'b': 2, 'c': 3 }\n     */\n    function toPlainObject(value) {\n      return copyObject(value, keysIn(value));\n    }\n\n    /**\n     * Converts `value` to a safe integer. A safe integer can be compared and\n     * represented correctly.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to convert.\n     * @returns {number} Returns the converted integer.\n     * @example\n     *\n     * _.toSafeInteger(3.2);\n     * // => 3\n     *\n     * _.toSafeInteger(Number.MIN_VALUE);\n     * // => 0\n     *\n     * _.toSafeInteger(Infinity);\n     * // => 9007199254740991\n     *\n     * _.toSafeInteger('3.2');\n     * // => 3\n     */\n    function toSafeInteger(value) {\n      return value\n        ? baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER)\n        : (value === 0 ? value : 0);\n    }\n\n    /**\n     * Converts `value` to a string. An empty string is returned for `null`\n     * and `undefined` values. The sign of `-0` is preserved.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Lang\n     * @param {*} value The value to convert.\n     * @returns {string} Returns the converted string.\n     * @example\n     *\n     * _.toString(null);\n     * // => ''\n     *\n     * _.toString(-0);\n     * // => '-0'\n     *\n     * _.toString([1, 2, 3]);\n     * // => '1,2,3'\n     */\n    function toString(value) {\n      return value == null ? '' : baseToString(value);\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Assigns own enumerable string keyed properties of source objects to the\n     * destination object. Source objects are applied from left to right.\n     * Subsequent sources overwrite property assignments of previous sources.\n     *\n     * **Note:** This method mutates `object` and is loosely based on\n     * [`Object.assign`](https://mdn.io/Object/assign).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.10.0\n     * @category Object\n     * @param {Object} object The destination object.\n     * @param {...Object} [sources] The source objects.\n     * @returns {Object} Returns `object`.\n     * @see _.assignIn\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     * }\n     *\n     * function Bar() {\n     *   this.c = 3;\n     * }\n     *\n     * Foo.prototype.b = 2;\n     * Bar.prototype.d = 4;\n     *\n     * _.assign({ 'a': 0 }, new Foo, new Bar);\n     * // => { 'a': 1, 'c': 3 }\n     */\n    var assign = createAssigner(function(object, source) {\n      if (isPrototype(source) || isArrayLike(source)) {\n        copyObject(source, keys(source), object);\n        return;\n      }\n      for (var key in source) {\n        if (hasOwnProperty.call(source, key)) {\n          assignValue(object, key, source[key]);\n        }\n      }\n    });\n\n    /**\n     * This method is like `_.assign` except that it iterates over own and\n     * inherited source properties.\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @alias extend\n     * @category Object\n     * @param {Object} object The destination object.\n     * @param {...Object} [sources] The source objects.\n     * @returns {Object} Returns `object`.\n     * @see _.assign\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     * }\n     *\n     * function Bar() {\n     *   this.c = 3;\n     * }\n     *\n     * Foo.prototype.b = 2;\n     * Bar.prototype.d = 4;\n     *\n     * _.assignIn({ 'a': 0 }, new Foo, new Bar);\n     * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 }\n     */\n    var assignIn = createAssigner(function(object, source) {\n      copyObject(source, keysIn(source), object);\n    });\n\n    /**\n     * This method is like `_.assignIn` except that it accepts `customizer`\n     * which is invoked to produce the assigned values. If `customizer` returns\n     * `undefined`, assignment is handled by the method instead. The `customizer`\n     * is invoked with five arguments: (objValue, srcValue, key, object, source).\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @alias extendWith\n     * @category Object\n     * @param {Object} object The destination object.\n     * @param {...Object} sources The source objects.\n     * @param {Function} [customizer] The function to customize assigned values.\n     * @returns {Object} Returns `object`.\n     * @see _.assignWith\n     * @example\n     *\n     * function customizer(objValue, srcValue) {\n     *   return _.isUndefined(objValue) ? srcValue : objValue;\n     * }\n     *\n     * var defaults = _.partialRight(_.assignInWith, customizer);\n     *\n     * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n     * // => { 'a': 1, 'b': 2 }\n     */\n    var assignInWith = createAssigner(function(object, source, srcIndex, customizer) {\n      copyObject(source, keysIn(source), object, customizer);\n    });\n\n    /**\n     * This method is like `_.assign` except that it accepts `customizer`\n     * which is invoked to produce the assigned values. If `customizer` returns\n     * `undefined`, assignment is handled by the method instead. The `customizer`\n     * is invoked with five arguments: (objValue, srcValue, key, object, source).\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The destination object.\n     * @param {...Object} sources The source objects.\n     * @param {Function} [customizer] The function to customize assigned values.\n     * @returns {Object} Returns `object`.\n     * @see _.assignInWith\n     * @example\n     *\n     * function customizer(objValue, srcValue) {\n     *   return _.isUndefined(objValue) ? srcValue : objValue;\n     * }\n     *\n     * var defaults = _.partialRight(_.assignWith, customizer);\n     *\n     * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n     * // => { 'a': 1, 'b': 2 }\n     */\n    var assignWith = createAssigner(function(object, source, srcIndex, customizer) {\n      copyObject(source, keys(source), object, customizer);\n    });\n\n    /**\n     * Creates an array of values corresponding to `paths` of `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.0.0\n     * @category Object\n     * @param {Object} object The object to iterate over.\n     * @param {...(string|string[])} [paths] The property paths to pick.\n     * @returns {Array} Returns the picked values.\n     * @example\n     *\n     * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };\n     *\n     * _.at(object, ['a[0].b.c', 'a[1]']);\n     * // => [3, 4]\n     */\n    var at = flatRest(baseAt);\n\n    /**\n     * Creates an object that inherits from the `prototype` object. If a\n     * `properties` object is given, its own enumerable string keyed properties\n     * are assigned to the created object.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.3.0\n     * @category Object\n     * @param {Object} prototype The object to inherit from.\n     * @param {Object} [properties] The properties to assign to the object.\n     * @returns {Object} Returns the new object.\n     * @example\n     *\n     * function Shape() {\n     *   this.x = 0;\n     *   this.y = 0;\n     * }\n     *\n     * function Circle() {\n     *   Shape.call(this);\n     * }\n     *\n     * Circle.prototype = _.create(Shape.prototype, {\n     *   'constructor': Circle\n     * });\n     *\n     * var circle = new Circle;\n     * circle instanceof Circle;\n     * // => true\n     *\n     * circle instanceof Shape;\n     * // => true\n     */\n    function create(prototype, properties) {\n      var result = baseCreate(prototype);\n      return properties == null ? result : baseAssign(result, properties);\n    }\n\n    /**\n     * Assigns own and inherited enumerable string keyed properties of source\n     * objects to the destination object for all destination properties that\n     * resolve to `undefined`. Source objects are applied from left to right.\n     * Once a property is set, additional values of the same property are ignored.\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Object\n     * @param {Object} object The destination object.\n     * @param {...Object} [sources] The source objects.\n     * @returns {Object} Returns `object`.\n     * @see _.defaultsDeep\n     * @example\n     *\n     * _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n     * // => { 'a': 1, 'b': 2 }\n     */\n    var defaults = baseRest(function(object, sources) {\n      object = Object(object);\n\n      var index = -1;\n      var length = sources.length;\n      var guard = length > 2 ? sources[2] : undefined;\n\n      if (guard && isIterateeCall(sources[0], sources[1], guard)) {\n        length = 1;\n      }\n\n      while (++index < length) {\n        var source = sources[index];\n        var props = keysIn(source);\n        var propsIndex = -1;\n        var propsLength = props.length;\n\n        while (++propsIndex < propsLength) {\n          var key = props[propsIndex];\n          var value = object[key];\n\n          if (value === undefined ||\n              (eq(value, objectProto[key]) && !hasOwnProperty.call(object, key))) {\n            object[key] = source[key];\n          }\n        }\n      }\n\n      return object;\n    });\n\n    /**\n     * This method is like `_.defaults` except that it recursively assigns\n     * default properties.\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.10.0\n     * @category Object\n     * @param {Object} object The destination object.\n     * @param {...Object} [sources] The source objects.\n     * @returns {Object} Returns `object`.\n     * @see _.defaults\n     * @example\n     *\n     * _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } });\n     * // => { 'a': { 'b': 2, 'c': 3 } }\n     */\n    var defaultsDeep = baseRest(function(args) {\n      args.push(undefined, customDefaultsMerge);\n      return apply(mergeWith, undefined, args);\n    });\n\n    /**\n     * This method is like `_.find` except that it returns the key of the first\n     * element `predicate` returns truthy for instead of the element itself.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.1.0\n     * @category Object\n     * @param {Object} object The object to inspect.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {string|undefined} Returns the key of the matched element,\n     *  else `undefined`.\n     * @example\n     *\n     * var users = {\n     *   'barney':  { 'age': 36, 'active': true },\n     *   'fred':    { 'age': 40, 'active': false },\n     *   'pebbles': { 'age': 1,  'active': true }\n     * };\n     *\n     * _.findKey(users, function(o) { return o.age < 40; });\n     * // => 'barney' (iteration order is not guaranteed)\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.findKey(users, { 'age': 1, 'active': true });\n     * // => 'pebbles'\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.findKey(users, ['active', false]);\n     * // => 'fred'\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.findKey(users, 'active');\n     * // => 'barney'\n     */\n    function findKey(object, predicate) {\n      return baseFindKey(object, getIteratee(predicate, 3), baseForOwn);\n    }\n\n    /**\n     * This method is like `_.findKey` except that it iterates over elements of\n     * a collection in the opposite order.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Object\n     * @param {Object} object The object to inspect.\n     * @param {Function} [predicate=_.identity] The function invoked per iteration.\n     * @returns {string|undefined} Returns the key of the matched element,\n     *  else `undefined`.\n     * @example\n     *\n     * var users = {\n     *   'barney':  { 'age': 36, 'active': true },\n     *   'fred':    { 'age': 40, 'active': false },\n     *   'pebbles': { 'age': 1,  'active': true }\n     * };\n     *\n     * _.findLastKey(users, function(o) { return o.age < 40; });\n     * // => returns 'pebbles' assuming `_.findKey` returns 'barney'\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.findLastKey(users, { 'age': 36, 'active': true });\n     * // => 'barney'\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.findLastKey(users, ['active', false]);\n     * // => 'fred'\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.findLastKey(users, 'active');\n     * // => 'pebbles'\n     */\n    function findLastKey(object, predicate) {\n      return baseFindKey(object, getIteratee(predicate, 3), baseForOwnRight);\n    }\n\n    /**\n     * Iterates over own and inherited enumerable string keyed properties of an\n     * object and invokes `iteratee` for each property. The iteratee is invoked\n     * with three arguments: (value, key, object). Iteratee functions may exit\n     * iteration early by explicitly returning `false`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.3.0\n     * @category Object\n     * @param {Object} object The object to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Object} Returns `object`.\n     * @see _.forInRight\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.forIn(new Foo, function(value, key) {\n     *   console.log(key);\n     * });\n     * // => Logs 'a', 'b', then 'c' (iteration order is not guaranteed).\n     */\n    function forIn(object, iteratee) {\n      return object == null\n        ? object\n        : baseFor(object, getIteratee(iteratee, 3), keysIn);\n    }\n\n    /**\n     * This method is like `_.forIn` except that it iterates over properties of\n     * `object` in the opposite order.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Object\n     * @param {Object} object The object to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Object} Returns `object`.\n     * @see _.forIn\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.forInRight(new Foo, function(value, key) {\n     *   console.log(key);\n     * });\n     * // => Logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'.\n     */\n    function forInRight(object, iteratee) {\n      return object == null\n        ? object\n        : baseForRight(object, getIteratee(iteratee, 3), keysIn);\n    }\n\n    /**\n     * Iterates over own enumerable string keyed properties of an object and\n     * invokes `iteratee` for each property. The iteratee is invoked with three\n     * arguments: (value, key, object). Iteratee functions may exit iteration\n     * early by explicitly returning `false`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.3.0\n     * @category Object\n     * @param {Object} object The object to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Object} Returns `object`.\n     * @see _.forOwnRight\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.forOwn(new Foo, function(value, key) {\n     *   console.log(key);\n     * });\n     * // => Logs 'a' then 'b' (iteration order is not guaranteed).\n     */\n    function forOwn(object, iteratee) {\n      return object && baseForOwn(object, getIteratee(iteratee, 3));\n    }\n\n    /**\n     * This method is like `_.forOwn` except that it iterates over properties of\n     * `object` in the opposite order.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.0.0\n     * @category Object\n     * @param {Object} object The object to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Object} Returns `object`.\n     * @see _.forOwn\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.forOwnRight(new Foo, function(value, key) {\n     *   console.log(key);\n     * });\n     * // => Logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'.\n     */\n    function forOwnRight(object, iteratee) {\n      return object && baseForOwnRight(object, getIteratee(iteratee, 3));\n    }\n\n    /**\n     * Creates an array of function property names from own enumerable properties\n     * of `object`.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Object\n     * @param {Object} object The object to inspect.\n     * @returns {Array} Returns the function names.\n     * @see _.functionsIn\n     * @example\n     *\n     * function Foo() {\n     *   this.a = _.constant('a');\n     *   this.b = _.constant('b');\n     * }\n     *\n     * Foo.prototype.c = _.constant('c');\n     *\n     * _.functions(new Foo);\n     * // => ['a', 'b']\n     */\n    function functions(object) {\n      return object == null ? [] : baseFunctions(object, keys(object));\n    }\n\n    /**\n     * Creates an array of function property names from own and inherited\n     * enumerable properties of `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The object to inspect.\n     * @returns {Array} Returns the function names.\n     * @see _.functions\n     * @example\n     *\n     * function Foo() {\n     *   this.a = _.constant('a');\n     *   this.b = _.constant('b');\n     * }\n     *\n     * Foo.prototype.c = _.constant('c');\n     *\n     * _.functionsIn(new Foo);\n     * // => ['a', 'b', 'c']\n     */\n    function functionsIn(object) {\n      return object == null ? [] : baseFunctions(object, keysIn(object));\n    }\n\n    /**\n     * Gets the value at `path` of `object`. If the resolved value is\n     * `undefined`, the `defaultValue` is returned in its place.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.7.0\n     * @category Object\n     * @param {Object} object The object to query.\n     * @param {Array|string} path The path of the property to get.\n     * @param {*} [defaultValue] The value returned for `undefined` resolved values.\n     * @returns {*} Returns the resolved value.\n     * @example\n     *\n     * var object = { 'a': [{ 'b': { 'c': 3 } }] };\n     *\n     * _.get(object, 'a[0].b.c');\n     * // => 3\n     *\n     * _.get(object, ['a', '0', 'b', 'c']);\n     * // => 3\n     *\n     * _.get(object, 'a.b.c', 'default');\n     * // => 'default'\n     */\n    function get(object, path, defaultValue) {\n      var result = object == null ? undefined : baseGet(object, path);\n      return result === undefined ? defaultValue : result;\n    }\n\n    /**\n     * Checks if `path` is a direct property of `object`.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Object\n     * @param {Object} object The object to query.\n     * @param {Array|string} path The path to check.\n     * @returns {boolean} Returns `true` if `path` exists, else `false`.\n     * @example\n     *\n     * var object = { 'a': { 'b': 2 } };\n     * var other = _.create({ 'a': _.create({ 'b': 2 }) });\n     *\n     * _.has(object, 'a');\n     * // => true\n     *\n     * _.has(object, 'a.b');\n     * // => true\n     *\n     * _.has(object, ['a', 'b']);\n     * // => true\n     *\n     * _.has(other, 'a');\n     * // => false\n     */\n    function has(object, path) {\n      return object != null && hasPath(object, path, baseHas);\n    }\n\n    /**\n     * Checks if `path` is a direct or inherited property of `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The object to query.\n     * @param {Array|string} path The path to check.\n     * @returns {boolean} Returns `true` if `path` exists, else `false`.\n     * @example\n     *\n     * var object = _.create({ 'a': _.create({ 'b': 2 }) });\n     *\n     * _.hasIn(object, 'a');\n     * // => true\n     *\n     * _.hasIn(object, 'a.b');\n     * // => true\n     *\n     * _.hasIn(object, ['a', 'b']);\n     * // => true\n     *\n     * _.hasIn(object, 'b');\n     * // => false\n     */\n    function hasIn(object, path) {\n      return object != null && hasPath(object, path, baseHasIn);\n    }\n\n    /**\n     * Creates an object composed of the inverted keys and values of `object`.\n     * If `object` contains duplicate values, subsequent values overwrite\n     * property assignments of previous values.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.7.0\n     * @category Object\n     * @param {Object} object The object to invert.\n     * @returns {Object} Returns the new inverted object.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': 2, 'c': 1 };\n     *\n     * _.invert(object);\n     * // => { '1': 'c', '2': 'b' }\n     */\n    var invert = createInverter(function(result, value, key) {\n      if (value != null &&\n          typeof value.toString != 'function') {\n        value = nativeObjectToString.call(value);\n      }\n\n      result[value] = key;\n    }, constant(identity));\n\n    /**\n     * This method is like `_.invert` except that the inverted object is generated\n     * from the results of running each element of `object` thru `iteratee`. The\n     * corresponding inverted value of each inverted key is an array of keys\n     * responsible for generating the inverted value. The iteratee is invoked\n     * with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.1.0\n     * @category Object\n     * @param {Object} object The object to invert.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {Object} Returns the new inverted object.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': 2, 'c': 1 };\n     *\n     * _.invertBy(object);\n     * // => { '1': ['a', 'c'], '2': ['b'] }\n     *\n     * _.invertBy(object, function(value) {\n     *   return 'group' + value;\n     * });\n     * // => { 'group1': ['a', 'c'], 'group2': ['b'] }\n     */\n    var invertBy = createInverter(function(result, value, key) {\n      if (value != null &&\n          typeof value.toString != 'function') {\n        value = nativeObjectToString.call(value);\n      }\n\n      if (hasOwnProperty.call(result, value)) {\n        result[value].push(key);\n      } else {\n        result[value] = [key];\n      }\n    }, getIteratee);\n\n    /**\n     * Invokes the method at `path` of `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The object to query.\n     * @param {Array|string} path The path of the method to invoke.\n     * @param {...*} [args] The arguments to invoke the method with.\n     * @returns {*} Returns the result of the invoked method.\n     * @example\n     *\n     * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] };\n     *\n     * _.invoke(object, 'a[0].b.c.slice', 1, 3);\n     * // => [2, 3]\n     */\n    var invoke = baseRest(baseInvoke);\n\n    /**\n     * Creates an array of the own enumerable property names of `object`.\n     *\n     * **Note:** Non-object values are coerced to objects. See the\n     * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)\n     * for more details.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Object\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property names.\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.keys(new Foo);\n     * // => ['a', 'b'] (iteration order is not guaranteed)\n     *\n     * _.keys('hi');\n     * // => ['0', '1']\n     */\n    function keys(object) {\n      return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);\n    }\n\n    /**\n     * Creates an array of the own and inherited enumerable property names of `object`.\n     *\n     * **Note:** Non-object values are coerced to objects.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Object\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property names.\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.keysIn(new Foo);\n     * // => ['a', 'b', 'c'] (iteration order is not guaranteed)\n     */\n    function keysIn(object) {\n      return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);\n    }\n\n    /**\n     * The opposite of `_.mapValues`; this method creates an object with the\n     * same values as `object` and keys generated by running each own enumerable\n     * string keyed property of `object` thru `iteratee`. The iteratee is invoked\n     * with three arguments: (value, key, object).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.8.0\n     * @category Object\n     * @param {Object} object The object to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Object} Returns the new mapped object.\n     * @see _.mapValues\n     * @example\n     *\n     * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) {\n     *   return key + value;\n     * });\n     * // => { 'a1': 1, 'b2': 2 }\n     */\n    function mapKeys(object, iteratee) {\n      var result = {};\n      iteratee = getIteratee(iteratee, 3);\n\n      baseForOwn(object, function(value, key, object) {\n        baseAssignValue(result, iteratee(value, key, object), value);\n      });\n      return result;\n    }\n\n    /**\n     * Creates an object with the same keys as `object` and values generated\n     * by running each own enumerable string keyed property of `object` thru\n     * `iteratee`. The iteratee is invoked with three arguments:\n     * (value, key, object).\n     *\n     * @static\n     * @memberOf _\n     * @since 2.4.0\n     * @category Object\n     * @param {Object} object The object to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Object} Returns the new mapped object.\n     * @see _.mapKeys\n     * @example\n     *\n     * var users = {\n     *   'fred':    { 'user': 'fred',    'age': 40 },\n     *   'pebbles': { 'user': 'pebbles', 'age': 1 }\n     * };\n     *\n     * _.mapValues(users, function(o) { return o.age; });\n     * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.mapValues(users, 'age');\n     * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed)\n     */\n    function mapValues(object, iteratee) {\n      var result = {};\n      iteratee = getIteratee(iteratee, 3);\n\n      baseForOwn(object, function(value, key, object) {\n        baseAssignValue(result, key, iteratee(value, key, object));\n      });\n      return result;\n    }\n\n    /**\n     * This method is like `_.assign` except that it recursively merges own and\n     * inherited enumerable string keyed properties of source objects into the\n     * destination object. Source properties that resolve to `undefined` are\n     * skipped if a destination value exists. Array and plain object properties\n     * are merged recursively. Other objects and value types are overridden by\n     * assignment. Source objects are applied from left to right. Subsequent\n     * sources overwrite property assignments of previous sources.\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.5.0\n     * @category Object\n     * @param {Object} object The destination object.\n     * @param {...Object} [sources] The source objects.\n     * @returns {Object} Returns `object`.\n     * @example\n     *\n     * var object = {\n     *   'a': [{ 'b': 2 }, { 'd': 4 }]\n     * };\n     *\n     * var other = {\n     *   'a': [{ 'c': 3 }, { 'e': 5 }]\n     * };\n     *\n     * _.merge(object, other);\n     * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }\n     */\n    var merge = createAssigner(function(object, source, srcIndex) {\n      baseMerge(object, source, srcIndex);\n    });\n\n    /**\n     * This method is like `_.merge` except that it accepts `customizer` which\n     * is invoked to produce the merged values of the destination and source\n     * properties. If `customizer` returns `undefined`, merging is handled by the\n     * method instead. The `customizer` is invoked with six arguments:\n     * (objValue, srcValue, key, object, source, stack).\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The destination object.\n     * @param {...Object} sources The source objects.\n     * @param {Function} customizer The function to customize assigned values.\n     * @returns {Object} Returns `object`.\n     * @example\n     *\n     * function customizer(objValue, srcValue) {\n     *   if (_.isArray(objValue)) {\n     *     return objValue.concat(srcValue);\n     *   }\n     * }\n     *\n     * var object = { 'a': [1], 'b': [2] };\n     * var other = { 'a': [3], 'b': [4] };\n     *\n     * _.mergeWith(object, other, customizer);\n     * // => { 'a': [1, 3], 'b': [2, 4] }\n     */\n    var mergeWith = createAssigner(function(object, source, srcIndex, customizer) {\n      baseMerge(object, source, srcIndex, customizer);\n    });\n\n    /**\n     * The opposite of `_.pick`; this method creates an object composed of the\n     * own and inherited enumerable property paths of `object` that are not omitted.\n     *\n     * **Note:** This method is considerably slower than `_.pick`.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Object\n     * @param {Object} object The source object.\n     * @param {...(string|string[])} [paths] The property paths to omit.\n     * @returns {Object} Returns the new object.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': '2', 'c': 3 };\n     *\n     * _.omit(object, ['a', 'c']);\n     * // => { 'b': '2' }\n     */\n    var omit = flatRest(function(object, paths) {\n      var result = {};\n      if (object == null) {\n        return result;\n      }\n      var isDeep = false;\n      paths = arrayMap(paths, function(path) {\n        path = castPath(path, object);\n        isDeep || (isDeep = path.length > 1);\n        return path;\n      });\n      copyObject(object, getAllKeysIn(object), result);\n      if (isDeep) {\n        result = baseClone(result, CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAG, customOmitClone);\n      }\n      var length = paths.length;\n      while (length--) {\n        baseUnset(result, paths[length]);\n      }\n      return result;\n    });\n\n    /**\n     * The opposite of `_.pickBy`; this method creates an object composed of\n     * the own and inherited enumerable string keyed properties of `object` that\n     * `predicate` doesn't return truthy for. The predicate is invoked with two\n     * arguments: (value, key).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The source object.\n     * @param {Function} [predicate=_.identity] The function invoked per property.\n     * @returns {Object} Returns the new object.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': '2', 'c': 3 };\n     *\n     * _.omitBy(object, _.isNumber);\n     * // => { 'b': '2' }\n     */\n    function omitBy(object, predicate) {\n      return pickBy(object, negate(getIteratee(predicate)));\n    }\n\n    /**\n     * Creates an object composed of the picked `object` properties.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Object\n     * @param {Object} object The source object.\n     * @param {...(string|string[])} [paths] The property paths to pick.\n     * @returns {Object} Returns the new object.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': '2', 'c': 3 };\n     *\n     * _.pick(object, ['a', 'c']);\n     * // => { 'a': 1, 'c': 3 }\n     */\n    var pick = flatRest(function(object, paths) {\n      return object == null ? {} : basePick(object, paths);\n    });\n\n    /**\n     * Creates an object composed of the `object` properties `predicate` returns\n     * truthy for. The predicate is invoked with two arguments: (value, key).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The source object.\n     * @param {Function} [predicate=_.identity] The function invoked per property.\n     * @returns {Object} Returns the new object.\n     * @example\n     *\n     * var object = { 'a': 1, 'b': '2', 'c': 3 };\n     *\n     * _.pickBy(object, _.isNumber);\n     * // => { 'a': 1, 'c': 3 }\n     */\n    function pickBy(object, predicate) {\n      if (object == null) {\n        return {};\n      }\n      var props = arrayMap(getAllKeysIn(object), function(prop) {\n        return [prop];\n      });\n      predicate = getIteratee(predicate);\n      return basePickBy(object, props, function(value, path) {\n        return predicate(value, path[0]);\n      });\n    }\n\n    /**\n     * This method is like `_.get` except that if the resolved value is a\n     * function it's invoked with the `this` binding of its parent object and\n     * its result is returned.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Object\n     * @param {Object} object The object to query.\n     * @param {Array|string} path The path of the property to resolve.\n     * @param {*} [defaultValue] The value returned for `undefined` resolved values.\n     * @returns {*} Returns the resolved value.\n     * @example\n     *\n     * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] };\n     *\n     * _.result(object, 'a[0].b.c1');\n     * // => 3\n     *\n     * _.result(object, 'a[0].b.c2');\n     * // => 4\n     *\n     * _.result(object, 'a[0].b.c3', 'default');\n     * // => 'default'\n     *\n     * _.result(object, 'a[0].b.c3', _.constant('default'));\n     * // => 'default'\n     */\n    function result(object, path, defaultValue) {\n      path = castPath(path, object);\n\n      var index = -1,\n          length = path.length;\n\n      // Ensure the loop is entered when path is empty.\n      if (!length) {\n        length = 1;\n        object = undefined;\n      }\n      while (++index < length) {\n        var value = object == null ? undefined : object[toKey(path[index])];\n        if (value === undefined) {\n          index = length;\n          value = defaultValue;\n        }\n        object = isFunction(value) ? value.call(object) : value;\n      }\n      return object;\n    }\n\n    /**\n     * Sets the value at `path` of `object`. If a portion of `path` doesn't exist,\n     * it's created. Arrays are created for missing index properties while objects\n     * are created for all other missing properties. Use `_.setWith` to customize\n     * `path` creation.\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.7.0\n     * @category Object\n     * @param {Object} object The object to modify.\n     * @param {Array|string} path The path of the property to set.\n     * @param {*} value The value to set.\n     * @returns {Object} Returns `object`.\n     * @example\n     *\n     * var object = { 'a': [{ 'b': { 'c': 3 } }] };\n     *\n     * _.set(object, 'a[0].b.c', 4);\n     * console.log(object.a[0].b.c);\n     * // => 4\n     *\n     * _.set(object, ['x', '0', 'y', 'z'], 5);\n     * console.log(object.x[0].y.z);\n     * // => 5\n     */\n    function set(object, path, value) {\n      return object == null ? object : baseSet(object, path, value);\n    }\n\n    /**\n     * This method is like `_.set` except that it accepts `customizer` which is\n     * invoked to produce the objects of `path`.  If `customizer` returns `undefined`\n     * path creation is handled by the method instead. The `customizer` is invoked\n     * with three arguments: (nsValue, key, nsObject).\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The object to modify.\n     * @param {Array|string} path The path of the property to set.\n     * @param {*} value The value to set.\n     * @param {Function} [customizer] The function to customize assigned values.\n     * @returns {Object} Returns `object`.\n     * @example\n     *\n     * var object = {};\n     *\n     * _.setWith(object, '[0][1]', 'a', Object);\n     * // => { '0': { '1': 'a' } }\n     */\n    function setWith(object, path, value, customizer) {\n      customizer = typeof customizer == 'function' ? customizer : undefined;\n      return object == null ? object : baseSet(object, path, value, customizer);\n    }\n\n    /**\n     * Creates an array of own enumerable string keyed-value pairs for `object`\n     * which can be consumed by `_.fromPairs`. If `object` is a map or set, its\n     * entries are returned.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @alias entries\n     * @category Object\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the key-value pairs.\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.toPairs(new Foo);\n     * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed)\n     */\n    var toPairs = createToPairs(keys);\n\n    /**\n     * Creates an array of own and inherited enumerable string keyed-value pairs\n     * for `object` which can be consumed by `_.fromPairs`. If `object` is a map\n     * or set, its entries are returned.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @alias entriesIn\n     * @category Object\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the key-value pairs.\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.toPairsIn(new Foo);\n     * // => [['a', 1], ['b', 2], ['c', 3]] (iteration order is not guaranteed)\n     */\n    var toPairsIn = createToPairs(keysIn);\n\n    /**\n     * An alternative to `_.reduce`; this method transforms `object` to a new\n     * `accumulator` object which is the result of running each of its own\n     * enumerable string keyed properties thru `iteratee`, with each invocation\n     * potentially mutating the `accumulator` object. If `accumulator` is not\n     * provided, a new object with the same `[[Prototype]]` will be used. The\n     * iteratee is invoked with four arguments: (accumulator, value, key, object).\n     * Iteratee functions may exit iteration early by explicitly returning `false`.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.3.0\n     * @category Object\n     * @param {Object} object The object to iterate over.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @param {*} [accumulator] The custom accumulator value.\n     * @returns {*} Returns the accumulated value.\n     * @example\n     *\n     * _.transform([2, 3, 4], function(result, n) {\n     *   result.push(n *= n);\n     *   return n % 2 == 0;\n     * }, []);\n     * // => [4, 9]\n     *\n     * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) {\n     *   (result[value] || (result[value] = [])).push(key);\n     * }, {});\n     * // => { '1': ['a', 'c'], '2': ['b'] }\n     */\n    function transform(object, iteratee, accumulator) {\n      var isArr = isArray(object),\n          isArrLike = isArr || isBuffer(object) || isTypedArray(object);\n\n      iteratee = getIteratee(iteratee, 4);\n      if (accumulator == null) {\n        var Ctor = object && object.constructor;\n        if (isArrLike) {\n          accumulator = isArr ? new Ctor : [];\n        }\n        else if (isObject(object)) {\n          accumulator = isFunction(Ctor) ? baseCreate(getPrototype(object)) : {};\n        }\n        else {\n          accumulator = {};\n        }\n      }\n      (isArrLike ? arrayEach : baseForOwn)(object, function(value, index, object) {\n        return iteratee(accumulator, value, index, object);\n      });\n      return accumulator;\n    }\n\n    /**\n     * Removes the property at `path` of `object`.\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Object\n     * @param {Object} object The object to modify.\n     * @param {Array|string} path The path of the property to unset.\n     * @returns {boolean} Returns `true` if the property is deleted, else `false`.\n     * @example\n     *\n     * var object = { 'a': [{ 'b': { 'c': 7 } }] };\n     * _.unset(object, 'a[0].b.c');\n     * // => true\n     *\n     * console.log(object);\n     * // => { 'a': [{ 'b': {} }] };\n     *\n     * _.unset(object, ['a', '0', 'b', 'c']);\n     * // => true\n     *\n     * console.log(object);\n     * // => { 'a': [{ 'b': {} }] };\n     */\n    function unset(object, path) {\n      return object == null ? true : baseUnset(object, path);\n    }\n\n    /**\n     * This method is like `_.set` except that accepts `updater` to produce the\n     * value to set. Use `_.updateWith` to customize `path` creation. The `updater`\n     * is invoked with one argument: (value).\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.6.0\n     * @category Object\n     * @param {Object} object The object to modify.\n     * @param {Array|string} path The path of the property to set.\n     * @param {Function} updater The function to produce the updated value.\n     * @returns {Object} Returns `object`.\n     * @example\n     *\n     * var object = { 'a': [{ 'b': { 'c': 3 } }] };\n     *\n     * _.update(object, 'a[0].b.c', function(n) { return n * n; });\n     * console.log(object.a[0].b.c);\n     * // => 9\n     *\n     * _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; });\n     * console.log(object.x[0].y.z);\n     * // => 0\n     */\n    function update(object, path, updater) {\n      return object == null ? object : baseUpdate(object, path, castFunction(updater));\n    }\n\n    /**\n     * This method is like `_.update` except that it accepts `customizer` which is\n     * invoked to produce the objects of `path`.  If `customizer` returns `undefined`\n     * path creation is handled by the method instead. The `customizer` is invoked\n     * with three arguments: (nsValue, key, nsObject).\n     *\n     * **Note:** This method mutates `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.6.0\n     * @category Object\n     * @param {Object} object The object to modify.\n     * @param {Array|string} path The path of the property to set.\n     * @param {Function} updater The function to produce the updated value.\n     * @param {Function} [customizer] The function to customize assigned values.\n     * @returns {Object} Returns `object`.\n     * @example\n     *\n     * var object = {};\n     *\n     * _.updateWith(object, '[0][1]', _.constant('a'), Object);\n     * // => { '0': { '1': 'a' } }\n     */\n    function updateWith(object, path, updater, customizer) {\n      customizer = typeof customizer == 'function' ? customizer : undefined;\n      return object == null ? object : baseUpdate(object, path, castFunction(updater), customizer);\n    }\n\n    /**\n     * Creates an array of the own enumerable string keyed property values of `object`.\n     *\n     * **Note:** Non-object values are coerced to objects.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Object\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property values.\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.values(new Foo);\n     * // => [1, 2] (iteration order is not guaranteed)\n     *\n     * _.values('hi');\n     * // => ['h', 'i']\n     */\n    function values(object) {\n      return object == null ? [] : baseValues(object, keys(object));\n    }\n\n    /**\n     * Creates an array of the own and inherited enumerable string keyed property\n     * values of `object`.\n     *\n     * **Note:** Non-object values are coerced to objects.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Object\n     * @param {Object} object The object to query.\n     * @returns {Array} Returns the array of property values.\n     * @example\n     *\n     * function Foo() {\n     *   this.a = 1;\n     *   this.b = 2;\n     * }\n     *\n     * Foo.prototype.c = 3;\n     *\n     * _.valuesIn(new Foo);\n     * // => [1, 2, 3] (iteration order is not guaranteed)\n     */\n    function valuesIn(object) {\n      return object == null ? [] : baseValues(object, keysIn(object));\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Clamps `number` within the inclusive `lower` and `upper` bounds.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Number\n     * @param {number} number The number to clamp.\n     * @param {number} [lower] The lower bound.\n     * @param {number} upper The upper bound.\n     * @returns {number} Returns the clamped number.\n     * @example\n     *\n     * _.clamp(-10, -5, 5);\n     * // => -5\n     *\n     * _.clamp(10, -5, 5);\n     * // => 5\n     */\n    function clamp(number, lower, upper) {\n      if (upper === undefined) {\n        upper = lower;\n        lower = undefined;\n      }\n      if (upper !== undefined) {\n        upper = toNumber(upper);\n        upper = upper === upper ? upper : 0;\n      }\n      if (lower !== undefined) {\n        lower = toNumber(lower);\n        lower = lower === lower ? lower : 0;\n      }\n      return baseClamp(toNumber(number), lower, upper);\n    }\n\n    /**\n     * Checks if `n` is between `start` and up to, but not including, `end`. If\n     * `end` is not specified, it's set to `start` with `start` then set to `0`.\n     * If `start` is greater than `end` the params are swapped to support\n     * negative ranges.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.3.0\n     * @category Number\n     * @param {number} number The number to check.\n     * @param {number} [start=0] The start of the range.\n     * @param {number} end The end of the range.\n     * @returns {boolean} Returns `true` if `number` is in the range, else `false`.\n     * @see _.range, _.rangeRight\n     * @example\n     *\n     * _.inRange(3, 2, 4);\n     * // => true\n     *\n     * _.inRange(4, 8);\n     * // => true\n     *\n     * _.inRange(4, 2);\n     * // => false\n     *\n     * _.inRange(2, 2);\n     * // => false\n     *\n     * _.inRange(1.2, 2);\n     * // => true\n     *\n     * _.inRange(5.2, 4);\n     * // => false\n     *\n     * _.inRange(-3, -2, -6);\n     * // => true\n     */\n    function inRange(number, start, end) {\n      start = toFinite(start);\n      if (end === undefined) {\n        end = start;\n        start = 0;\n      } else {\n        end = toFinite(end);\n      }\n      number = toNumber(number);\n      return baseInRange(number, start, end);\n    }\n\n    /**\n     * Produces a random number between the inclusive `lower` and `upper` bounds.\n     * If only one argument is provided a number between `0` and the given number\n     * is returned. If `floating` is `true`, or either `lower` or `upper` are\n     * floats, a floating-point number is returned instead of an integer.\n     *\n     * **Note:** JavaScript follows the IEEE-754 standard for resolving\n     * floating-point values which can produce unexpected results.\n     *\n     * @static\n     * @memberOf _\n     * @since 0.7.0\n     * @category Number\n     * @param {number} [lower=0] The lower bound.\n     * @param {number} [upper=1] The upper bound.\n     * @param {boolean} [floating] Specify returning a floating-point number.\n     * @returns {number} Returns the random number.\n     * @example\n     *\n     * _.random(0, 5);\n     * // => an integer between 0 and 5\n     *\n     * _.random(5);\n     * // => also an integer between 0 and 5\n     *\n     * _.random(5, true);\n     * // => a floating-point number between 0 and 5\n     *\n     * _.random(1.2, 5.2);\n     * // => a floating-point number between 1.2 and 5.2\n     */\n    function random(lower, upper, floating) {\n      if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) {\n        upper = floating = undefined;\n      }\n      if (floating === undefined) {\n        if (typeof upper == 'boolean') {\n          floating = upper;\n          upper = undefined;\n        }\n        else if (typeof lower == 'boolean') {\n          floating = lower;\n          lower = undefined;\n        }\n      }\n      if (lower === undefined && upper === undefined) {\n        lower = 0;\n        upper = 1;\n      }\n      else {\n        lower = toFinite(lower);\n        if (upper === undefined) {\n          upper = lower;\n          lower = 0;\n        } else {\n          upper = toFinite(upper);\n        }\n      }\n      if (lower > upper) {\n        var temp = lower;\n        lower = upper;\n        upper = temp;\n      }\n      if (floating || lower % 1 || upper % 1) {\n        var rand = nativeRandom();\n        return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper);\n      }\n      return baseRandom(lower, upper);\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the camel cased string.\n     * @example\n     *\n     * _.camelCase('Foo Bar');\n     * // => 'fooBar'\n     *\n     * _.camelCase('--foo-bar--');\n     * // => 'fooBar'\n     *\n     * _.camelCase('__FOO_BAR__');\n     * // => 'fooBar'\n     */\n    var camelCase = createCompounder(function(result, word, index) {\n      word = word.toLowerCase();\n      return result + (index ? capitalize(word) : word);\n    });\n\n    /**\n     * Converts the first character of `string` to upper case and the remaining\n     * to lower case.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to capitalize.\n     * @returns {string} Returns the capitalized string.\n     * @example\n     *\n     * _.capitalize('FRED');\n     * // => 'Fred'\n     */\n    function capitalize(string) {\n      return upperFirst(toString(string).toLowerCase());\n    }\n\n    /**\n     * Deburrs `string` by converting\n     * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table)\n     * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A)\n     * letters to basic Latin letters and removing\n     * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to deburr.\n     * @returns {string} Returns the deburred string.\n     * @example\n     *\n     * _.deburr('déjà vu');\n     * // => 'deja vu'\n     */\n    function deburr(string) {\n      string = toString(string);\n      return string && string.replace(reLatin, deburrLetter).replace(reComboMark, '');\n    }\n\n    /**\n     * Checks if `string` ends with the given target string.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to inspect.\n     * @param {string} [target] The string to search for.\n     * @param {number} [position=string.length] The position to search up to.\n     * @returns {boolean} Returns `true` if `string` ends with `target`,\n     *  else `false`.\n     * @example\n     *\n     * _.endsWith('abc', 'c');\n     * // => true\n     *\n     * _.endsWith('abc', 'b');\n     * // => false\n     *\n     * _.endsWith('abc', 'b', 2);\n     * // => true\n     */\n    function endsWith(string, target, position) {\n      string = toString(string);\n      target = baseToString(target);\n\n      var length = string.length;\n      position = position === undefined\n        ? length\n        : baseClamp(toInteger(position), 0, length);\n\n      var end = position;\n      position -= target.length;\n      return position >= 0 && string.slice(position, end) == target;\n    }\n\n    /**\n     * Converts the characters \"&\", \"<\", \">\", '\"', and \"'\" in `string` to their\n     * corresponding HTML entities.\n     *\n     * **Note:** No other characters are escaped. To escape additional\n     * characters use a third-party library like [_he_](https://mths.be/he).\n     *\n     * Though the \">\" character is escaped for symmetry, characters like\n     * \">\" and \"/\" don't need escaping in HTML and have no special meaning\n     * unless they're part of a tag or unquoted attribute value. See\n     * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands)\n     * (under \"semi-related fun fact\") for more details.\n     *\n     * When working with HTML you should always\n     * [quote attribute values](http://wonko.com/post/html-escaping) to reduce\n     * XSS vectors.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category String\n     * @param {string} [string=''] The string to escape.\n     * @returns {string} Returns the escaped string.\n     * @example\n     *\n     * _.escape('fred, barney, & pebbles');\n     * // => 'fred, barney, &amp; pebbles'\n     */\n    function escape(string) {\n      string = toString(string);\n      return (string && reHasUnescapedHtml.test(string))\n        ? string.replace(reUnescapedHtml, escapeHtmlChar)\n        : string;\n    }\n\n    /**\n     * Escapes the `RegExp` special characters \"^\", \"$\", \"\\\", \".\", \"*\", \"+\",\n     * \"?\", \"(\", \")\", \"[\", \"]\", \"{\", \"}\", and \"|\" in `string`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to escape.\n     * @returns {string} Returns the escaped string.\n     * @example\n     *\n     * _.escapeRegExp('[lodash](https://lodash.com/)');\n     * // => '\\[lodash\\]\\(https://lodash\\.com/\\)'\n     */\n    function escapeRegExp(string) {\n      string = toString(string);\n      return (string && reHasRegExpChar.test(string))\n        ? string.replace(reRegExpChar, '\\\\$&')\n        : string;\n    }\n\n    /**\n     * Converts `string` to\n     * [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the kebab cased string.\n     * @example\n     *\n     * _.kebabCase('Foo Bar');\n     * // => 'foo-bar'\n     *\n     * _.kebabCase('fooBar');\n     * // => 'foo-bar'\n     *\n     * _.kebabCase('__FOO_BAR__');\n     * // => 'foo-bar'\n     */\n    var kebabCase = createCompounder(function(result, word, index) {\n      return result + (index ? '-' : '') + word.toLowerCase();\n    });\n\n    /**\n     * Converts `string`, as space separated words, to lower case.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the lower cased string.\n     * @example\n     *\n     * _.lowerCase('--Foo-Bar--');\n     * // => 'foo bar'\n     *\n     * _.lowerCase('fooBar');\n     * // => 'foo bar'\n     *\n     * _.lowerCase('__FOO_BAR__');\n     * // => 'foo bar'\n     */\n    var lowerCase = createCompounder(function(result, word, index) {\n      return result + (index ? ' ' : '') + word.toLowerCase();\n    });\n\n    /**\n     * Converts the first character of `string` to lower case.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the converted string.\n     * @example\n     *\n     * _.lowerFirst('Fred');\n     * // => 'fred'\n     *\n     * _.lowerFirst('FRED');\n     * // => 'fRED'\n     */\n    var lowerFirst = createCaseFirst('toLowerCase');\n\n    /**\n     * Pads `string` on the left and right sides if it's shorter than `length`.\n     * Padding characters are truncated if they can't be evenly divided by `length`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to pad.\n     * @param {number} [length=0] The padding length.\n     * @param {string} [chars=' '] The string used as padding.\n     * @returns {string} Returns the padded string.\n     * @example\n     *\n     * _.pad('abc', 8);\n     * // => '  abc   '\n     *\n     * _.pad('abc', 8, '_-');\n     * // => '_-abc_-_'\n     *\n     * _.pad('abc', 3);\n     * // => 'abc'\n     */\n    function pad(string, length, chars) {\n      string = toString(string);\n      length = toInteger(length);\n\n      var strLength = length ? stringSize(string) : 0;\n      if (!length || strLength >= length) {\n        return string;\n      }\n      var mid = (length - strLength) / 2;\n      return (\n        createPadding(nativeFloor(mid), chars) +\n        string +\n        createPadding(nativeCeil(mid), chars)\n      );\n    }\n\n    /**\n     * Pads `string` on the right side if it's shorter than `length`. Padding\n     * characters are truncated if they exceed `length`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to pad.\n     * @param {number} [length=0] The padding length.\n     * @param {string} [chars=' '] The string used as padding.\n     * @returns {string} Returns the padded string.\n     * @example\n     *\n     * _.padEnd('abc', 6);\n     * // => 'abc   '\n     *\n     * _.padEnd('abc', 6, '_-');\n     * // => 'abc_-_'\n     *\n     * _.padEnd('abc', 3);\n     * // => 'abc'\n     */\n    function padEnd(string, length, chars) {\n      string = toString(string);\n      length = toInteger(length);\n\n      var strLength = length ? stringSize(string) : 0;\n      return (length && strLength < length)\n        ? (string + createPadding(length - strLength, chars))\n        : string;\n    }\n\n    /**\n     * Pads `string` on the left side if it's shorter than `length`. Padding\n     * characters are truncated if they exceed `length`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to pad.\n     * @param {number} [length=0] The padding length.\n     * @param {string} [chars=' '] The string used as padding.\n     * @returns {string} Returns the padded string.\n     * @example\n     *\n     * _.padStart('abc', 6);\n     * // => '   abc'\n     *\n     * _.padStart('abc', 6, '_-');\n     * // => '_-_abc'\n     *\n     * _.padStart('abc', 3);\n     * // => 'abc'\n     */\n    function padStart(string, length, chars) {\n      string = toString(string);\n      length = toInteger(length);\n\n      var strLength = length ? stringSize(string) : 0;\n      return (length && strLength < length)\n        ? (createPadding(length - strLength, chars) + string)\n        : string;\n    }\n\n    /**\n     * Converts `string` to an integer of the specified radix. If `radix` is\n     * `undefined` or `0`, a `radix` of `10` is used unless `value` is a\n     * hexadecimal, in which case a `radix` of `16` is used.\n     *\n     * **Note:** This method aligns with the\n     * [ES5 implementation](https://es5.github.io/#x15.1.2.2) of `parseInt`.\n     *\n     * @static\n     * @memberOf _\n     * @since 1.1.0\n     * @category String\n     * @param {string} string The string to convert.\n     * @param {number} [radix=10] The radix to interpret `value` by.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {number} Returns the converted integer.\n     * @example\n     *\n     * _.parseInt('08');\n     * // => 8\n     *\n     * _.map(['6', '08', '10'], _.parseInt);\n     * // => [6, 8, 10]\n     */\n    function parseInt(string, radix, guard) {\n      if (guard || radix == null) {\n        radix = 0;\n      } else if (radix) {\n        radix = +radix;\n      }\n      return nativeParseInt(toString(string).replace(reTrimStart, ''), radix || 0);\n    }\n\n    /**\n     * Repeats the given string `n` times.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to repeat.\n     * @param {number} [n=1] The number of times to repeat the string.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {string} Returns the repeated string.\n     * @example\n     *\n     * _.repeat('*', 3);\n     * // => '***'\n     *\n     * _.repeat('abc', 2);\n     * // => 'abcabc'\n     *\n     * _.repeat('abc', 0);\n     * // => ''\n     */\n    function repeat(string, n, guard) {\n      if ((guard ? isIterateeCall(string, n, guard) : n === undefined)) {\n        n = 1;\n      } else {\n        n = toInteger(n);\n      }\n      return baseRepeat(toString(string), n);\n    }\n\n    /**\n     * Replaces matches for `pattern` in `string` with `replacement`.\n     *\n     * **Note:** This method is based on\n     * [`String#replace`](https://mdn.io/String/replace).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to modify.\n     * @param {RegExp|string} pattern The pattern to replace.\n     * @param {Function|string} replacement The match replacement.\n     * @returns {string} Returns the modified string.\n     * @example\n     *\n     * _.replace('Hi Fred', 'Fred', 'Barney');\n     * // => 'Hi Barney'\n     */\n    function replace() {\n      var args = arguments,\n          string = toString(args[0]);\n\n      return args.length < 3 ? string : string.replace(args[1], args[2]);\n    }\n\n    /**\n     * Converts `string` to\n     * [snake case](https://en.wikipedia.org/wiki/Snake_case).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the snake cased string.\n     * @example\n     *\n     * _.snakeCase('Foo Bar');\n     * // => 'foo_bar'\n     *\n     * _.snakeCase('fooBar');\n     * // => 'foo_bar'\n     *\n     * _.snakeCase('--FOO-BAR--');\n     * // => 'foo_bar'\n     */\n    var snakeCase = createCompounder(function(result, word, index) {\n      return result + (index ? '_' : '') + word.toLowerCase();\n    });\n\n    /**\n     * Splits `string` by `separator`.\n     *\n     * **Note:** This method is based on\n     * [`String#split`](https://mdn.io/String/split).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to split.\n     * @param {RegExp|string} separator The separator pattern to split by.\n     * @param {number} [limit] The length to truncate results to.\n     * @returns {Array} Returns the string segments.\n     * @example\n     *\n     * _.split('a-b-c', '-', 2);\n     * // => ['a', 'b']\n     */\n    function split(string, separator, limit) {\n      if (limit && typeof limit != 'number' && isIterateeCall(string, separator, limit)) {\n        separator = limit = undefined;\n      }\n      limit = limit === undefined ? MAX_ARRAY_LENGTH : limit >>> 0;\n      if (!limit) {\n        return [];\n      }\n      string = toString(string);\n      if (string && (\n            typeof separator == 'string' ||\n            (separator != null && !isRegExp(separator))\n          )) {\n        separator = baseToString(separator);\n        if (!separator && hasUnicode(string)) {\n          return castSlice(stringToArray(string), 0, limit);\n        }\n      }\n      return string.split(separator, limit);\n    }\n\n    /**\n     * Converts `string` to\n     * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage).\n     *\n     * @static\n     * @memberOf _\n     * @since 3.1.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the start cased string.\n     * @example\n     *\n     * _.startCase('--foo-bar--');\n     * // => 'Foo Bar'\n     *\n     * _.startCase('fooBar');\n     * // => 'Foo Bar'\n     *\n     * _.startCase('__FOO_BAR__');\n     * // => 'FOO BAR'\n     */\n    var startCase = createCompounder(function(result, word, index) {\n      return result + (index ? ' ' : '') + upperFirst(word);\n    });\n\n    /**\n     * Checks if `string` starts with the given target string.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to inspect.\n     * @param {string} [target] The string to search for.\n     * @param {number} [position=0] The position to search from.\n     * @returns {boolean} Returns `true` if `string` starts with `target`,\n     *  else `false`.\n     * @example\n     *\n     * _.startsWith('abc', 'a');\n     * // => true\n     *\n     * _.startsWith('abc', 'b');\n     * // => false\n     *\n     * _.startsWith('abc', 'b', 1);\n     * // => true\n     */\n    function startsWith(string, target, position) {\n      string = toString(string);\n      position = position == null\n        ? 0\n        : baseClamp(toInteger(position), 0, string.length);\n\n      target = baseToString(target);\n      return string.slice(position, position + target.length) == target;\n    }\n\n    /**\n     * Creates a compiled template function that can interpolate data properties\n     * in \"interpolate\" delimiters, HTML-escape interpolated data properties in\n     * \"escape\" delimiters, and execute JavaScript in \"evaluate\" delimiters. Data\n     * properties may be accessed as free variables in the template. If a setting\n     * object is given, it takes precedence over `_.templateSettings` values.\n     *\n     * **Note:** In the development build `_.template` utilizes\n     * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl)\n     * for easier debugging.\n     *\n     * For more information on precompiling templates see\n     * [lodash's custom builds documentation](https://lodash.com/custom-builds).\n     *\n     * For more information on Chrome extension sandboxes see\n     * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval).\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category String\n     * @param {string} [string=''] The template string.\n     * @param {Object} [options={}] The options object.\n     * @param {RegExp} [options.escape=_.templateSettings.escape]\n     *  The HTML \"escape\" delimiter.\n     * @param {RegExp} [options.evaluate=_.templateSettings.evaluate]\n     *  The \"evaluate\" delimiter.\n     * @param {Object} [options.imports=_.templateSettings.imports]\n     *  An object to import into the template as free variables.\n     * @param {RegExp} [options.interpolate=_.templateSettings.interpolate]\n     *  The \"interpolate\" delimiter.\n     * @param {string} [options.sourceURL='lodash.templateSources[n]']\n     *  The sourceURL of the compiled template.\n     * @param {string} [options.variable='obj']\n     *  The data object variable name.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Function} Returns the compiled template function.\n     * @example\n     *\n     * // Use the \"interpolate\" delimiter to create a compiled template.\n     * var compiled = _.template('hello <%= user %>!');\n     * compiled({ 'user': 'fred' });\n     * // => 'hello fred!'\n     *\n     * // Use the HTML \"escape\" delimiter to escape data property values.\n     * var compiled = _.template('<b><%- value %></b>');\n     * compiled({ 'value': '<script>' });\n     * // => '<b>&lt;script&gt;</b>'\n     *\n     * // Use the \"evaluate\" delimiter to execute JavaScript and generate HTML.\n     * var compiled = _.template('<% _.forEach(users, function(user) { %><li><%- user %></li><% }); %>');\n     * compiled({ 'users': ['fred', 'barney'] });\n     * // => '<li>fred</li><li>barney</li>'\n     *\n     * // Use the internal `print` function in \"evaluate\" delimiters.\n     * var compiled = _.template('<% print(\"hello \" + user); %>!');\n     * compiled({ 'user': 'barney' });\n     * // => 'hello barney!'\n     *\n     * // Use the ES template literal delimiter as an \"interpolate\" delimiter.\n     * // Disable support by replacing the \"interpolate\" delimiter.\n     * var compiled = _.template('hello ${ user }!');\n     * compiled({ 'user': 'pebbles' });\n     * // => 'hello pebbles!'\n     *\n     * // Use backslashes to treat delimiters as plain text.\n     * var compiled = _.template('<%= \"\\\\<%- value %\\\\>\" %>');\n     * compiled({ 'value': 'ignored' });\n     * // => '<%- value %>'\n     *\n     * // Use the `imports` option to import `jQuery` as `jq`.\n     * var text = '<% jq.each(users, function(user) { %><li><%- user %></li><% }); %>';\n     * var compiled = _.template(text, { 'imports': { 'jq': jQuery } });\n     * compiled({ 'users': ['fred', 'barney'] });\n     * // => '<li>fred</li><li>barney</li>'\n     *\n     * // Use the `sourceURL` option to specify a custom sourceURL for the template.\n     * var compiled = _.template('hello <%= user %>!', { 'sourceURL': '/basic/greeting.jst' });\n     * compiled(data);\n     * // => Find the source of \"greeting.jst\" under the Sources tab or Resources panel of the web inspector.\n     *\n     * // Use the `variable` option to ensure a with-statement isn't used in the compiled template.\n     * var compiled = _.template('hi <%= data.user %>!', { 'variable': 'data' });\n     * compiled.source;\n     * // => function(data) {\n     * //   var __t, __p = '';\n     * //   __p += 'hi ' + ((__t = ( data.user )) == null ? '' : __t) + '!';\n     * //   return __p;\n     * // }\n     *\n     * // Use custom template delimiters.\n     * _.templateSettings.interpolate = /{{([\\s\\S]+?)}}/g;\n     * var compiled = _.template('hello {{ user }}!');\n     * compiled({ 'user': 'mustache' });\n     * // => 'hello mustache!'\n     *\n     * // Use the `source` property to inline compiled templates for meaningful\n     * // line numbers in error messages and stack traces.\n     * fs.writeFileSync(path.join(process.cwd(), 'jst.js'), '\\\n     *   var JST = {\\\n     *     \"main\": ' + _.template(mainText).source + '\\\n     *   };\\\n     * ');\n     */\n    function template(string, options, guard) {\n      // Based on John Resig's `tmpl` implementation\n      // (http://ejohn.org/blog/javascript-micro-templating/)\n      // and Laura Doktorova's doT.js (https://github.com/olado/doT).\n      var settings = lodash.templateSettings;\n\n      if (guard && isIterateeCall(string, options, guard)) {\n        options = undefined;\n      }\n      string = toString(string);\n      options = assignInWith({}, options, settings, customDefaultsAssignIn);\n\n      var imports = assignInWith({}, options.imports, settings.imports, customDefaultsAssignIn),\n          importsKeys = keys(imports),\n          importsValues = baseValues(imports, importsKeys);\n\n      var isEscaping,\n          isEvaluating,\n          index = 0,\n          interpolate = options.interpolate || reNoMatch,\n          source = \"__p += '\";\n\n      // Compile the regexp to match each delimiter.\n      var reDelimiters = RegExp(\n        (options.escape || reNoMatch).source + '|' +\n        interpolate.source + '|' +\n        (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + '|' +\n        (options.evaluate || reNoMatch).source + '|$'\n      , 'g');\n\n      // Use a sourceURL for easier debugging.\n      // The sourceURL gets injected into the source that's eval-ed, so be careful\n      // to normalize all kinds of whitespace, so e.g. newlines (and unicode versions of it) can't sneak in\n      // and escape the comment, thus injecting code that gets evaled.\n      var sourceURL = '//# sourceURL=' +\n        (hasOwnProperty.call(options, 'sourceURL')\n          ? (options.sourceURL + '').replace(/\\s/g, ' ')\n          : ('lodash.templateSources[' + (++templateCounter) + ']')\n        ) + '\\n';\n\n      string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) {\n        interpolateValue || (interpolateValue = esTemplateValue);\n\n        // Escape characters that can't be included in string literals.\n        source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar);\n\n        // Replace delimiters with snippets.\n        if (escapeValue) {\n          isEscaping = true;\n          source += \"' +\\n__e(\" + escapeValue + \") +\\n'\";\n        }\n        if (evaluateValue) {\n          isEvaluating = true;\n          source += \"';\\n\" + evaluateValue + \";\\n__p += '\";\n        }\n        if (interpolateValue) {\n          source += \"' +\\n((__t = (\" + interpolateValue + \")) == null ? '' : __t) +\\n'\";\n        }\n        index = offset + match.length;\n\n        // The JS engine embedded in Adobe products needs `match` returned in\n        // order to produce the correct `offset` value.\n        return match;\n      });\n\n      source += \"';\\n\";\n\n      // If `variable` is not specified wrap a with-statement around the generated\n      // code to add the data object to the top of the scope chain.\n      var variable = hasOwnProperty.call(options, 'variable') && options.variable;\n      if (!variable) {\n        source = 'with (obj) {\\n' + source + '\\n}\\n';\n      }\n      // Cleanup code by stripping empty strings.\n      source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source)\n        .replace(reEmptyStringMiddle, '$1')\n        .replace(reEmptyStringTrailing, '$1;');\n\n      // Frame code as the function body.\n      source = 'function(' + (variable || 'obj') + ') {\\n' +\n        (variable\n          ? ''\n          : 'obj || (obj = {});\\n'\n        ) +\n        \"var __t, __p = ''\" +\n        (isEscaping\n           ? ', __e = _.escape'\n           : ''\n        ) +\n        (isEvaluating\n          ? ', __j = Array.prototype.join;\\n' +\n            \"function print() { __p += __j.call(arguments, '') }\\n\"\n          : ';\\n'\n        ) +\n        source +\n        'return __p\\n}';\n\n      var result = attempt(function() {\n        return Function(importsKeys, sourceURL + 'return ' + source)\n          .apply(undefined, importsValues);\n      });\n\n      // Provide the compiled function's source by its `toString` method or\n      // the `source` property as a convenience for inlining compiled templates.\n      result.source = source;\n      if (isError(result)) {\n        throw result;\n      }\n      return result;\n    }\n\n    /**\n     * Converts `string`, as a whole, to lower case just like\n     * [String#toLowerCase](https://mdn.io/toLowerCase).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the lower cased string.\n     * @example\n     *\n     * _.toLower('--Foo-Bar--');\n     * // => '--foo-bar--'\n     *\n     * _.toLower('fooBar');\n     * // => 'foobar'\n     *\n     * _.toLower('__FOO_BAR__');\n     * // => '__foo_bar__'\n     */\n    function toLower(value) {\n      return toString(value).toLowerCase();\n    }\n\n    /**\n     * Converts `string`, as a whole, to upper case just like\n     * [String#toUpperCase](https://mdn.io/toUpperCase).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the upper cased string.\n     * @example\n     *\n     * _.toUpper('--foo-bar--');\n     * // => '--FOO-BAR--'\n     *\n     * _.toUpper('fooBar');\n     * // => 'FOOBAR'\n     *\n     * _.toUpper('__foo_bar__');\n     * // => '__FOO_BAR__'\n     */\n    function toUpper(value) {\n      return toString(value).toUpperCase();\n    }\n\n    /**\n     * Removes leading and trailing whitespace or specified characters from `string`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to trim.\n     * @param {string} [chars=whitespace] The characters to trim.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {string} Returns the trimmed string.\n     * @example\n     *\n     * _.trim('  abc  ');\n     * // => 'abc'\n     *\n     * _.trim('-_-abc-_-', '_-');\n     * // => 'abc'\n     *\n     * _.map(['  foo  ', '  bar  '], _.trim);\n     * // => ['foo', 'bar']\n     */\n    function trim(string, chars, guard) {\n      string = toString(string);\n      if (string && (guard || chars === undefined)) {\n        return string.replace(reTrim, '');\n      }\n      if (!string || !(chars = baseToString(chars))) {\n        return string;\n      }\n      var strSymbols = stringToArray(string),\n          chrSymbols = stringToArray(chars),\n          start = charsStartIndex(strSymbols, chrSymbols),\n          end = charsEndIndex(strSymbols, chrSymbols) + 1;\n\n      return castSlice(strSymbols, start, end).join('');\n    }\n\n    /**\n     * Removes trailing whitespace or specified characters from `string`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to trim.\n     * @param {string} [chars=whitespace] The characters to trim.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {string} Returns the trimmed string.\n     * @example\n     *\n     * _.trimEnd('  abc  ');\n     * // => '  abc'\n     *\n     * _.trimEnd('-_-abc-_-', '_-');\n     * // => '-_-abc'\n     */\n    function trimEnd(string, chars, guard) {\n      string = toString(string);\n      if (string && (guard || chars === undefined)) {\n        return string.replace(reTrimEnd, '');\n      }\n      if (!string || !(chars = baseToString(chars))) {\n        return string;\n      }\n      var strSymbols = stringToArray(string),\n          end = charsEndIndex(strSymbols, stringToArray(chars)) + 1;\n\n      return castSlice(strSymbols, 0, end).join('');\n    }\n\n    /**\n     * Removes leading whitespace or specified characters from `string`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to trim.\n     * @param {string} [chars=whitespace] The characters to trim.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {string} Returns the trimmed string.\n     * @example\n     *\n     * _.trimStart('  abc  ');\n     * // => 'abc  '\n     *\n     * _.trimStart('-_-abc-_-', '_-');\n     * // => 'abc-_-'\n     */\n    function trimStart(string, chars, guard) {\n      string = toString(string);\n      if (string && (guard || chars === undefined)) {\n        return string.replace(reTrimStart, '');\n      }\n      if (!string || !(chars = baseToString(chars))) {\n        return string;\n      }\n      var strSymbols = stringToArray(string),\n          start = charsStartIndex(strSymbols, stringToArray(chars));\n\n      return castSlice(strSymbols, start).join('');\n    }\n\n    /**\n     * Truncates `string` if it's longer than the given maximum string length.\n     * The last characters of the truncated string are replaced with the omission\n     * string which defaults to \"...\".\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to truncate.\n     * @param {Object} [options={}] The options object.\n     * @param {number} [options.length=30] The maximum string length.\n     * @param {string} [options.omission='...'] The string to indicate text is omitted.\n     * @param {RegExp|string} [options.separator] The separator pattern to truncate to.\n     * @returns {string} Returns the truncated string.\n     * @example\n     *\n     * _.truncate('hi-diddly-ho there, neighborino');\n     * // => 'hi-diddly-ho there, neighbo...'\n     *\n     * _.truncate('hi-diddly-ho there, neighborino', {\n     *   'length': 24,\n     *   'separator': ' '\n     * });\n     * // => 'hi-diddly-ho there,...'\n     *\n     * _.truncate('hi-diddly-ho there, neighborino', {\n     *   'length': 24,\n     *   'separator': /,? +/\n     * });\n     * // => 'hi-diddly-ho there...'\n     *\n     * _.truncate('hi-diddly-ho there, neighborino', {\n     *   'omission': ' [...]'\n     * });\n     * // => 'hi-diddly-ho there, neig [...]'\n     */\n    function truncate(string, options) {\n      var length = DEFAULT_TRUNC_LENGTH,\n          omission = DEFAULT_TRUNC_OMISSION;\n\n      if (isObject(options)) {\n        var separator = 'separator' in options ? options.separator : separator;\n        length = 'length' in options ? toInteger(options.length) : length;\n        omission = 'omission' in options ? baseToString(options.omission) : omission;\n      }\n      string = toString(string);\n\n      var strLength = string.length;\n      if (hasUnicode(string)) {\n        var strSymbols = stringToArray(string);\n        strLength = strSymbols.length;\n      }\n      if (length >= strLength) {\n        return string;\n      }\n      var end = length - stringSize(omission);\n      if (end < 1) {\n        return omission;\n      }\n      var result = strSymbols\n        ? castSlice(strSymbols, 0, end).join('')\n        : string.slice(0, end);\n\n      if (separator === undefined) {\n        return result + omission;\n      }\n      if (strSymbols) {\n        end += (result.length - end);\n      }\n      if (isRegExp(separator)) {\n        if (string.slice(end).search(separator)) {\n          var match,\n              substring = result;\n\n          if (!separator.global) {\n            separator = RegExp(separator.source, toString(reFlags.exec(separator)) + 'g');\n          }\n          separator.lastIndex = 0;\n          while ((match = separator.exec(substring))) {\n            var newEnd = match.index;\n          }\n          result = result.slice(0, newEnd === undefined ? end : newEnd);\n        }\n      } else if (string.indexOf(baseToString(separator), end) != end) {\n        var index = result.lastIndexOf(separator);\n        if (index > -1) {\n          result = result.slice(0, index);\n        }\n      }\n      return result + omission;\n    }\n\n    /**\n     * The inverse of `_.escape`; this method converts the HTML entities\n     * `&amp;`, `&lt;`, `&gt;`, `&quot;`, and `&#39;` in `string` to\n     * their corresponding characters.\n     *\n     * **Note:** No other HTML entities are unescaped. To unescape additional\n     * HTML entities use a third-party library like [_he_](https://mths.be/he).\n     *\n     * @static\n     * @memberOf _\n     * @since 0.6.0\n     * @category String\n     * @param {string} [string=''] The string to unescape.\n     * @returns {string} Returns the unescaped string.\n     * @example\n     *\n     * _.unescape('fred, barney, &amp; pebbles');\n     * // => 'fred, barney, & pebbles'\n     */\n    function unescape(string) {\n      string = toString(string);\n      return (string && reHasEscapedHtml.test(string))\n        ? string.replace(reEscapedHtml, unescapeHtmlChar)\n        : string;\n    }\n\n    /**\n     * Converts `string`, as space separated words, to upper case.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the upper cased string.\n     * @example\n     *\n     * _.upperCase('--foo-bar');\n     * // => 'FOO BAR'\n     *\n     * _.upperCase('fooBar');\n     * // => 'FOO BAR'\n     *\n     * _.upperCase('__foo_bar__');\n     * // => 'FOO BAR'\n     */\n    var upperCase = createCompounder(function(result, word, index) {\n      return result + (index ? ' ' : '') + word.toUpperCase();\n    });\n\n    /**\n     * Converts the first character of `string` to upper case.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category String\n     * @param {string} [string=''] The string to convert.\n     * @returns {string} Returns the converted string.\n     * @example\n     *\n     * _.upperFirst('fred');\n     * // => 'Fred'\n     *\n     * _.upperFirst('FRED');\n     * // => 'FRED'\n     */\n    var upperFirst = createCaseFirst('toUpperCase');\n\n    /**\n     * Splits `string` into an array of its words.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category String\n     * @param {string} [string=''] The string to inspect.\n     * @param {RegExp|string} [pattern] The pattern to match words.\n     * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.\n     * @returns {Array} Returns the words of `string`.\n     * @example\n     *\n     * _.words('fred, barney, & pebbles');\n     * // => ['fred', 'barney', 'pebbles']\n     *\n     * _.words('fred, barney, & pebbles', /[^, ]+/g);\n     * // => ['fred', 'barney', '&', 'pebbles']\n     */\n    function words(string, pattern, guard) {\n      string = toString(string);\n      pattern = guard ? undefined : pattern;\n\n      if (pattern === undefined) {\n        return hasUnicodeWord(string) ? unicodeWords(string) : asciiWords(string);\n      }\n      return string.match(pattern) || [];\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Attempts to invoke `func`, returning either the result or the caught error\n     * object. Any additional arguments are provided to `func` when it's invoked.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Util\n     * @param {Function} func The function to attempt.\n     * @param {...*} [args] The arguments to invoke `func` with.\n     * @returns {*} Returns the `func` result or error object.\n     * @example\n     *\n     * // Avoid throwing errors for invalid selectors.\n     * var elements = _.attempt(function(selector) {\n     *   return document.querySelectorAll(selector);\n     * }, '>_>');\n     *\n     * if (_.isError(elements)) {\n     *   elements = [];\n     * }\n     */\n    var attempt = baseRest(function(func, args) {\n      try {\n        return apply(func, undefined, args);\n      } catch (e) {\n        return isError(e) ? e : new Error(e);\n      }\n    });\n\n    /**\n     * Binds methods of an object to the object itself, overwriting the existing\n     * method.\n     *\n     * **Note:** This method doesn't set the \"length\" property of bound functions.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Util\n     * @param {Object} object The object to bind and assign the bound methods to.\n     * @param {...(string|string[])} methodNames The object method names to bind.\n     * @returns {Object} Returns `object`.\n     * @example\n     *\n     * var view = {\n     *   'label': 'docs',\n     *   'click': function() {\n     *     console.log('clicked ' + this.label);\n     *   }\n     * };\n     *\n     * _.bindAll(view, ['click']);\n     * jQuery(element).on('click', view.click);\n     * // => Logs 'clicked docs' when clicked.\n     */\n    var bindAll = flatRest(function(object, methodNames) {\n      arrayEach(methodNames, function(key) {\n        key = toKey(key);\n        baseAssignValue(object, key, bind(object[key], object));\n      });\n      return object;\n    });\n\n    /**\n     * Creates a function that iterates over `pairs` and invokes the corresponding\n     * function of the first predicate to return truthy. The predicate-function\n     * pairs are invoked with the `this` binding and arguments of the created\n     * function.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Util\n     * @param {Array} pairs The predicate-function pairs.\n     * @returns {Function} Returns the new composite function.\n     * @example\n     *\n     * var func = _.cond([\n     *   [_.matches({ 'a': 1 }),           _.constant('matches A')],\n     *   [_.conforms({ 'b': _.isNumber }), _.constant('matches B')],\n     *   [_.stubTrue,                      _.constant('no match')]\n     * ]);\n     *\n     * func({ 'a': 1, 'b': 2 });\n     * // => 'matches A'\n     *\n     * func({ 'a': 0, 'b': 1 });\n     * // => 'matches B'\n     *\n     * func({ 'a': '1', 'b': '2' });\n     * // => 'no match'\n     */\n    function cond(pairs) {\n      var length = pairs == null ? 0 : pairs.length,\n          toIteratee = getIteratee();\n\n      pairs = !length ? [] : arrayMap(pairs, function(pair) {\n        if (typeof pair[1] != 'function') {\n          throw new TypeError(FUNC_ERROR_TEXT);\n        }\n        return [toIteratee(pair[0]), pair[1]];\n      });\n\n      return baseRest(function(args) {\n        var index = -1;\n        while (++index < length) {\n          var pair = pairs[index];\n          if (apply(pair[0], this, args)) {\n            return apply(pair[1], this, args);\n          }\n        }\n      });\n    }\n\n    /**\n     * Creates a function that invokes the predicate properties of `source` with\n     * the corresponding property values of a given object, returning `true` if\n     * all predicates return truthy, else `false`.\n     *\n     * **Note:** The created function is equivalent to `_.conformsTo` with\n     * `source` partially applied.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Util\n     * @param {Object} source The object of property predicates to conform to.\n     * @returns {Function} Returns the new spec function.\n     * @example\n     *\n     * var objects = [\n     *   { 'a': 2, 'b': 1 },\n     *   { 'a': 1, 'b': 2 }\n     * ];\n     *\n     * _.filter(objects, _.conforms({ 'b': function(n) { return n > 1; } }));\n     * // => [{ 'a': 1, 'b': 2 }]\n     */\n    function conforms(source) {\n      return baseConforms(baseClone(source, CLONE_DEEP_FLAG));\n    }\n\n    /**\n     * Creates a function that returns `value`.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.4.0\n     * @category Util\n     * @param {*} value The value to return from the new function.\n     * @returns {Function} Returns the new constant function.\n     * @example\n     *\n     * var objects = _.times(2, _.constant({ 'a': 1 }));\n     *\n     * console.log(objects);\n     * // => [{ 'a': 1 }, { 'a': 1 }]\n     *\n     * console.log(objects[0] === objects[1]);\n     * // => true\n     */\n    function constant(value) {\n      return function() {\n        return value;\n      };\n    }\n\n    /**\n     * Checks `value` to determine whether a default value should be returned in\n     * its place. The `defaultValue` is returned if `value` is `NaN`, `null`,\n     * or `undefined`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.14.0\n     * @category Util\n     * @param {*} value The value to check.\n     * @param {*} defaultValue The default value.\n     * @returns {*} Returns the resolved value.\n     * @example\n     *\n     * _.defaultTo(1, 10);\n     * // => 1\n     *\n     * _.defaultTo(undefined, 10);\n     * // => 10\n     */\n    function defaultTo(value, defaultValue) {\n      return (value == null || value !== value) ? defaultValue : value;\n    }\n\n    /**\n     * Creates a function that returns the result of invoking the given functions\n     * with the `this` binding of the created function, where each successive\n     * invocation is supplied the return value of the previous.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Util\n     * @param {...(Function|Function[])} [funcs] The functions to invoke.\n     * @returns {Function} Returns the new composite function.\n     * @see _.flowRight\n     * @example\n     *\n     * function square(n) {\n     *   return n * n;\n     * }\n     *\n     * var addSquare = _.flow([_.add, square]);\n     * addSquare(1, 2);\n     * // => 9\n     */\n    var flow = createFlow();\n\n    /**\n     * This method is like `_.flow` except that it creates a function that\n     * invokes the given functions from right to left.\n     *\n     * @static\n     * @since 3.0.0\n     * @memberOf _\n     * @category Util\n     * @param {...(Function|Function[])} [funcs] The functions to invoke.\n     * @returns {Function} Returns the new composite function.\n     * @see _.flow\n     * @example\n     *\n     * function square(n) {\n     *   return n * n;\n     * }\n     *\n     * var addSquare = _.flowRight([square, _.add]);\n     * addSquare(1, 2);\n     * // => 9\n     */\n    var flowRight = createFlow(true);\n\n    /**\n     * This method returns the first argument it receives.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Util\n     * @param {*} value Any value.\n     * @returns {*} Returns `value`.\n     * @example\n     *\n     * var object = { 'a': 1 };\n     *\n     * console.log(_.identity(object) === object);\n     * // => true\n     */\n    function identity(value) {\n      return value;\n    }\n\n    /**\n     * Creates a function that invokes `func` with the arguments of the created\n     * function. If `func` is a property name, the created function returns the\n     * property value for a given element. If `func` is an array or object, the\n     * created function returns `true` for elements that contain the equivalent\n     * source properties, otherwise it returns `false`.\n     *\n     * @static\n     * @since 4.0.0\n     * @memberOf _\n     * @category Util\n     * @param {*} [func=_.identity] The value to convert to a callback.\n     * @returns {Function} Returns the callback.\n     * @example\n     *\n     * var users = [\n     *   { 'user': 'barney', 'age': 36, 'active': true },\n     *   { 'user': 'fred',   'age': 40, 'active': false }\n     * ];\n     *\n     * // The `_.matches` iteratee shorthand.\n     * _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));\n     * // => [{ 'user': 'barney', 'age': 36, 'active': true }]\n     *\n     * // The `_.matchesProperty` iteratee shorthand.\n     * _.filter(users, _.iteratee(['user', 'fred']));\n     * // => [{ 'user': 'fred', 'age': 40 }]\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.map(users, _.iteratee('user'));\n     * // => ['barney', 'fred']\n     *\n     * // Create custom iteratee shorthands.\n     * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {\n     *   return !_.isRegExp(func) ? iteratee(func) : function(string) {\n     *     return func.test(string);\n     *   };\n     * });\n     *\n     * _.filter(['abc', 'def'], /ef/);\n     * // => ['def']\n     */\n    function iteratee(func) {\n      return baseIteratee(typeof func == 'function' ? func : baseClone(func, CLONE_DEEP_FLAG));\n    }\n\n    /**\n     * Creates a function that performs a partial deep comparison between a given\n     * object and `source`, returning `true` if the given object has equivalent\n     * property values, else `false`.\n     *\n     * **Note:** The created function is equivalent to `_.isMatch` with `source`\n     * partially applied.\n     *\n     * Partial comparisons will match empty array and empty object `source`\n     * values against any array or object value, respectively. See `_.isEqual`\n     * for a list of supported value comparisons.\n     *\n     * **Note:** Multiple values can be checked by combining several matchers\n     * using `_.overSome`\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Util\n     * @param {Object} source The object of property values to match.\n     * @returns {Function} Returns the new spec function.\n     * @example\n     *\n     * var objects = [\n     *   { 'a': 1, 'b': 2, 'c': 3 },\n     *   { 'a': 4, 'b': 5, 'c': 6 }\n     * ];\n     *\n     * _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));\n     * // => [{ 'a': 4, 'b': 5, 'c': 6 }]\n     *\n     * // Checking for several possible values\n     * _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));\n     * // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]\n     */\n    function matches(source) {\n      return baseMatches(baseClone(source, CLONE_DEEP_FLAG));\n    }\n\n    /**\n     * Creates a function that performs a partial deep comparison between the\n     * value at `path` of a given object to `srcValue`, returning `true` if the\n     * object value is equivalent, else `false`.\n     *\n     * **Note:** Partial comparisons will match empty array and empty object\n     * `srcValue` values against any array or object value, respectively. See\n     * `_.isEqual` for a list of supported value comparisons.\n     *\n     * **Note:** Multiple values can be checked by combining several matchers\n     * using `_.overSome`\n     *\n     * @static\n     * @memberOf _\n     * @since 3.2.0\n     * @category Util\n     * @param {Array|string} path The path of the property to get.\n     * @param {*} srcValue The value to match.\n     * @returns {Function} Returns the new spec function.\n     * @example\n     *\n     * var objects = [\n     *   { 'a': 1, 'b': 2, 'c': 3 },\n     *   { 'a': 4, 'b': 5, 'c': 6 }\n     * ];\n     *\n     * _.find(objects, _.matchesProperty('a', 4));\n     * // => { 'a': 4, 'b': 5, 'c': 6 }\n     *\n     * // Checking for several possible values\n     * _.filter(objects, _.overSome([_.matchesProperty('a', 1), _.matchesProperty('a', 4)]));\n     * // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]\n     */\n    function matchesProperty(path, srcValue) {\n      return baseMatchesProperty(path, baseClone(srcValue, CLONE_DEEP_FLAG));\n    }\n\n    /**\n     * Creates a function that invokes the method at `path` of a given object.\n     * Any additional arguments are provided to the invoked method.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.7.0\n     * @category Util\n     * @param {Array|string} path The path of the method to invoke.\n     * @param {...*} [args] The arguments to invoke the method with.\n     * @returns {Function} Returns the new invoker function.\n     * @example\n     *\n     * var objects = [\n     *   { 'a': { 'b': _.constant(2) } },\n     *   { 'a': { 'b': _.constant(1) } }\n     * ];\n     *\n     * _.map(objects, _.method('a.b'));\n     * // => [2, 1]\n     *\n     * _.map(objects, _.method(['a', 'b']));\n     * // => [2, 1]\n     */\n    var method = baseRest(function(path, args) {\n      return function(object) {\n        return baseInvoke(object, path, args);\n      };\n    });\n\n    /**\n     * The opposite of `_.method`; this method creates a function that invokes\n     * the method at a given path of `object`. Any additional arguments are\n     * provided to the invoked method.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.7.0\n     * @category Util\n     * @param {Object} object The object to query.\n     * @param {...*} [args] The arguments to invoke the method with.\n     * @returns {Function} Returns the new invoker function.\n     * @example\n     *\n     * var array = _.times(3, _.constant),\n     *     object = { 'a': array, 'b': array, 'c': array };\n     *\n     * _.map(['a[2]', 'c[0]'], _.methodOf(object));\n     * // => [2, 0]\n     *\n     * _.map([['a', '2'], ['c', '0']], _.methodOf(object));\n     * // => [2, 0]\n     */\n    var methodOf = baseRest(function(object, args) {\n      return function(path) {\n        return baseInvoke(object, path, args);\n      };\n    });\n\n    /**\n     * Adds all own enumerable string keyed function properties of a source\n     * object to the destination object. If `object` is a function, then methods\n     * are added to its prototype as well.\n     *\n     * **Note:** Use `_.runInContext` to create a pristine `lodash` function to\n     * avoid conflicts caused by modifying the original.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Util\n     * @param {Function|Object} [object=lodash] The destination object.\n     * @param {Object} source The object of functions to add.\n     * @param {Object} [options={}] The options object.\n     * @param {boolean} [options.chain=true] Specify whether mixins are chainable.\n     * @returns {Function|Object} Returns `object`.\n     * @example\n     *\n     * function vowels(string) {\n     *   return _.filter(string, function(v) {\n     *     return /[aeiou]/i.test(v);\n     *   });\n     * }\n     *\n     * _.mixin({ 'vowels': vowels });\n     * _.vowels('fred');\n     * // => ['e']\n     *\n     * _('fred').vowels().value();\n     * // => ['e']\n     *\n     * _.mixin({ 'vowels': vowels }, { 'chain': false });\n     * _('fred').vowels();\n     * // => ['e']\n     */\n    function mixin(object, source, options) {\n      var props = keys(source),\n          methodNames = baseFunctions(source, props);\n\n      if (options == null &&\n          !(isObject(source) && (methodNames.length || !props.length))) {\n        options = source;\n        source = object;\n        object = this;\n        methodNames = baseFunctions(source, keys(source));\n      }\n      var chain = !(isObject(options) && 'chain' in options) || !!options.chain,\n          isFunc = isFunction(object);\n\n      arrayEach(methodNames, function(methodName) {\n        var func = source[methodName];\n        object[methodName] = func;\n        if (isFunc) {\n          object.prototype[methodName] = function() {\n            var chainAll = this.__chain__;\n            if (chain || chainAll) {\n              var result = object(this.__wrapped__),\n                  actions = result.__actions__ = copyArray(this.__actions__);\n\n              actions.push({ 'func': func, 'args': arguments, 'thisArg': object });\n              result.__chain__ = chainAll;\n              return result;\n            }\n            return func.apply(object, arrayPush([this.value()], arguments));\n          };\n        }\n      });\n\n      return object;\n    }\n\n    /**\n     * Reverts the `_` variable to its previous value and returns a reference to\n     * the `lodash` function.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Util\n     * @returns {Function} Returns the `lodash` function.\n     * @example\n     *\n     * var lodash = _.noConflict();\n     */\n    function noConflict() {\n      if (root._ === this) {\n        root._ = oldDash;\n      }\n      return this;\n    }\n\n    /**\n     * This method returns `undefined`.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.3.0\n     * @category Util\n     * @example\n     *\n     * _.times(2, _.noop);\n     * // => [undefined, undefined]\n     */\n    function noop() {\n      // No operation performed.\n    }\n\n    /**\n     * Creates a function that gets the argument at index `n`. If `n` is negative,\n     * the nth argument from the end is returned.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Util\n     * @param {number} [n=0] The index of the argument to return.\n     * @returns {Function} Returns the new pass-thru function.\n     * @example\n     *\n     * var func = _.nthArg(1);\n     * func('a', 'b', 'c', 'd');\n     * // => 'b'\n     *\n     * var func = _.nthArg(-2);\n     * func('a', 'b', 'c', 'd');\n     * // => 'c'\n     */\n    function nthArg(n) {\n      n = toInteger(n);\n      return baseRest(function(args) {\n        return baseNth(args, n);\n      });\n    }\n\n    /**\n     * Creates a function that invokes `iteratees` with the arguments it receives\n     * and returns their results.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Util\n     * @param {...(Function|Function[])} [iteratees=[_.identity]]\n     *  The iteratees to invoke.\n     * @returns {Function} Returns the new function.\n     * @example\n     *\n     * var func = _.over([Math.max, Math.min]);\n     *\n     * func(1, 2, 3, 4);\n     * // => [4, 1]\n     */\n    var over = createOver(arrayMap);\n\n    /**\n     * Creates a function that checks if **all** of the `predicates` return\n     * truthy when invoked with the arguments it receives.\n     *\n     * Following shorthands are possible for providing predicates.\n     * Pass an `Object` and it will be used as an parameter for `_.matches` to create the predicate.\n     * Pass an `Array` of parameters for `_.matchesProperty` and the predicate will be created using them.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Util\n     * @param {...(Function|Function[])} [predicates=[_.identity]]\n     *  The predicates to check.\n     * @returns {Function} Returns the new function.\n     * @example\n     *\n     * var func = _.overEvery([Boolean, isFinite]);\n     *\n     * func('1');\n     * // => true\n     *\n     * func(null);\n     * // => false\n     *\n     * func(NaN);\n     * // => false\n     */\n    var overEvery = createOver(arrayEvery);\n\n    /**\n     * Creates a function that checks if **any** of the `predicates` return\n     * truthy when invoked with the arguments it receives.\n     *\n     * Following shorthands are possible for providing predicates.\n     * Pass an `Object` and it will be used as an parameter for `_.matches` to create the predicate.\n     * Pass an `Array` of parameters for `_.matchesProperty` and the predicate will be created using them.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Util\n     * @param {...(Function|Function[])} [predicates=[_.identity]]\n     *  The predicates to check.\n     * @returns {Function} Returns the new function.\n     * @example\n     *\n     * var func = _.overSome([Boolean, isFinite]);\n     *\n     * func('1');\n     * // => true\n     *\n     * func(null);\n     * // => true\n     *\n     * func(NaN);\n     * // => false\n     *\n     * var matchesFunc = _.overSome([{ 'a': 1 }, { 'a': 2 }])\n     * var matchesPropertyFunc = _.overSome([['a', 1], ['a', 2]])\n     */\n    var overSome = createOver(arraySome);\n\n    /**\n     * Creates a function that returns the value at `path` of a given object.\n     *\n     * @static\n     * @memberOf _\n     * @since 2.4.0\n     * @category Util\n     * @param {Array|string} path The path of the property to get.\n     * @returns {Function} Returns the new accessor function.\n     * @example\n     *\n     * var objects = [\n     *   { 'a': { 'b': 2 } },\n     *   { 'a': { 'b': 1 } }\n     * ];\n     *\n     * _.map(objects, _.property('a.b'));\n     * // => [2, 1]\n     *\n     * _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b');\n     * // => [1, 2]\n     */\n    function property(path) {\n      return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path);\n    }\n\n    /**\n     * The opposite of `_.property`; this method creates a function that returns\n     * the value at a given path of `object`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.0.0\n     * @category Util\n     * @param {Object} object The object to query.\n     * @returns {Function} Returns the new accessor function.\n     * @example\n     *\n     * var array = [0, 1, 2],\n     *     object = { 'a': array, 'b': array, 'c': array };\n     *\n     * _.map(['a[2]', 'c[0]'], _.propertyOf(object));\n     * // => [2, 0]\n     *\n     * _.map([['a', '2'], ['c', '0']], _.propertyOf(object));\n     * // => [2, 0]\n     */\n    function propertyOf(object) {\n      return function(path) {\n        return object == null ? undefined : baseGet(object, path);\n      };\n    }\n\n    /**\n     * Creates an array of numbers (positive and/or negative) progressing from\n     * `start` up to, but not including, `end`. A step of `-1` is used if a negative\n     * `start` is specified without an `end` or `step`. If `end` is not specified,\n     * it's set to `start` with `start` then set to `0`.\n     *\n     * **Note:** JavaScript follows the IEEE-754 standard for resolving\n     * floating-point values which can produce unexpected results.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Util\n     * @param {number} [start=0] The start of the range.\n     * @param {number} end The end of the range.\n     * @param {number} [step=1] The value to increment or decrement by.\n     * @returns {Array} Returns the range of numbers.\n     * @see _.inRange, _.rangeRight\n     * @example\n     *\n     * _.range(4);\n     * // => [0, 1, 2, 3]\n     *\n     * _.range(-4);\n     * // => [0, -1, -2, -3]\n     *\n     * _.range(1, 5);\n     * // => [1, 2, 3, 4]\n     *\n     * _.range(0, 20, 5);\n     * // => [0, 5, 10, 15]\n     *\n     * _.range(0, -4, -1);\n     * // => [0, -1, -2, -3]\n     *\n     * _.range(1, 4, 0);\n     * // => [1, 1, 1]\n     *\n     * _.range(0);\n     * // => []\n     */\n    var range = createRange();\n\n    /**\n     * This method is like `_.range` except that it populates values in\n     * descending order.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Util\n     * @param {number} [start=0] The start of the range.\n     * @param {number} end The end of the range.\n     * @param {number} [step=1] The value to increment or decrement by.\n     * @returns {Array} Returns the range of numbers.\n     * @see _.inRange, _.range\n     * @example\n     *\n     * _.rangeRight(4);\n     * // => [3, 2, 1, 0]\n     *\n     * _.rangeRight(-4);\n     * // => [-3, -2, -1, 0]\n     *\n     * _.rangeRight(1, 5);\n     * // => [4, 3, 2, 1]\n     *\n     * _.rangeRight(0, 20, 5);\n     * // => [15, 10, 5, 0]\n     *\n     * _.rangeRight(0, -4, -1);\n     * // => [-3, -2, -1, 0]\n     *\n     * _.rangeRight(1, 4, 0);\n     * // => [1, 1, 1]\n     *\n     * _.rangeRight(0);\n     * // => []\n     */\n    var rangeRight = createRange(true);\n\n    /**\n     * This method returns a new empty array.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.13.0\n     * @category Util\n     * @returns {Array} Returns the new empty array.\n     * @example\n     *\n     * var arrays = _.times(2, _.stubArray);\n     *\n     * console.log(arrays);\n     * // => [[], []]\n     *\n     * console.log(arrays[0] === arrays[1]);\n     * // => false\n     */\n    function stubArray() {\n      return [];\n    }\n\n    /**\n     * This method returns `false`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.13.0\n     * @category Util\n     * @returns {boolean} Returns `false`.\n     * @example\n     *\n     * _.times(2, _.stubFalse);\n     * // => [false, false]\n     */\n    function stubFalse() {\n      return false;\n    }\n\n    /**\n     * This method returns a new empty object.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.13.0\n     * @category Util\n     * @returns {Object} Returns the new empty object.\n     * @example\n     *\n     * var objects = _.times(2, _.stubObject);\n     *\n     * console.log(objects);\n     * // => [{}, {}]\n     *\n     * console.log(objects[0] === objects[1]);\n     * // => false\n     */\n    function stubObject() {\n      return {};\n    }\n\n    /**\n     * This method returns an empty string.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.13.0\n     * @category Util\n     * @returns {string} Returns the empty string.\n     * @example\n     *\n     * _.times(2, _.stubString);\n     * // => ['', '']\n     */\n    function stubString() {\n      return '';\n    }\n\n    /**\n     * This method returns `true`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.13.0\n     * @category Util\n     * @returns {boolean} Returns `true`.\n     * @example\n     *\n     * _.times(2, _.stubTrue);\n     * // => [true, true]\n     */\n    function stubTrue() {\n      return true;\n    }\n\n    /**\n     * Invokes the iteratee `n` times, returning an array of the results of\n     * each invocation. The iteratee is invoked with one argument; (index).\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Util\n     * @param {number} n The number of times to invoke `iteratee`.\n     * @param {Function} [iteratee=_.identity] The function invoked per iteration.\n     * @returns {Array} Returns the array of results.\n     * @example\n     *\n     * _.times(3, String);\n     * // => ['0', '1', '2']\n     *\n     *  _.times(4, _.constant(0));\n     * // => [0, 0, 0, 0]\n     */\n    function times(n, iteratee) {\n      n = toInteger(n);\n      if (n < 1 || n > MAX_SAFE_INTEGER) {\n        return [];\n      }\n      var index = MAX_ARRAY_LENGTH,\n          length = nativeMin(n, MAX_ARRAY_LENGTH);\n\n      iteratee = getIteratee(iteratee);\n      n -= MAX_ARRAY_LENGTH;\n\n      var result = baseTimes(length, iteratee);\n      while (++index < n) {\n        iteratee(index);\n      }\n      return result;\n    }\n\n    /**\n     * Converts `value` to a property path array.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Util\n     * @param {*} value The value to convert.\n     * @returns {Array} Returns the new property path array.\n     * @example\n     *\n     * _.toPath('a.b.c');\n     * // => ['a', 'b', 'c']\n     *\n     * _.toPath('a[0].b.c');\n     * // => ['a', '0', 'b', 'c']\n     */\n    function toPath(value) {\n      if (isArray(value)) {\n        return arrayMap(value, toKey);\n      }\n      return isSymbol(value) ? [value] : copyArray(stringToPath(toString(value)));\n    }\n\n    /**\n     * Generates a unique ID. If `prefix` is given, the ID is appended to it.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Util\n     * @param {string} [prefix=''] The value to prefix the ID with.\n     * @returns {string} Returns the unique ID.\n     * @example\n     *\n     * _.uniqueId('contact_');\n     * // => 'contact_104'\n     *\n     * _.uniqueId();\n     * // => '105'\n     */\n    function uniqueId(prefix) {\n      var id = ++idCounter;\n      return toString(prefix) + id;\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * Adds two numbers.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.4.0\n     * @category Math\n     * @param {number} augend The first number in an addition.\n     * @param {number} addend The second number in an addition.\n     * @returns {number} Returns the total.\n     * @example\n     *\n     * _.add(6, 4);\n     * // => 10\n     */\n    var add = createMathOperation(function(augend, addend) {\n      return augend + addend;\n    }, 0);\n\n    /**\n     * Computes `number` rounded up to `precision`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.10.0\n     * @category Math\n     * @param {number} number The number to round up.\n     * @param {number} [precision=0] The precision to round up to.\n     * @returns {number} Returns the rounded up number.\n     * @example\n     *\n     * _.ceil(4.006);\n     * // => 5\n     *\n     * _.ceil(6.004, 2);\n     * // => 6.01\n     *\n     * _.ceil(6040, -2);\n     * // => 6100\n     */\n    var ceil = createRound('ceil');\n\n    /**\n     * Divide two numbers.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.7.0\n     * @category Math\n     * @param {number} dividend The first number in a division.\n     * @param {number} divisor The second number in a division.\n     * @returns {number} Returns the quotient.\n     * @example\n     *\n     * _.divide(6, 4);\n     * // => 1.5\n     */\n    var divide = createMathOperation(function(dividend, divisor) {\n      return dividend / divisor;\n    }, 1);\n\n    /**\n     * Computes `number` rounded down to `precision`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.10.0\n     * @category Math\n     * @param {number} number The number to round down.\n     * @param {number} [precision=0] The precision to round down to.\n     * @returns {number} Returns the rounded down number.\n     * @example\n     *\n     * _.floor(4.006);\n     * // => 4\n     *\n     * _.floor(0.046, 2);\n     * // => 0.04\n     *\n     * _.floor(4060, -2);\n     * // => 4000\n     */\n    var floor = createRound('floor');\n\n    /**\n     * Computes the maximum value of `array`. If `array` is empty or falsey,\n     * `undefined` is returned.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Math\n     * @param {Array} array The array to iterate over.\n     * @returns {*} Returns the maximum value.\n     * @example\n     *\n     * _.max([4, 2, 8, 6]);\n     * // => 8\n     *\n     * _.max([]);\n     * // => undefined\n     */\n    function max(array) {\n      return (array && array.length)\n        ? baseExtremum(array, identity, baseGt)\n        : undefined;\n    }\n\n    /**\n     * This method is like `_.max` except that it accepts `iteratee` which is\n     * invoked for each element in `array` to generate the criterion by which\n     * the value is ranked. The iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Math\n     * @param {Array} array The array to iterate over.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {*} Returns the maximum value.\n     * @example\n     *\n     * var objects = [{ 'n': 1 }, { 'n': 2 }];\n     *\n     * _.maxBy(objects, function(o) { return o.n; });\n     * // => { 'n': 2 }\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.maxBy(objects, 'n');\n     * // => { 'n': 2 }\n     */\n    function maxBy(array, iteratee) {\n      return (array && array.length)\n        ? baseExtremum(array, getIteratee(iteratee, 2), baseGt)\n        : undefined;\n    }\n\n    /**\n     * Computes the mean of the values in `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Math\n     * @param {Array} array The array to iterate over.\n     * @returns {number} Returns the mean.\n     * @example\n     *\n     * _.mean([4, 2, 8, 6]);\n     * // => 5\n     */\n    function mean(array) {\n      return baseMean(array, identity);\n    }\n\n    /**\n     * This method is like `_.mean` except that it accepts `iteratee` which is\n     * invoked for each element in `array` to generate the value to be averaged.\n     * The iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.7.0\n     * @category Math\n     * @param {Array} array The array to iterate over.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {number} Returns the mean.\n     * @example\n     *\n     * var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];\n     *\n     * _.meanBy(objects, function(o) { return o.n; });\n     * // => 5\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.meanBy(objects, 'n');\n     * // => 5\n     */\n    function meanBy(array, iteratee) {\n      return baseMean(array, getIteratee(iteratee, 2));\n    }\n\n    /**\n     * Computes the minimum value of `array`. If `array` is empty or falsey,\n     * `undefined` is returned.\n     *\n     * @static\n     * @since 0.1.0\n     * @memberOf _\n     * @category Math\n     * @param {Array} array The array to iterate over.\n     * @returns {*} Returns the minimum value.\n     * @example\n     *\n     * _.min([4, 2, 8, 6]);\n     * // => 2\n     *\n     * _.min([]);\n     * // => undefined\n     */\n    function min(array) {\n      return (array && array.length)\n        ? baseExtremum(array, identity, baseLt)\n        : undefined;\n    }\n\n    /**\n     * This method is like `_.min` except that it accepts `iteratee` which is\n     * invoked for each element in `array` to generate the criterion by which\n     * the value is ranked. The iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Math\n     * @param {Array} array The array to iterate over.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {*} Returns the minimum value.\n     * @example\n     *\n     * var objects = [{ 'n': 1 }, { 'n': 2 }];\n     *\n     * _.minBy(objects, function(o) { return o.n; });\n     * // => { 'n': 1 }\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.minBy(objects, 'n');\n     * // => { 'n': 1 }\n     */\n    function minBy(array, iteratee) {\n      return (array && array.length)\n        ? baseExtremum(array, getIteratee(iteratee, 2), baseLt)\n        : undefined;\n    }\n\n    /**\n     * Multiply two numbers.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.7.0\n     * @category Math\n     * @param {number} multiplier The first number in a multiplication.\n     * @param {number} multiplicand The second number in a multiplication.\n     * @returns {number} Returns the product.\n     * @example\n     *\n     * _.multiply(6, 4);\n     * // => 24\n     */\n    var multiply = createMathOperation(function(multiplier, multiplicand) {\n      return multiplier * multiplicand;\n    }, 1);\n\n    /**\n     * Computes `number` rounded to `precision`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.10.0\n     * @category Math\n     * @param {number} number The number to round.\n     * @param {number} [precision=0] The precision to round to.\n     * @returns {number} Returns the rounded number.\n     * @example\n     *\n     * _.round(4.006);\n     * // => 4\n     *\n     * _.round(4.006, 2);\n     * // => 4.01\n     *\n     * _.round(4060, -2);\n     * // => 4100\n     */\n    var round = createRound('round');\n\n    /**\n     * Subtract two numbers.\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Math\n     * @param {number} minuend The first number in a subtraction.\n     * @param {number} subtrahend The second number in a subtraction.\n     * @returns {number} Returns the difference.\n     * @example\n     *\n     * _.subtract(6, 4);\n     * // => 2\n     */\n    var subtract = createMathOperation(function(minuend, subtrahend) {\n      return minuend - subtrahend;\n    }, 0);\n\n    /**\n     * Computes the sum of the values in `array`.\n     *\n     * @static\n     * @memberOf _\n     * @since 3.4.0\n     * @category Math\n     * @param {Array} array The array to iterate over.\n     * @returns {number} Returns the sum.\n     * @example\n     *\n     * _.sum([4, 2, 8, 6]);\n     * // => 20\n     */\n    function sum(array) {\n      return (array && array.length)\n        ? baseSum(array, identity)\n        : 0;\n    }\n\n    /**\n     * This method is like `_.sum` except that it accepts `iteratee` which is\n     * invoked for each element in `array` to generate the value to be summed.\n     * The iteratee is invoked with one argument: (value).\n     *\n     * @static\n     * @memberOf _\n     * @since 4.0.0\n     * @category Math\n     * @param {Array} array The array to iterate over.\n     * @param {Function} [iteratee=_.identity] The iteratee invoked per element.\n     * @returns {number} Returns the sum.\n     * @example\n     *\n     * var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];\n     *\n     * _.sumBy(objects, function(o) { return o.n; });\n     * // => 20\n     *\n     * // The `_.property` iteratee shorthand.\n     * _.sumBy(objects, 'n');\n     * // => 20\n     */\n    function sumBy(array, iteratee) {\n      return (array && array.length)\n        ? baseSum(array, getIteratee(iteratee, 2))\n        : 0;\n    }\n\n    /*------------------------------------------------------------------------*/\n\n    // Add methods that return wrapped values in chain sequences.\n    lodash.after = after;\n    lodash.ary = ary;\n    lodash.assign = assign;\n    lodash.assignIn = assignIn;\n    lodash.assignInWith = assignInWith;\n    lodash.assignWith = assignWith;\n    lodash.at = at;\n    lodash.before = before;\n    lodash.bind = bind;\n    lodash.bindAll = bindAll;\n    lodash.bindKey = bindKey;\n    lodash.castArray = castArray;\n    lodash.chain = chain;\n    lodash.chunk = chunk;\n    lodash.compact = compact;\n    lodash.concat = concat;\n    lodash.cond = cond;\n    lodash.conforms = conforms;\n    lodash.constant = constant;\n    lodash.countBy = countBy;\n    lodash.create = create;\n    lodash.curry = curry;\n    lodash.curryRight = curryRight;\n    lodash.debounce = debounce;\n    lodash.defaults = defaults;\n    lodash.defaultsDeep = defaultsDeep;\n    lodash.defer = defer;\n    lodash.delay = delay;\n    lodash.difference = difference;\n    lodash.differenceBy = differenceBy;\n    lodash.differenceWith = differenceWith;\n    lodash.drop = drop;\n    lodash.dropRight = dropRight;\n    lodash.dropRightWhile = dropRightWhile;\n    lodash.dropWhile = dropWhile;\n    lodash.fill = fill;\n    lodash.filter = filter;\n    lodash.flatMap = flatMap;\n    lodash.flatMapDeep = flatMapDeep;\n    lodash.flatMapDepth = flatMapDepth;\n    lodash.flatten = flatten;\n    lodash.flattenDeep = flattenDeep;\n    lodash.flattenDepth = flattenDepth;\n    lodash.flip = flip;\n    lodash.flow = flow;\n    lodash.flowRight = flowRight;\n    lodash.fromPairs = fromPairs;\n    lodash.functions = functions;\n    lodash.functionsIn = functionsIn;\n    lodash.groupBy = groupBy;\n    lodash.initial = initial;\n    lodash.intersection = intersection;\n    lodash.intersectionBy = intersectionBy;\n    lodash.intersectionWith = intersectionWith;\n    lodash.invert = invert;\n    lodash.invertBy = invertBy;\n    lodash.invokeMap = invokeMap;\n    lodash.iteratee = iteratee;\n    lodash.keyBy = keyBy;\n    lodash.keys = keys;\n    lodash.keysIn = keysIn;\n    lodash.map = map;\n    lodash.mapKeys = mapKeys;\n    lodash.mapValues = mapValues;\n    lodash.matches = matches;\n    lodash.matchesProperty = matchesProperty;\n    lodash.memoize = memoize;\n    lodash.merge = merge;\n    lodash.mergeWith = mergeWith;\n    lodash.method = method;\n    lodash.methodOf = methodOf;\n    lodash.mixin = mixin;\n    lodash.negate = negate;\n    lodash.nthArg = nthArg;\n    lodash.omit = omit;\n    lodash.omitBy = omitBy;\n    lodash.once = once;\n    lodash.orderBy = orderBy;\n    lodash.over = over;\n    lodash.overArgs = overArgs;\n    lodash.overEvery = overEvery;\n    lodash.overSome = overSome;\n    lodash.partial = partial;\n    lodash.partialRight = partialRight;\n    lodash.partition = partition;\n    lodash.pick = pick;\n    lodash.pickBy = pickBy;\n    lodash.property = property;\n    lodash.propertyOf = propertyOf;\n    lodash.pull = pull;\n    lodash.pullAll = pullAll;\n    lodash.pullAllBy = pullAllBy;\n    lodash.pullAllWith = pullAllWith;\n    lodash.pullAt = pullAt;\n    lodash.range = range;\n    lodash.rangeRight = rangeRight;\n    lodash.rearg = rearg;\n    lodash.reject = reject;\n    lodash.remove = remove;\n    lodash.rest = rest;\n    lodash.reverse = reverse;\n    lodash.sampleSize = sampleSize;\n    lodash.set = set;\n    lodash.setWith = setWith;\n    lodash.shuffle = shuffle;\n    lodash.slice = slice;\n    lodash.sortBy = sortBy;\n    lodash.sortedUniq = sortedUniq;\n    lodash.sortedUniqBy = sortedUniqBy;\n    lodash.split = split;\n    lodash.spread = spread;\n    lodash.tail = tail;\n    lodash.take = take;\n    lodash.takeRight = takeRight;\n    lodash.takeRightWhile = takeRightWhile;\n    lodash.takeWhile = takeWhile;\n    lodash.tap = tap;\n    lodash.throttle = throttle;\n    lodash.thru = thru;\n    lodash.toArray = toArray;\n    lodash.toPairs = toPairs;\n    lodash.toPairsIn = toPairsIn;\n    lodash.toPath = toPath;\n    lodash.toPlainObject = toPlainObject;\n    lodash.transform = transform;\n    lodash.unary = unary;\n    lodash.union = union;\n    lodash.unionBy = unionBy;\n    lodash.unionWith = unionWith;\n    lodash.uniq = uniq;\n    lodash.uniqBy = uniqBy;\n    lodash.uniqWith = uniqWith;\n    lodash.unset = unset;\n    lodash.unzip = unzip;\n    lodash.unzipWith = unzipWith;\n    lodash.update = update;\n    lodash.updateWith = updateWith;\n    lodash.values = values;\n    lodash.valuesIn = valuesIn;\n    lodash.without = without;\n    lodash.words = words;\n    lodash.wrap = wrap;\n    lodash.xor = xor;\n    lodash.xorBy = xorBy;\n    lodash.xorWith = xorWith;\n    lodash.zip = zip;\n    lodash.zipObject = zipObject;\n    lodash.zipObjectDeep = zipObjectDeep;\n    lodash.zipWith = zipWith;\n\n    // Add aliases.\n    lodash.entries = toPairs;\n    lodash.entriesIn = toPairsIn;\n    lodash.extend = assignIn;\n    lodash.extendWith = assignInWith;\n\n    // Add methods to `lodash.prototype`.\n    mixin(lodash, lodash);\n\n    /*------------------------------------------------------------------------*/\n\n    // Add methods that return unwrapped values in chain sequences.\n    lodash.add = add;\n    lodash.attempt = attempt;\n    lodash.camelCase = camelCase;\n    lodash.capitalize = capitalize;\n    lodash.ceil = ceil;\n    lodash.clamp = clamp;\n    lodash.clone = clone;\n    lodash.cloneDeep = cloneDeep;\n    lodash.cloneDeepWith = cloneDeepWith;\n    lodash.cloneWith = cloneWith;\n    lodash.conformsTo = conformsTo;\n    lodash.deburr = deburr;\n    lodash.defaultTo = defaultTo;\n    lodash.divide = divide;\n    lodash.endsWith = endsWith;\n    lodash.eq = eq;\n    lodash.escape = escape;\n    lodash.escapeRegExp = escapeRegExp;\n    lodash.every = every;\n    lodash.find = find;\n    lodash.findIndex = findIndex;\n    lodash.findKey = findKey;\n    lodash.findLast = findLast;\n    lodash.findLastIndex = findLastIndex;\n    lodash.findLastKey = findLastKey;\n    lodash.floor = floor;\n    lodash.forEach = forEach;\n    lodash.forEachRight = forEachRight;\n    lodash.forIn = forIn;\n    lodash.forInRight = forInRight;\n    lodash.forOwn = forOwn;\n    lodash.forOwnRight = forOwnRight;\n    lodash.get = get;\n    lodash.gt = gt;\n    lodash.gte = gte;\n    lodash.has = has;\n    lodash.hasIn = hasIn;\n    lodash.head = head;\n    lodash.identity = identity;\n    lodash.includes = includes;\n    lodash.indexOf = indexOf;\n    lodash.inRange = inRange;\n    lodash.invoke = invoke;\n    lodash.isArguments = isArguments;\n    lodash.isArray = isArray;\n    lodash.isArrayBuffer = isArrayBuffer;\n    lodash.isArrayLike = isArrayLike;\n    lodash.isArrayLikeObject = isArrayLikeObject;\n    lodash.isBoolean = isBoolean;\n    lodash.isBuffer = isBuffer;\n    lodash.isDate = isDate;\n    lodash.isElement = isElement;\n    lodash.isEmpty = isEmpty;\n    lodash.isEqual = isEqual;\n    lodash.isEqualWith = isEqualWith;\n    lodash.isError = isError;\n    lodash.isFinite = isFinite;\n    lodash.isFunction = isFunction;\n    lodash.isInteger = isInteger;\n    lodash.isLength = isLength;\n    lodash.isMap = isMap;\n    lodash.isMatch = isMatch;\n    lodash.isMatchWith = isMatchWith;\n    lodash.isNaN = isNaN;\n    lodash.isNative = isNative;\n    lodash.isNil = isNil;\n    lodash.isNull = isNull;\n    lodash.isNumber = isNumber;\n    lodash.isObject = isObject;\n    lodash.isObjectLike = isObjectLike;\n    lodash.isPlainObject = isPlainObject;\n    lodash.isRegExp = isRegExp;\n    lodash.isSafeInteger = isSafeInteger;\n    lodash.isSet = isSet;\n    lodash.isString = isString;\n    lodash.isSymbol = isSymbol;\n    lodash.isTypedArray = isTypedArray;\n    lodash.isUndefined = isUndefined;\n    lodash.isWeakMap = isWeakMap;\n    lodash.isWeakSet = isWeakSet;\n    lodash.join = join;\n    lodash.kebabCase = kebabCase;\n    lodash.last = last;\n    lodash.lastIndexOf = lastIndexOf;\n    lodash.lowerCase = lowerCase;\n    lodash.lowerFirst = lowerFirst;\n    lodash.lt = lt;\n    lodash.lte = lte;\n    lodash.max = max;\n    lodash.maxBy = maxBy;\n    lodash.mean = mean;\n    lodash.meanBy = meanBy;\n    lodash.min = min;\n    lodash.minBy = minBy;\n    lodash.stubArray = stubArray;\n    lodash.stubFalse = stubFalse;\n    lodash.stubObject = stubObject;\n    lodash.stubString = stubString;\n    lodash.stubTrue = stubTrue;\n    lodash.multiply = multiply;\n    lodash.nth = nth;\n    lodash.noConflict = noConflict;\n    lodash.noop = noop;\n    lodash.now = now;\n    lodash.pad = pad;\n    lodash.padEnd = padEnd;\n    lodash.padStart = padStart;\n    lodash.parseInt = parseInt;\n    lodash.random = random;\n    lodash.reduce = reduce;\n    lodash.reduceRight = reduceRight;\n    lodash.repeat = repeat;\n    lodash.replace = replace;\n    lodash.result = result;\n    lodash.round = round;\n    lodash.runInContext = runInContext;\n    lodash.sample = sample;\n    lodash.size = size;\n    lodash.snakeCase = snakeCase;\n    lodash.some = some;\n    lodash.sortedIndex = sortedIndex;\n    lodash.sortedIndexBy = sortedIndexBy;\n    lodash.sortedIndexOf = sortedIndexOf;\n    lodash.sortedLastIndex = sortedLastIndex;\n    lodash.sortedLastIndexBy = sortedLastIndexBy;\n    lodash.sortedLastIndexOf = sortedLastIndexOf;\n    lodash.startCase = startCase;\n    lodash.startsWith = startsWith;\n    lodash.subtract = subtract;\n    lodash.sum = sum;\n    lodash.sumBy = sumBy;\n    lodash.template = template;\n    lodash.times = times;\n    lodash.toFinite = toFinite;\n    lodash.toInteger = toInteger;\n    lodash.toLength = toLength;\n    lodash.toLower = toLower;\n    lodash.toNumber = toNumber;\n    lodash.toSafeInteger = toSafeInteger;\n    lodash.toString = toString;\n    lodash.toUpper = toUpper;\n    lodash.trim = trim;\n    lodash.trimEnd = trimEnd;\n    lodash.trimStart = trimStart;\n    lodash.truncate = truncate;\n    lodash.unescape = unescape;\n    lodash.uniqueId = uniqueId;\n    lodash.upperCase = upperCase;\n    lodash.upperFirst = upperFirst;\n\n    // Add aliases.\n    lodash.each = forEach;\n    lodash.eachRight = forEachRight;\n    lodash.first = head;\n\n    mixin(lodash, (function() {\n      var source = {};\n      baseForOwn(lodash, function(func, methodName) {\n        if (!hasOwnProperty.call(lodash.prototype, methodName)) {\n          source[methodName] = func;\n        }\n      });\n      return source;\n    }()), { 'chain': false });\n\n    /*------------------------------------------------------------------------*/\n\n    /**\n     * The semantic version number.\n     *\n     * @static\n     * @memberOf _\n     * @type {string}\n     */\n    lodash.VERSION = VERSION;\n\n    // Assign default placeholders.\n    arrayEach(['bind', 'bindKey', 'curry', 'curryRight', 'partial', 'partialRight'], function(methodName) {\n      lodash[methodName].placeholder = lodash;\n    });\n\n    // Add `LazyWrapper` methods for `_.drop` and `_.take` variants.\n    arrayEach(['drop', 'take'], function(methodName, index) {\n      LazyWrapper.prototype[methodName] = function(n) {\n        n = n === undefined ? 1 : nativeMax(toInteger(n), 0);\n\n        var result = (this.__filtered__ && !index)\n          ? new LazyWrapper(this)\n          : this.clone();\n\n        if (result.__filtered__) {\n          result.__takeCount__ = nativeMin(n, result.__takeCount__);\n        } else {\n          result.__views__.push({\n            'size': nativeMin(n, MAX_ARRAY_LENGTH),\n            'type': methodName + (result.__dir__ < 0 ? 'Right' : '')\n          });\n        }\n        return result;\n      };\n\n      LazyWrapper.prototype[methodName + 'Right'] = function(n) {\n        return this.reverse()[methodName](n).reverse();\n      };\n    });\n\n    // Add `LazyWrapper` methods that accept an `iteratee` value.\n    arrayEach(['filter', 'map', 'takeWhile'], function(methodName, index) {\n      var type = index + 1,\n          isFilter = type == LAZY_FILTER_FLAG || type == LAZY_WHILE_FLAG;\n\n      LazyWrapper.prototype[methodName] = function(iteratee) {\n        var result = this.clone();\n        result.__iteratees__.push({\n          'iteratee': getIteratee(iteratee, 3),\n          'type': type\n        });\n        result.__filtered__ = result.__filtered__ || isFilter;\n        return result;\n      };\n    });\n\n    // Add `LazyWrapper` methods for `_.head` and `_.last`.\n    arrayEach(['head', 'last'], function(methodName, index) {\n      var takeName = 'take' + (index ? 'Right' : '');\n\n      LazyWrapper.prototype[methodName] = function() {\n        return this[takeName](1).value()[0];\n      };\n    });\n\n    // Add `LazyWrapper` methods for `_.initial` and `_.tail`.\n    arrayEach(['initial', 'tail'], function(methodName, index) {\n      var dropName = 'drop' + (index ? '' : 'Right');\n\n      LazyWrapper.prototype[methodName] = function() {\n        return this.__filtered__ ? new LazyWrapper(this) : this[dropName](1);\n      };\n    });\n\n    LazyWrapper.prototype.compact = function() {\n      return this.filter(identity);\n    };\n\n    LazyWrapper.prototype.find = function(predicate) {\n      return this.filter(predicate).head();\n    };\n\n    LazyWrapper.prototype.findLast = function(predicate) {\n      return this.reverse().find(predicate);\n    };\n\n    LazyWrapper.prototype.invokeMap = baseRest(function(path, args) {\n      if (typeof path == 'function') {\n        return new LazyWrapper(this);\n      }\n      return this.map(function(value) {\n        return baseInvoke(value, path, args);\n      });\n    });\n\n    LazyWrapper.prototype.reject = function(predicate) {\n      return this.filter(negate(getIteratee(predicate)));\n    };\n\n    LazyWrapper.prototype.slice = function(start, end) {\n      start = toInteger(start);\n\n      var result = this;\n      if (result.__filtered__ && (start > 0 || end < 0)) {\n        return new LazyWrapper(result);\n      }\n      if (start < 0) {\n        result = result.takeRight(-start);\n      } else if (start) {\n        result = result.drop(start);\n      }\n      if (end !== undefined) {\n        end = toInteger(end);\n        result = end < 0 ? result.dropRight(-end) : result.take(end - start);\n      }\n      return result;\n    };\n\n    LazyWrapper.prototype.takeRightWhile = function(predicate) {\n      return this.reverse().takeWhile(predicate).reverse();\n    };\n\n    LazyWrapper.prototype.toArray = function() {\n      return this.take(MAX_ARRAY_LENGTH);\n    };\n\n    // Add `LazyWrapper` methods to `lodash.prototype`.\n    baseForOwn(LazyWrapper.prototype, function(func, methodName) {\n      var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),\n          isTaker = /^(?:head|last)$/.test(methodName),\n          lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],\n          retUnwrapped = isTaker || /^find/.test(methodName);\n\n      if (!lodashFunc) {\n        return;\n      }\n      lodash.prototype[methodName] = function() {\n        var value = this.__wrapped__,\n            args = isTaker ? [1] : arguments,\n            isLazy = value instanceof LazyWrapper,\n            iteratee = args[0],\n            useLazy = isLazy || isArray(value);\n\n        var interceptor = function(value) {\n          var result = lodashFunc.apply(lodash, arrayPush([value], args));\n          return (isTaker && chainAll) ? result[0] : result;\n        };\n\n        if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {\n          // Avoid lazy use if the iteratee has a \"length\" value other than `1`.\n          isLazy = useLazy = false;\n        }\n        var chainAll = this.__chain__,\n            isHybrid = !!this.__actions__.length,\n            isUnwrapped = retUnwrapped && !chainAll,\n            onlyLazy = isLazy && !isHybrid;\n\n        if (!retUnwrapped && useLazy) {\n          value = onlyLazy ? value : new LazyWrapper(this);\n          var result = func.apply(value, args);\n          result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });\n          return new LodashWrapper(result, chainAll);\n        }\n        if (isUnwrapped && onlyLazy) {\n          return func.apply(this, args);\n        }\n        result = this.thru(interceptor);\n        return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;\n      };\n    });\n\n    // Add `Array` methods to `lodash.prototype`.\n    arrayEach(['pop', 'push', 'shift', 'sort', 'splice', 'unshift'], function(methodName) {\n      var func = arrayProto[methodName],\n          chainName = /^(?:push|sort|unshift)$/.test(methodName) ? 'tap' : 'thru',\n          retUnwrapped = /^(?:pop|shift)$/.test(methodName);\n\n      lodash.prototype[methodName] = function() {\n        var args = arguments;\n        if (retUnwrapped && !this.__chain__) {\n          var value = this.value();\n          return func.apply(isArray(value) ? value : [], args);\n        }\n        return this[chainName](function(value) {\n          return func.apply(isArray(value) ? value : [], args);\n        });\n      };\n    });\n\n    // Map minified method names to their real names.\n    baseForOwn(LazyWrapper.prototype, function(func, methodName) {\n      var lodashFunc = lodash[methodName];\n      if (lodashFunc) {\n        var key = lodashFunc.name + '';\n        if (!hasOwnProperty.call(realNames, key)) {\n          realNames[key] = [];\n        }\n        realNames[key].push({ 'name': methodName, 'func': lodashFunc });\n      }\n    });\n\n    realNames[createHybrid(undefined, WRAP_BIND_KEY_FLAG).name] = [{\n      'name': 'wrapper',\n      'func': undefined\n    }];\n\n    // Add methods to `LazyWrapper`.\n    LazyWrapper.prototype.clone = lazyClone;\n    LazyWrapper.prototype.reverse = lazyReverse;\n    LazyWrapper.prototype.value = lazyValue;\n\n    // Add chain sequence methods to the `lodash` wrapper.\n    lodash.prototype.at = wrapperAt;\n    lodash.prototype.chain = wrapperChain;\n    lodash.prototype.commit = wrapperCommit;\n    lodash.prototype.next = wrapperNext;\n    lodash.prototype.plant = wrapperPlant;\n    lodash.prototype.reverse = wrapperReverse;\n    lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;\n\n    // Add lazy aliases.\n    lodash.prototype.first = lodash.prototype.head;\n\n    if (symIterator) {\n      lodash.prototype[symIterator] = wrapperToIterator;\n    }\n    return lodash;\n  });\n\n  /*--------------------------------------------------------------------------*/\n\n  // Export lodash.\n  var _ = runInContext();\n\n  // Some AMD build optimizers, like r.js, check for condition patterns like:\n  if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {\n    // Expose Lodash on the global object to prevent errors when Lodash is\n    // loaded by a script tag in the presence of an AMD loader.\n    // See http://requirejs.org/docs/errors.html#mismatch for more details.\n    // Use `_.noConflict` to remove Lodash from the global object.\n    root._ = _;\n\n    // Define as an anonymous module so, through path mapping, it can be\n    // referenced as the \"underscore\" module.\n    define(function() {\n      return _;\n    });\n  }\n  // Check for `exports` after `define` in case a build optimizer adds it.\n  else if (freeModule) {\n    // Export for Node.js.\n    (freeModule.exports = _)._ = _;\n    // Export for CommonJS support.\n    freeExports._ = _;\n  }\n  else {\n    // Export to the global object.\n    root._ = _;\n  }\n}.call(this));\n\n}).call(this,typeof global !== \"undefined\" ? global : typeof self !== \"undefined\" ? self : typeof window !== \"undefined\" ? window : {})\n},{}],13:[function(require,module,exports){\n// shim for using process in browser\n\nvar process = module.exports = {};\nvar queue = [];\nvar draining = false;\n\nfunction drainQueue() {\n    if (draining) {\n        return;\n    }\n    draining = true;\n    var currentQueue;\n    var len = queue.length;\n    while(len) {\n        currentQueue = queue;\n        queue = [];\n        var i = -1;\n        while (++i < len) {\n            currentQueue[i]();\n        }\n        len = queue.length;\n    }\n    draining = false;\n}\nprocess.nextTick = function (fun) {\n    queue.push(fun);\n    if (!draining) {\n        setTimeout(drainQueue, 0);\n    }\n};\n\nprocess.title = 'browser';\nprocess.browser = true;\nprocess.env = {};\nprocess.argv = [];\nprocess.version = ''; // empty string to avoid regexp issues\nprocess.versions = {};\n\nfunction noop() {}\n\nprocess.on = noop;\nprocess.addListener = noop;\nprocess.once = noop;\nprocess.off = noop;\nprocess.removeListener = noop;\nprocess.removeAllListeners = noop;\nprocess.emit = noop;\n\nprocess.binding = function (name) {\n    throw new Error('process.binding is not supported');\n};\n\n// TODO(shtylman)\nprocess.cwd = function () { return '/' };\nprocess.chdir = function (dir) {\n    throw new Error('process.chdir is not supported');\n};\nprocess.umask = function() { return 0; };\n\n},{}],14:[function(require,module,exports){\narguments[4][6][0].apply(exports,arguments)\n},{\"dup\":6}],15:[function(require,module,exports){\narguments[4][7][0].apply(exports,arguments)\n},{\"dup\":7}],16:[function(require,module,exports){\narguments[4][8][0].apply(exports,arguments)\n},{\"./support/isBuffer\":15,\"_process\":13,\"dup\":8,\"inherits\":14}],17:[function(require,module,exports){\n/*\n * Lexical analysis and token construction.\n */\n\n\"use strict\";\n\nvar _      = require(\"lodash\");\nvar events = require(\"events\");\nvar reg    = require(\"./reg.js\");\nvar state  = require(\"./state.js\").state;\n\nvar unicodeData = require(\"../data/ascii-identifier-data.js\");\nvar asciiIdentifierStartTable = unicodeData.asciiIdentifierStartTable;\nvar asciiIdentifierPartTable = unicodeData.asciiIdentifierPartTable;\nvar nonAsciiIdentifierStartTable = require(\"../data/non-ascii-identifier-start.js\");\nvar nonAsciiIdentifierPartTable = require(\"../data/non-ascii-identifier-part-only.js\");\n// Loading of this module is deferred as an optimization for ES2015 input\nvar es5IdentifierNames;\n\n// Some of these token types are from JavaScript Parser API\n// while others are specific to JSHint parser.\n// JS Parser API: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API\n\nvar Token = {\n  Identifier: 1,\n  Punctuator: 2,\n  NumericLiteral: 3,\n  StringLiteral: 4,\n  Comment: 5,\n  Keyword: 6,\n  RegExp: 9,\n  TemplateHead: 10,\n  TemplateMiddle: 11,\n  TemplateTail: 12,\n  NoSubstTemplate: 13\n};\n\nvar Context = {\n  Block: 1,\n  Template: 2\n};\n\nfunction isHex(str) {\n  return /^[0-9a-fA-F]+$/.test(str);\n}\n\nfunction isHexDigit(str) {\n  return str.length === 1 && isHex(str);\n}\n\n// Object that handles postponed lexing verifications that checks the parsed\n// environment state.\n\nfunction asyncTrigger() {\n  var _checks = [];\n\n  return {\n    push: function(fn) {\n      _checks.push(fn);\n    },\n\n    check: function() {\n      for (var check = 0; check < _checks.length; ++check) {\n        _checks[check]();\n      }\n\n      _checks.splice(0, _checks.length);\n    }\n  };\n}\n\n/*\n * Lexer for JSHint.\n *\n * This object does a char-by-char scan of the provided source code\n * and produces a sequence of tokens.\n *\n *   var lex = new Lexer(\"var i = 0;\");\n *   lex.start();\n *   lex.token(); // returns the next token\n *\n * You have to use the token() method to move the lexer forward\n * but you don't have to use its return value to get tokens. In addition\n * to token() method returning the next token, the Lexer object also\n * emits events.\n *\n *   lex.on(\"Identifier\", function(data) {\n *     if (data.name.indexOf(\"_\") >= 0) {\n *       // Produce a warning.\n *     }\n *   });\n *\n * Note that the token() method returns tokens in a JSLint-compatible\n * format while the event emitter uses a slightly modified version of\n * Mozilla's JavaScript Parser API. Eventually, we will move away from\n * JSLint format.\n */\nfunction Lexer(source) {\n  var lines = source;\n\n  if (typeof lines === \"string\") {\n    lines = lines\n      .replace(/\\r\\n/g, \"\\n\")\n      .replace(/\\r/g, \"\\n\")\n      .split(\"\\n\");\n  }\n\n  // If the first line is a shebang (#!), make it a blank and move on.\n  // Shebangs are used by Node scripts.\n\n  if (lines[0] && lines[0].substr(0, 2) === \"#!\") {\n    if (lines[0].indexOf(\"node\") !== -1) {\n      state.option.node = true;\n    }\n    lines[0] = \"\";\n  }\n\n  this.emitter = new events.EventEmitter();\n  this.source = source;\n  this.setLines(lines);\n  this.prereg = true;\n\n  this.line = 0;\n  this.char = 1;\n  this.from = 1;\n  this.input = \"\";\n  this.inComment = false;\n  this.context = [];\n  this.templateStarts = [];\n\n  for (var i = 0; i < state.option.indent; i += 1) {\n    state.tab += \" \";\n  }\n}\n\nLexer.prototype = {\n  _lines: [],\n\n  inContext: function(ctxType) {\n    return this.context.length > 0 && this.context[this.context.length - 1].type === ctxType;\n  },\n\n  pushContext: function(ctxType) {\n    this.context.push({ type: ctxType });\n  },\n\n  popContext: function() {\n    return this.context.pop();\n  },\n\n  currentContext: function() {\n    return this.context.length > 0 && this.context[this.context.length - 1];\n  },\n\n  getLines: function() {\n    this._lines = state.lines;\n    return this._lines;\n  },\n\n  setLines: function(val) {\n    this._lines = val;\n    state.lines = this._lines;\n  },\n\n  /*\n   * Return the next i character without actually moving the\n   * char pointer.\n   */\n  peek: function(i) {\n    return this.input.charAt(i || 0);\n  },\n\n  /*\n   * Move the char pointer forward i times.\n   */\n  skip: function(i) {\n    i = i || 1;\n    this.char += i;\n    this.input = this.input.slice(i);\n  },\n\n  /*\n   * Subscribe to a token event. The API for this method is similar\n   * Underscore.js i.e. you can subscribe to multiple events with\n   * one call:\n   *\n   *   lex.on(\"Identifier Number\", function(data) {\n   *     // ...\n   *   });\n   */\n  on: function(names, listener) {\n    names.split(\" \").forEach(function(name) {\n      this.emitter.on(name, listener);\n    }.bind(this));\n  },\n\n  /*\n   * Trigger a token event. All arguments will be passed to each\n   * listener.\n   */\n  trigger: function() {\n    this.emitter.emit.apply(this.emitter, Array.prototype.slice.call(arguments));\n  },\n\n  /*\n   * Postpone a token event. the checking condition is set as\n   * last parameter, and the trigger function is called in a\n   * stored callback. To be later called using the check() function\n   * by the parser. This avoids parser's peek() to give the lexer\n   * a false context.\n   */\n  triggerAsync: function(type, args, checks, fn) {\n    checks.push(function() {\n      if (fn()) {\n        this.trigger(type, args);\n      }\n    }.bind(this));\n  },\n\n  /*\n   * Extract a punctuator out of the next sequence of characters\n   * or return 'null' if its not possible.\n   *\n   * This method's implementation was heavily influenced by the\n   * scanPunctuator function in the Esprima parser's source code.\n   */\n  scanPunctuator: function() {\n    var ch1 = this.peek();\n    var ch2, ch3, ch4;\n\n    switch (ch1) {\n    // Most common single-character punctuators\n    case \".\":\n      if ((/^[0-9]$/).test(this.peek(1))) {\n        return null;\n      }\n      if (this.peek(1) === \".\" && this.peek(2) === \".\") {\n        return {\n          type: Token.Punctuator,\n          value: \"...\"\n        };\n      }\n      /* falls through */\n    case \"(\":\n    case \")\":\n    case \";\":\n    case \",\":\n    case \"[\":\n    case \"]\":\n    case \":\":\n    case \"~\":\n      return {\n        type: Token.Punctuator,\n        value: ch1\n      };\n\n    // A block/object opener\n    case \"{\":\n      this.pushContext(Context.Block);\n      return {\n        type: Token.Punctuator,\n        value: ch1\n      };\n\n    // A block/object closer\n    case \"}\":\n      if (this.inContext(Context.Block)) {\n        this.popContext();\n      }\n      return {\n        type: Token.Punctuator,\n        value: ch1\n      };\n\n    // A pound sign (for Node shebangs)\n    case \"#\":\n      return {\n        type: Token.Punctuator,\n        value: ch1\n      };\n\n    // We're at the end of input\n    case \"\":\n      return null;\n    }\n\n    // Peek more characters\n\n    ch2 = this.peek(1);\n    ch3 = this.peek(2);\n\n    if (ch1 === \"?\") {\n      // Optional chaining\n      if (ch2 === \".\" && !reg.decimalDigit.test(ch3)) {\n        return {\n          type: Token.Punctuator,\n          value: \"?.\"\n        };\n      }\n\n      return {\n        type: Token.Punctuator,\n        value: ch2 === \"?\" ? \"??\" : \"?\"\n      };\n    }\n\n    ch4 = this.peek(3);\n\n    // 4-character punctuator: >>>=\n\n    if (ch1 === \">\" && ch2 === \">\" && ch3 === \">\" && ch4 === \"=\") {\n      return {\n        type: Token.Punctuator,\n        value: \">>>=\"\n      };\n    }\n\n    // 3-character punctuators: === !== >>> <<= >>=\n\n    if (ch1 === \"=\" && ch2 === \"=\" && ch3 === \"=\") {\n      return {\n        type: Token.Punctuator,\n        value: \"===\"\n      };\n    }\n\n    if (ch1 === \"!\" && ch2 === \"=\" && ch3 === \"=\") {\n      return {\n        type: Token.Punctuator,\n        value: \"!==\"\n      };\n    }\n\n    if (ch1 === \">\" && ch2 === \">\" && ch3 === \">\") {\n      return {\n        type: Token.Punctuator,\n        value: \">>>\"\n      };\n    }\n\n    if (ch1 === \"<\" && ch2 === \"<\" && ch3 === \"=\") {\n      return {\n        type: Token.Punctuator,\n        value: \"<<=\"\n      };\n    }\n\n    if (ch1 === \">\" && ch2 === \">\" && ch3 === \"=\") {\n      return {\n        type: Token.Punctuator,\n        value: \">>=\"\n      };\n    }\n\n    // Fat arrow punctuator\n    if (ch1 === \"=\" && ch2 === \">\") {\n      return {\n        type: Token.Punctuator,\n        value: ch1 + ch2\n      };\n    }\n\n    // 2-character punctuators: ++ -- << >> && || **\n    if (ch1 === ch2 && (\"+-<>&|*\".indexOf(ch1) >= 0)) {\n      if (ch1 === \"*\" && ch3 === \"=\") {\n        return {\n          type: Token.Punctuator,\n          value: ch1 + ch2 + ch3\n        };\n      }\n\n      return {\n        type: Token.Punctuator,\n        value: ch1 + ch2\n      };\n    }\n\n    // <= >= != += -= *= %= &= |= ^= /=\n    if (\"<>=!+-*%&|^/\".indexOf(ch1) >= 0) {\n      if (ch2 === \"=\") {\n        return {\n          type: Token.Punctuator,\n          value: ch1 + ch2\n        };\n      }\n\n      return {\n        type: Token.Punctuator,\n        value: ch1\n      };\n    }\n\n    return null;\n  },\n\n  /*\n   * Extract a comment out of the next sequence of characters and/or\n   * lines or return 'null' if its not possible. Since comments can\n   * span across multiple lines this method has to move the char\n   * pointer.\n   *\n   * In addition to normal JavaScript comments (// and /*) this method\n   * also recognizes JSHint- and JSLint-specific comments such as\n   * /*jshint, /*jslint, /*globals and so on.\n   */\n  scanComments: function(checks) {\n    var ch1 = this.peek();\n    var ch2 = this.peek(1);\n    var rest = this.input.substr(2);\n    var startLine = this.line;\n    var startChar = this.char;\n    var self = this;\n\n    // Create a comment token object and make sure it\n    // has all the data JSHint needs to work with special\n    // comments.\n\n    function commentToken(label, body, opt) {\n      var special = [\n        \"jshint\", \"jshint.unstable\", \"jslint\", \"members\", \"member\", \"globals\",\n        \"global\", \"exported\"\n      ];\n      var isSpecial = false;\n      var value = label + body;\n      var commentType = \"plain\";\n      opt = opt || {};\n\n      if (opt.isMultiline) {\n        value += \"*/\";\n      }\n\n      body = body.replace(/\\n/g, \" \");\n\n      if (label === \"/*\" && reg.fallsThrough.test(body)) {\n        isSpecial = true;\n        commentType = \"falls through\";\n      }\n\n      special.forEach(function(str) {\n        if (isSpecial) {\n          return;\n        }\n\n        // Don't recognize any special comments other than jshint for single-line\n        // comments. This introduced many problems with legit comments.\n        if (label === \"//\" && str !== \"jshint\" && str !== \"jshint.unstable\") {\n          return;\n        }\n\n        if (body.charAt(str.length) === \" \" && body.substr(0, str.length) === str) {\n          isSpecial = true;\n          label = label + str;\n          body = body.substr(str.length);\n        }\n\n        if (!isSpecial && body.charAt(0) === \" \" && body.charAt(str.length + 1) === \" \" &&\n          body.substr(1, str.length) === str) {\n          isSpecial = true;\n          label = label + \" \" + str;\n          body = body.substr(str.length + 1);\n        }\n\n        // To handle rarer case when special word is separated from label by\n        // multiple spaces or tabs\n        var strIndex = body.indexOf(str);\n        if (!isSpecial && strIndex >= 0 && body.charAt(strIndex + str.length) === \" \") {\n          var isAllWhitespace = body.substr(0, strIndex).trim().length === 0;\n          if (isAllWhitespace) {\n            isSpecial = true;\n            body = body.substr(str.length + strIndex);\n          }\n        }\n\n        if (!isSpecial) {\n          return;\n        }\n\n        switch (str) {\n        case \"member\":\n          commentType = \"members\";\n          break;\n        case \"global\":\n          commentType = \"globals\";\n          break;\n        default:\n          var options = body.split(\":\").map(function(v) {\n            return v.replace(/^\\s+/, \"\").replace(/\\s+$/, \"\");\n          });\n\n          if (options.length === 2) {\n            switch (options[0]) {\n            case \"ignore\":\n              switch (options[1]) {\n              case \"start\":\n                self.ignoringLinterErrors = true;\n                isSpecial = false;\n                break;\n              case \"end\":\n                self.ignoringLinterErrors = false;\n                isSpecial = false;\n                break;\n              }\n            }\n          }\n\n          commentType = str;\n        }\n      });\n\n      return {\n        type: Token.Comment,\n        commentType: commentType,\n        value: value,\n        body: body,\n        isSpecial: isSpecial,\n        isMalformed: opt.isMalformed || false\n      };\n    }\n\n    // End of unbegun comment. Raise an error and skip that input.\n    if (ch1 === \"*\" && ch2 === \"/\") {\n      this.trigger(\"error\", {\n        code: \"E018\",\n        line: startLine,\n        character: startChar\n      });\n\n      this.skip(2);\n      return null;\n    }\n\n    // Comments must start either with // or /*\n    if (ch1 !== \"/\" || (ch2 !== \"*\" && ch2 !== \"/\")) {\n      return null;\n    }\n\n    // One-line comment\n    if (ch2 === \"/\") {\n      this.skip(this.input.length); // Skip to the EOL.\n      return commentToken(\"//\", rest);\n    }\n\n    var body = \"\";\n\n    /* Multi-line comment */\n    if (ch2 === \"*\") {\n      this.inComment = true;\n      this.skip(2);\n\n      while (this.peek() !== \"*\" || this.peek(1) !== \"/\") {\n        if (this.peek() === \"\") { // End of Line\n          body += \"\\n\";\n\n          // If we hit EOF and our comment is still unclosed,\n          // trigger an error and end the comment implicitly.\n          if (!this.nextLine(checks)) {\n            this.trigger(\"error\", {\n              code: \"E017\",\n              line: startLine,\n              character: startChar\n            });\n\n            this.inComment = false;\n            return commentToken(\"/*\", body, {\n              isMultiline: true,\n              isMalformed: true\n            });\n          }\n        } else {\n          body += this.peek();\n          this.skip();\n        }\n      }\n\n      this.skip(2);\n      this.inComment = false;\n      return commentToken(\"/*\", body, { isMultiline: true });\n    }\n  },\n\n  /*\n   * Extract a keyword out of the next sequence of characters or\n   * return 'null' if its not possible.\n   */\n  scanKeyword: function() {\n    var result = /^[a-zA-Z_$][a-zA-Z0-9_$]*/.exec(this.input);\n    var keywords = [\n      \"if\", \"in\", \"do\", \"var\", \"for\", \"new\",\n      \"try\", \"let\", \"this\", \"else\", \"case\",\n      \"void\", \"with\", \"enum\", \"while\", \"break\",\n      \"catch\", \"throw\", \"const\", \"yield\", \"class\",\n      \"super\", \"return\", \"typeof\", \"delete\",\n      \"switch\", \"export\", \"import\", \"default\",\n      \"finally\", \"extends\", \"function\", \"continue\",\n      \"debugger\", \"instanceof\", \"true\", \"false\", \"null\", \"async\", \"await\"\n    ];\n\n    if (result && keywords.indexOf(result[0]) >= 0) {\n      return {\n        type: Token.Keyword,\n        value: result[0]\n      };\n    }\n\n    return null;\n  },\n\n  /*\n   * Extract a JavaScript identifier out of the next sequence of\n   * characters or return 'null' if its not possible.\n   */\n  scanIdentifier: function(checks) {\n    var id = \"\";\n    var index = 0;\n    var char, value;\n\n    function isNonAsciiIdentifierStart(code) {\n      return nonAsciiIdentifierStartTable.indexOf(code) > -1;\n    }\n\n    function isNonAsciiIdentifierPart(code) {\n      return isNonAsciiIdentifierStart(code) || nonAsciiIdentifierPartTable.indexOf(code) > -1;\n    }\n\n    var readUnicodeEscapeSequence = function() {\n      /*jshint validthis:true */\n      index += 1;\n\n      if (this.peek(index) !== \"u\") {\n        return null;\n      }\n\n      var sequence = this.peek(index + 1) + this.peek(index + 2) +\n        this.peek(index + 3) + this.peek(index + 4);\n      var code;\n\n      if (isHex(sequence)) {\n        code = parseInt(sequence, 16);\n\n        if (asciiIdentifierPartTable[code] || isNonAsciiIdentifierPart(code)) {\n          index += 5;\n          return \"\\\\u\" + sequence;\n        }\n\n        return null;\n      }\n\n      return null;\n    }.bind(this);\n\n    var getIdentifierStart = function() {\n      /*jshint validthis:true */\n      var chr = this.peek(index);\n      var code = chr.charCodeAt(0);\n\n      if (code === 92) {\n        return readUnicodeEscapeSequence();\n      }\n\n      if (code < 128) {\n        if (asciiIdentifierStartTable[code]) {\n          index += 1;\n          return chr;\n        }\n\n        return null;\n      }\n\n      if (isNonAsciiIdentifierStart(code)) {\n        index += 1;\n        return chr;\n      }\n\n      return null;\n    }.bind(this);\n\n    var getIdentifierPart = function() {\n      /*jshint validthis:true */\n      var chr = this.peek(index);\n      var code = chr.charCodeAt(0);\n\n      if (code === 92) {\n        return readUnicodeEscapeSequence();\n      }\n\n      if (code < 128) {\n        if (asciiIdentifierPartTable[code]) {\n          index += 1;\n          return chr;\n        }\n\n        return null;\n      }\n\n      if (isNonAsciiIdentifierPart(code)) {\n        index += 1;\n        return chr;\n      }\n\n      return null;\n    }.bind(this);\n\n    function removeEscapeSequences(id) {\n      return id.replace(/\\\\u([0-9a-fA-F]{4})/g, function(m0, codepoint) {\n        return String.fromCharCode(parseInt(codepoint, 16));\n      });\n    }\n\n    char = getIdentifierStart();\n    if (char === null) {\n      return null;\n    }\n\n    id = char;\n    for (;;) {\n      char = getIdentifierPart();\n\n      if (char === null) {\n        break;\n      }\n\n      id += char;\n    }\n\n    value = removeEscapeSequences(id);\n\n    if (!state.inES6(true)) {\n      es5IdentifierNames = require(\"../data/es5-identifier-names.js\");\n\n      if (!es5IdentifierNames.test(value)) {\n        this.triggerAsync(\n          \"warning\",\n          {\n            code: \"W119\",\n            line: this.line,\n            character: this.char,\n            data: [\"unicode 8\", \"6\"]\n          },\n          checks,\n          function() { return true; }\n        );\n      }\n    }\n\n    return {\n      type: Token.Identifier,\n      value: value,\n      text: id,\n      tokenLength: id.length\n    };\n  },\n\n  /*\n   * Extract a numeric literal out of the next sequence of\n   * characters or return 'null' if its not possible. This method\n   * supports all numeric literals described in section 7.8.3\n   * of the EcmaScript 5 specification.\n   *\n   * This method's implementation was heavily influenced by the\n   * scanNumericLiteral function in the Esprima parser's source code.\n   */\n  scanNumericLiteral: function(checks) {\n    var index = 0;\n    var value = \"\";\n    var length = this.input.length;\n    var char = this.peek(index);\n    var isAllowedDigit = isDecimalDigit;\n    var base = 10;\n    var isLegacy = false;\n    var isNonOctal = false;\n\n    function isDecimalDigit(str) {\n      return (/^[0-9]$/).test(str);\n    }\n\n    function isOctalDigit(str) {\n      return (/^[0-7]$/).test(str);\n    }\n\n    function isNonOctalDigit(str) {\n      return str === \"8\" || str === \"9\";\n    }\n\n    function isBinaryDigit(str) {\n      return (/^[01]$/).test(str);\n    }\n\n    function isIdentifierStart(ch) {\n      return (ch === \"$\") || (ch === \"_\") || (ch === \"\\\\\") ||\n        (ch >= \"a\" && ch <= \"z\") || (ch >= \"A\" && ch <= \"Z\");\n    }\n\n    // Numbers must start either with a decimal digit or a point.\n\n    if (char !== \".\" && !isDecimalDigit(char)) {\n      return null;\n    }\n\n    if (char !== \".\") {\n      value = this.peek(index);\n      index += 1;\n      char = this.peek(index);\n\n      if (value === \"0\") {\n        // Base-16 numbers.\n        if (char === \"x\" || char === \"X\") {\n          isAllowedDigit = isHexDigit;\n          base = 16;\n\n          index += 1;\n          value += char;\n        }\n\n        // Base-8 numbers.\n        if (char === \"o\" || char === \"O\") {\n          isAllowedDigit = isOctalDigit;\n          base = 8;\n\n          if (!state.inES6(true)) {\n            this.triggerAsync(\n              \"warning\",\n              {\n                code: \"W119\",\n                line: this.line,\n                character: this.char,\n                data: [ \"Octal integer literal\", \"6\" ]\n              },\n              checks,\n              function() { return true; }\n            );\n          }\n\n          index += 1;\n          value += char;\n        }\n\n        // Base-2 numbers.\n        if (char === \"b\" || char === \"B\") {\n          isAllowedDigit = isBinaryDigit;\n          base = 2;\n\n          if (!state.inES6(true)) {\n            this.triggerAsync(\n              \"warning\",\n              {\n                code: \"W119\",\n                line: this.line,\n                character: this.char,\n                data: [ \"Binary integer literal\", \"6\" ]\n              },\n              checks,\n              function() { return true; }\n            );\n          }\n\n          index += 1;\n          value += char;\n        }\n\n        // Legacy base-8 numbers.\n        if (isOctalDigit(char)) {\n          isAllowedDigit = isOctalDigit;\n          base = 8;\n          isLegacy = true;\n\n        } else if (isDecimalDigit(char)) {\n          isNonOctal = true;\n        }\n      }\n\n      while (index < length) {\n        char = this.peek(index);\n\n        if (isLegacy && isNonOctalDigit(char)) {\n          base = 10;\n          isLegacy = false;\n          isNonOctal = true;\n          isAllowedDigit = isDecimalDigit;\n        }\n\n        if (!isAllowedDigit(char)) {\n          break;\n        }\n        value += char;\n        index += 1;\n      }\n\n      var isBigInt = this.peek(index) === 'n';\n\n      if (isAllowedDigit !== isDecimalDigit || isBigInt) {\n        if (isBigInt) {\n          this.triggerAsync(\n            \"warning\",\n            {\n              code: \"W119\",\n              line: this.line,\n              character: this.char,\n              data: [ \"BigInt\", \"11\" ]\n            },\n            checks,\n            function() { return !state.inES11(); }\n          );\n\n          if (isLegacy || isNonOctal) {\n            this.triggerAsync(\n              \"error\",\n              {\n                code: \"E067\",\n                line: this.line,\n                character: this.char,\n                data: [value + char]\n              },\n              checks,\n              function() { return true; }\n            );\n          }\n\n          value += char;\n          index += 1;\n        } else if (!isLegacy && value.length <= 2) { // 0x\n          return {\n            type: Token.NumericLiteral,\n            value: value,\n            isMalformed: true\n          };\n        }\n\n        if (index < length) {\n          char = this.peek(index);\n          if (isIdentifierStart(char)) {\n            return null;\n          }\n        }\n\n        return {\n          type: Token.NumericLiteral,\n          value: value,\n          base: base,\n          isLegacy: isLegacy,\n          isMalformed: false\n        };\n      }\n    }\n\n    // Decimal digits.\n\n    if (char === \".\") {\n      value += char;\n      index += 1;\n\n      while (index < length) {\n        char = this.peek(index);\n        if (!isDecimalDigit(char)) {\n          break;\n        }\n        value += char;\n        index += 1;\n      }\n    }\n\n    // Exponent part.\n\n    if (char === \"e\" || char === \"E\") {\n      value += char;\n      index += 1;\n      char = this.peek(index);\n\n      if (char === \"+\" || char === \"-\") {\n        value += this.peek(index);\n        index += 1;\n      }\n\n      char = this.peek(index);\n      if (isDecimalDigit(char)) {\n        value += char;\n        index += 1;\n\n        while (index < length) {\n          char = this.peek(index);\n          if (!isDecimalDigit(char)) {\n            break;\n          }\n          value += char;\n          index += 1;\n        }\n      } else {\n        return null;\n      }\n    }\n\n    if (index < length) {\n      char = this.peek(index);\n      if (isIdentifierStart(char)) {\n        return null;\n      }\n    }\n\n    // TODO: Extend this check to other numeric literals\n    this.triggerAsync(\"warning\", {\n      code: \"W045\",\n      line: this.line,\n      character: this.char + value.length,\n      data: [ value ]\n    }, checks, function() { return !isFinite(value); });\n\n    return {\n      type: Token.NumericLiteral,\n      value: value,\n      base: base,\n      isNonOctal: isNonOctal,\n      isMalformed: false\n    };\n  },\n\n\n  // Assumes previously parsed character was \\ (=== '\\\\') and was not skipped.\n  scanEscapeSequence: function(checks) {\n    var allowNewLine = false;\n    var jump = 1;\n    this.skip();\n    var char = this.peek();\n\n    switch (char) {\n    case \"'\":\n      this.triggerAsync(\"warning\", {\n        code: \"W114\",\n        line: this.line,\n        character: this.char,\n        data: [ \"\\\\'\" ]\n      }, checks, function() {return state.jsonMode; });\n      break;\n    case \"b\":\n      char = \"\\\\b\";\n      break;\n    case \"f\":\n      char = \"\\\\f\";\n      break;\n    case \"n\":\n      char = \"\\\\n\";\n      break;\n    case \"r\":\n      char = \"\\\\r\";\n      break;\n    case \"t\":\n      char = \"\\\\t\";\n      break;\n    case \"0\":\n      char = \"\\\\0\";\n\n      // Octal literals fail in strict mode.\n      // Check if the number is between 00 and 07.\n      var n = parseInt(this.peek(1), 10);\n      this.triggerAsync(\"warning\", {\n        code: \"W115\",\n        line: this.line,\n        character: this.char\n      }, checks,\n      function() { return n >= 0 && n <= 7 && state.isStrict(); });\n      break;\n    case \"1\":\n    case \"2\":\n    case \"3\":\n    case \"4\":\n    case \"5\":\n    case \"6\":\n    case \"7\":\n      char = \"\\\\\" + char;\n      this.triggerAsync(\"warning\", {\n        code: \"W115\",\n        line: this.line,\n        character: this.char\n      }, checks,\n      function() { return state.isStrict(); });\n      break;\n    case \"u\":\n      var sequence = this.input.substr(1, 4);\n      var code = parseInt(sequence, 16);\n      if (!isHex(sequence)) {\n        // This condition unequivocally describes a syntax error.\n        // TODO: Re-factor as an \"error\" (not a \"warning\").\n        this.trigger(\"warning\", {\n          code: \"W052\",\n          line: this.line,\n          character: this.char,\n          data: [ \"u\" + sequence ]\n        });\n      }\n      char = String.fromCharCode(code);\n      jump = 5;\n      break;\n    case \"v\":\n      this.triggerAsync(\"warning\", {\n        code: \"W114\",\n        line: this.line,\n        character: this.char,\n        data: [ \"\\\\v\" ]\n      }, checks, function() { return state.jsonMode; });\n\n      char = \"\\v\";\n      break;\n    case \"x\":\n      var  x = parseInt(this.input.substr(1, 2), 16);\n\n      this.triggerAsync(\"warning\", {\n        code: \"W114\",\n        line: this.line,\n        character: this.char,\n        data: [ \"\\\\x-\" ]\n      }, checks, function() { return state.jsonMode; });\n\n      char = String.fromCharCode(x);\n      jump = 3;\n      break;\n    case \"\\\\\":\n      char = \"\\\\\\\\\";\n      break;\n    case \"/\":\n      break;\n    case \"\":\n      allowNewLine = true;\n      char = \"\";\n      break;\n    }\n\n    return { char: char, jump: jump, allowNewLine: allowNewLine };\n  },\n\n  /*\n   * Extract a template literal out of the next sequence of characters\n   * and/or lines or return 'null' if its not possible. Since template\n   * literals can span across multiple lines, this method has to move\n   * the char pointer.\n   */\n  scanTemplateLiteral: function(checks) {\n    var tokenType;\n    var value = \"\";\n    var ch;\n    var startLine = this.line;\n    var startChar = this.char;\n    var depth = this.templateStarts.length;\n\n    if (this.peek() === \"`\") {\n      if (!state.inES6(true)) {\n        this.triggerAsync(\n          \"warning\",\n          {\n            code: \"W119\",\n            line: this.line,\n            character: this.char,\n            data: [\"template literal syntax\", \"6\"]\n          },\n          checks,\n          function() { return true; }\n        );\n      }\n      // Template must start with a backtick.\n      tokenType = Token.TemplateHead;\n      this.templateStarts.push({ line: this.line, char: this.char });\n      depth = this.templateStarts.length;\n      this.skip(1);\n      this.pushContext(Context.Template);\n    } else if (this.inContext(Context.Template) && this.peek() === \"}\") {\n      // If we're in a template context, and we have a '}', lex a TemplateMiddle.\n      tokenType = Token.TemplateMiddle;\n    } else {\n      // Go lex something else.\n      return null;\n    }\n\n    while (this.peek() !== \"`\") {\n      while ((ch = this.peek()) === \"\") {\n        value += \"\\n\";\n        if (!this.nextLine(checks)) {\n          // Unclosed template literal --- point to the starting \"`\"\n          var startPos = this.templateStarts.pop();\n          this.trigger(\"error\", {\n            code: \"E052\",\n            line: startPos.line,\n            character: startPos.char\n          });\n          return {\n            type: tokenType,\n            value: value,\n            startLine: startLine,\n            startChar: startChar,\n            isUnclosed: true,\n            depth: depth,\n            context: this.popContext()\n          };\n        }\n      }\n\n      if (ch === '$' && this.peek(1) === '{') {\n        value += '${';\n        this.skip(2);\n        return {\n          type: tokenType,\n          value: value,\n          startLine: startLine,\n          startChar: startChar,\n          isUnclosed: false,\n          depth: depth,\n          context: this.currentContext()\n        };\n      } else if (ch === '\\\\') {\n        var escape = this.scanEscapeSequence(checks);\n        value += escape.char;\n        this.skip(escape.jump);\n      } else if (ch !== '`') {\n        // Otherwise, append the value and continue.\n        value += ch;\n        this.skip(1);\n      }\n    }\n\n    // Final value is either NoSubstTemplate or TemplateTail\n    tokenType = tokenType === Token.TemplateHead ? Token.NoSubstTemplate : Token.TemplateTail;\n    this.skip(1);\n    this.templateStarts.pop();\n\n    return {\n      type: tokenType,\n      value: value,\n      startLine: startLine,\n      startChar: startChar,\n      isUnclosed: false,\n      depth: depth,\n      context: this.popContext()\n    };\n  },\n\n  /*\n   * Extract a string out of the next sequence of characters and/or\n   * lines or return 'null' if its not possible. Since strings can\n   * span across multiple lines this method has to move the char\n   * pointer.\n   *\n   * This method recognizes pseudo-multiline JavaScript strings:\n   *\n   *   var str = \"hello\\\n   *   world\";\n   */\n  scanStringLiteral: function(checks) {\n    /*jshint loopfunc:true */\n    var quote = this.peek();\n\n    // String must start with a quote.\n    if (quote !== \"\\\"\" && quote !== \"'\") {\n      return null;\n    }\n\n    // In JSON strings must always use double quotes.\n    this.triggerAsync(\"warning\", {\n      code: \"W108\",\n      line: this.line,\n      character: this.char // +1?\n    }, checks, function() { return state.jsonMode && quote !== \"\\\"\"; });\n\n    var value = \"\";\n    var startLine = this.line;\n    var startChar = this.char;\n    var allowNewLine = false;\n\n    this.skip();\n\n    while (this.peek() !== quote) {\n      if (this.peek() === \"\") { // End Of Line\n\n        // If an EOL is not preceded by a backslash, show a warning\n        // and proceed like it was a legit multi-line string where\n        // author simply forgot to escape the newline symbol.\n        //\n        // Another approach is to implicitly close a string on EOL\n        // but it generates too many false positives.\n\n        if (!allowNewLine) {\n          // This condition unequivocally describes a syntax error.\n          // TODO: Emit error E029 and remove W112.\n          this.trigger(\"warning\", {\n            code: \"W112\",\n            line: this.line,\n            character: this.char\n          });\n        } else {\n          allowNewLine = false;\n\n          // Otherwise show a warning if multistr option was not set.\n          // For JSON, show warning no matter what.\n\n          this.triggerAsync(\"warning\", {\n            code: \"W043\",\n            line: this.line,\n            character: this.char\n          }, checks, function() { return !state.option.multistr; });\n\n          this.triggerAsync(\"warning\", {\n            code: \"W042\",\n            line: this.line,\n            character: this.char\n          }, checks, function() { return state.jsonMode && state.option.multistr; });\n        }\n\n        // If we get an EOF inside of an unclosed string, show an\n        // error and implicitly close it at the EOF point.\n\n        if (!this.nextLine(checks)) {\n          return {\n            type: Token.StringLiteral,\n            value: value,\n            startLine: startLine,\n            startChar: startChar,\n            isUnclosed: true,\n            quote: quote\n          };\n        }\n\n      } else { // Any character other than End Of Line\n\n        allowNewLine = false;\n        var char = this.peek();\n        var jump = 1; // A length of a jump, after we're done\n                      // parsing this character.\n\n        if (char < \" \") {\n          // Warn about a control character in a string.\n          this.triggerAsync(\n            \"warning\",\n            {\n              code: \"W113\",\n              line: this.line,\n              character: this.char,\n              data: [ \"<non-printable>\" ]\n            },\n            checks,\n            function() { return true; }\n          );\n        }\n\n        // Special treatment for some escaped characters.\n        if (char === \"\\\\\") {\n          var parsed = this.scanEscapeSequence(checks);\n          char = parsed.char;\n          jump = parsed.jump;\n          allowNewLine = parsed.allowNewLine;\n        }\n\n        // If char is the empty string, end of the line has been reached. In\n        // this case, `this.char` should not be incremented so that warnings\n        // and errors reported in the subsequent loop iteration have the\n        // correct character column offset.\n        if (char !== \"\") {\n          value += char;\n          this.skip(jump);\n        }\n      }\n    }\n\n    this.skip();\n    return {\n      type: Token.StringLiteral,\n      value: value,\n      startLine: startLine,\n      startChar: startChar,\n      isUnclosed: false,\n      quote: quote\n    };\n  },\n\n  /*\n   * Extract a regular expression out of the next sequence of\n   * characters and/or lines or return 'null' if its not possible.\n   *\n   * This method is platform dependent: it accepts almost any\n   * regular expression values but then tries to compile and run\n   * them using system's RegExp object. This means that there are\n   * rare edge cases where one JavaScript engine complains about\n   * your regular expression while others don't.\n   */\n  scanRegExp: function(checks) {\n    var index = 0;\n    var length = this.input.length;\n    var char = this.peek();\n    var value = char;\n    var body = \"\";\n    var groupReferences = [];\n    var allFlags = \"\";\n    var es5Flags = \"\";\n    var malformed = false;\n    var isCharSet = false;\n    var isCharSetRange = false;\n    var isGroup = false;\n    var isQuantifiable = false;\n    var hasInvalidQuantifier = false;\n    var escapedChars = \"\";\n    var hasUFlag = function() { return allFlags.indexOf(\"u\") > -1; };\n    var escapeSequence;\n    var groupCount = 0;\n    var terminated, malformedDesc;\n\n    var scanRegexpEscapeSequence = function() {\n      var next, sequence;\n      index += 1;\n      char = this.peek(index);\n\n      if (reg.nonzeroDigit.test(char)) {\n        sequence = char;\n        next = this.peek(index + 1);\n        while (reg.nonzeroDigit.test(next) || next === \"0\") {\n          index += 1;\n          char = next;\n          sequence += char;\n          body += char;\n          value += char;\n          next = this.peek(index + 1);\n        }\n        groupReferences.push(Number(sequence));\n        return sequence;\n      }\n\n      escapedChars += char;\n\n      if (char === \"u\" && this.peek(index + 1) === \"{\") {\n        var x = index + 2;\n        sequence = \"u{\";\n        next = this.peek(x);\n        while (isHex(next)) {\n          sequence += next;\n          x += 1;\n          next = this.peek(x);\n        }\n\n        if (next !== \"}\") {\n          this.triggerAsync(\n            \"error\",\n            {\n              code: \"E016\",\n              line: this.line,\n              character: this.char,\n              data: [ \"Invalid Unicode escape sequence\" ]\n            },\n            checks,\n            hasUFlag\n          );\n        } else if (sequence.length > 2) {\n          sequence += \"}\";\n          body += sequence;\n          value += sequence;\n          index = x + 1;\n          return sequence;\n        }\n      }\n\n      if (char === \"p\" || char === \"P\") {\n        var y = index + 2;\n        sequence = \"\";\n        next = \"\";\n\n        if (this.peek(index + 1) === \"{\") {\n          next = this.peek(y);\n          while (next && next !== \"}\") {\n            sequence += next;\n            y += 1;\n            next = this.peek(y);\n          }\n        }\n\n        // Module loading is intentionally deferred as an optimization for\n        // Node.js users who do not use Unicode escape sequences.\n        if (!sequence || !require(\"./validate-unicode-escape-sequence\")(sequence)) {\n          this.triggerAsync(\n            \"error\",\n            {\n              code: \"E016\",\n              line: this.line,\n              character: this.char,\n              data: [ \"Invalid Unicode property escape sequence\" ]\n            },\n            checks,\n            hasUFlag\n          );\n        }\n\n        if (sequence) {\n          sequence = char + \"{\" + sequence + \"}\";\n          body += sequence;\n          value += sequence;\n          index = y + 1;\n\n          if (!state.inES9()) {\n            this.triggerAsync(\n              \"warning\",\n              {\n                code: \"W119\",\n                line: this.line,\n                character: this.char,\n                data: [ \"Unicode property escape\", \"9\" ]\n              },\n              checks,\n              hasUFlag\n            );\n          }\n\n          return sequence;\n        }\n      }\n\n      // Unexpected control character\n      if (char < \" \") {\n        malformed = true;\n        this.triggerAsync(\n          \"warning\",\n          {\n            code: \"W048\",\n            line: this.line,\n            character: this.char\n          },\n          checks,\n          function() { return true; }\n        );\n      }\n\n      // Unexpected escaped character\n      if (char === \"<\") {\n        malformed = true;\n        this.triggerAsync(\n          \"warning\",\n          {\n            code: \"W049\",\n            line: this.line,\n            character: this.char,\n            data: [ char ]\n          },\n          checks,\n          function() { return true; }\n        );\n      } else if (char === \"0\" && reg.decimalDigit.test(this.peek(index + 1))) {\n        this.triggerAsync(\n          \"error\",\n          {\n            code: \"E016\",\n            line: this.line,\n            character: this.char,\n            data: [ \"Invalid decimal escape sequence\" ]\n          },\n          checks,\n          hasUFlag\n        );\n      }\n\n      index += 1;\n      body += char;\n      value += char;\n\n      return char;\n    }.bind(this);\n\n    var checkQuantifier = function() {\n      var lookahead = index;\n      var lowerBound = \"\";\n      var upperBound = \"\";\n      var next;\n\n      next = this.peek(lookahead + 1);\n\n      while (reg.decimalDigit.test(next)) {\n        lookahead += 1;\n        lowerBound += next;\n        next = this.peek(lookahead + 1);\n      }\n\n      if (!lowerBound) {\n        return false;\n      }\n\n      if (next === \"}\") {\n        return true;\n      }\n\n      if (next !== \",\") {\n        return false;\n      }\n\n      lookahead += 1;\n      next = this.peek(lookahead + 1);\n\n      while (reg.decimalDigit.test(next)) {\n        lookahead += 1;\n        upperBound += next;\n        next = this.peek(lookahead + 1);\n      }\n\n      if (next !== \"}\") {\n        return false;\n      }\n\n      if (upperBound) {\n        return Number(lowerBound) <= Number(upperBound);\n      }\n\n      return true;\n    }.bind(this);\n\n    var translateUFlag = function(body) {\n      // The BMP character to use as a replacement for astral symbols when\n      // translating an ES6 \"u\"-flagged pattern to an ES5-compatible\n      // approximation.\n      // Note: replacing with '\\uFFFF' enables false positives in unlikely\n      // scenarios. For example, `[\\u{1044f}-\\u{10440}]` is an invalid pattern\n      // that would not be detected by this substitution.\n      var astralSubstitute = \"\\uFFFF\";\n\n      return body\n        // Replace every Unicode escape sequence with the equivalent BMP\n        // character or a constant ASCII code point in the case of astral\n        // symbols. (See the above note on `astralSubstitute` for more\n        // information.)\n        .replace(/\\\\u\\{([0-9a-fA-F]+)\\}|\\\\u([a-fA-F0-9]{4})/g, function($0, $1, $2) {\n          var codePoint = parseInt($1 || $2, 16);\n          var literal;\n\n          if (codePoint > 0x10FFFF) {\n            malformed = true;\n            this.trigger(\"error\", {\n              code: \"E016\",\n              line: this.line,\n              character: this.char,\n              data: [ char ]\n            });\n\n            return;\n          }\n          literal = String.fromCharCode(codePoint);\n\n          if (reg.regexpSyntaxChars.test(literal)) {\n            return $0;\n          }\n\n          if (codePoint <= 0xFFFF) {\n            return String.fromCharCode(codePoint);\n          }\n          return astralSubstitute;\n        }.bind(this))\n        // Replace each paired surrogate with a single ASCII symbol to avoid\n        // throwing on regular expressions that are only valid in combination\n        // with the \"u\" flag.\n        .replace(\n          /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g,\n          astralSubstitute\n        );\n    }.bind(this);\n\n    // Regular expressions must start with '/'\n    if (!this.prereg || char !== \"/\") {\n      return null;\n    }\n\n    index += 1;\n    terminated = false;\n\n    // Try to get everything in between slashes. A couple of\n    // cases aside (see scanRegexpEscapeSequence) we don't really\n    // care whether the resulting expression is valid or not.\n    // We will check that later using the RegExp object.\n\n    while (index < length) {\n      // Because an iteration of this loop may terminate in a number of\n      // distinct locations, `isCharSetRange` is re-set at the onset of\n      // iteration.\n      isCharSetRange &= char === \"-\";\n      char = this.peek(index);\n      value += char;\n      body += char;\n\n      if (isCharSet) {\n        if (char === \"]\") {\n          if (this.peek(index - 1) !== \"\\\\\" || this.peek(index - 2) === \"\\\\\") {\n            isCharSet = false;\n          }\n        } else if (char === \"-\") {\n          isCharSetRange = true;\n        }\n      }\n\n      if (char === \"\\\\\") {\n        escapeSequence = scanRegexpEscapeSequence();\n\n        if (isCharSet && (this.peek(index) === \"-\" || isCharSetRange) &&\n          reg.regexpCharClasses.test(escapeSequence)) {\n          this.triggerAsync(\n            \"error\",\n            {\n              code: \"E016\",\n              line: this.line,\n              character: this.char,\n              data: [ \"Character class used in range\" ]\n            },\n            checks,\n            hasUFlag\n          );\n        }\n\n        continue;\n      }\n\n      if (isCharSet) {\n        index += 1;\n        continue;\n      }\n\n      if (char === \"{\" && !hasInvalidQuantifier) {\n        hasInvalidQuantifier = !checkQuantifier();\n      }\n\n      if (char === \"[\") {\n        isCharSet = true;\n        index += 1;\n        continue;\n      } else if (char === \"(\") {\n        isGroup = true;\n\n        if (this.peek(index + 1) === \"?\" &&\n          (this.peek(index + 2) === \"=\" || this.peek(index + 2) === \"!\")) {\n          isQuantifiable = true;\n        }\n      } else if (char === \")\") {\n        if (isQuantifiable) {\n          isQuantifiable = false;\n\n          if (reg.regexpQuantifiers.test(this.peek(index + 1))) {\n            this.triggerAsync(\n              \"error\",\n              {\n                code: \"E016\",\n                line: this.line,\n                character: this.char,\n                data: [ \"Quantified quantifiable\" ]\n              },\n              checks,\n              hasUFlag\n            );\n          }\n        } else {\n          groupCount += 1;\n        }\n\n        isGroup = false;\n      } else if (char === \"/\") {\n        body = body.substr(0, body.length - 1);\n        terminated = true;\n        index += 1;\n        break;\n      }\n\n      index += 1;\n    }\n\n    // A regular expression that was never closed is an\n    // error from which we cannot recover.\n\n    if (!terminated) {\n      this.trigger(\"error\", {\n        code: \"E015\",\n        line: this.line,\n        character: this.from\n      });\n\n      return void this.trigger(\"fatal\", {\n        line: this.line,\n        from: this.from\n      });\n    }\n\n    // Parse flags (if any).\n\n    while (index < length) {\n      char = this.peek(index);\n      if (!/[gimyus]/.test(char)) {\n        break;\n      }\n      if (char === \"y\") {\n        if (!state.inES6(true)) {\n          this.triggerAsync(\n            \"warning\",\n            {\n              code: \"W119\",\n              line: this.line,\n              character: this.char,\n              data: [ \"Sticky RegExp flag\", \"6\" ]\n            },\n            checks,\n            function() { return true; }\n          );\n        }\n      } else if (char === \"u\") {\n        if (!state.inES6(true)) {\n          this.triggerAsync(\n            \"warning\",\n            {\n              code: \"W119\",\n              line: this.line,\n              character: this.char,\n              data: [ \"Unicode RegExp flag\", \"6\" ]\n            },\n            checks,\n            function() { return true; }\n          );\n        }\n\n        var hasInvalidEscape = (function(groupReferences, groupCount, escapedChars, reg) {\n          var hasInvalidGroup = groupReferences.some(function(groupReference) {\n            if (groupReference > groupCount) {\n              return true;\n            }\n          });\n\n          if (hasInvalidGroup) {\n            return true;\n          }\n\n          return !escapedChars.split(\"\").every(function(escapedChar) {\n              return escapedChar === \"u\" ||\n                escapedChar === \"/\" ||\n                escapedChar === \"0\" ||\n                reg.regexpControlEscapes.test(escapedChar) ||\n                reg.regexpCharClasses.test(escapedChar) ||\n                reg.regexpSyntaxChars.test(escapedChar);\n            });\n        }(groupReferences, groupCount, escapedChars, reg));\n\n        if (hasInvalidEscape) {\n          malformedDesc = \"Invalid escape\";\n        } else if (hasInvalidQuantifier) {\n          malformedDesc = \"Invalid quantifier\";\n        }\n\n        body = translateUFlag(body);\n      } else if (char === \"s\") {\n        if (!state.inES9()) {\n          this.triggerAsync(\n            \"warning\",\n            {\n              code: \"W119\",\n              line: this.line,\n              character: this.char,\n              data: [ \"DotAll RegExp flag\", \"9\" ]\n            },\n            checks,\n            function() { return true; }\n          );\n        }\n        if (value.indexOf(\"s\") > -1) {\n          malformedDesc = \"Duplicate RegExp flag\";\n        }\n      } else {\n        es5Flags += char;\n      }\n\n      if (allFlags.indexOf(char) > -1) {\n        malformedDesc = \"Duplicate RegExp flag\";\n      }\n      allFlags += char;\n\n      value += char;\n      allFlags += char;\n      index += 1;\n    }\n\n    if (allFlags.indexOf(\"u\") === -1) {\n      this.triggerAsync(\"warning\", {\n        code: \"W147\",\n        line: this.line,\n        character: this.char\n      }, checks, function() { return state.option.regexpu; });\n    }\n\n    // Check regular expression for correctness.\n\n    try {\n      new RegExp(body, es5Flags);\n    } catch (err) {\n      /**\n       * Because JSHint relies on the current engine's RegExp parser to\n       * validate RegExp literals, the description (exposed as the \"data\"\n       * property on the error object) is platform dependent.\n       */\n      malformedDesc = err.message;\n    }\n\n    if (malformedDesc) {\n      malformed = true;\n      this.trigger(\"error\", {\n        code: \"E016\",\n        line: this.line,\n        character: this.char,\n        data: [ malformedDesc ]\n      });\n    } else if (allFlags.indexOf(\"s\") > -1 && !reg.regexpDot.test(body)) {\n      this.trigger(\"warning\", {\n        code: \"W148\",\n        line: this.line,\n        character: this.char\n      });\n    }\n\n    return {\n      type: Token.RegExp,\n      value: value,\n      isMalformed: malformed\n    };\n  },\n\n  /*\n   * Scan for any occurrence of non-breaking spaces. Non-breaking spaces\n   * can be mistakenly typed on OS X with option-space. Non UTF-8 web\n   * pages with non-breaking pages produce syntax errors.\n   */\n  scanNonBreakingSpaces: function() {\n    return state.option.nonbsp ?\n      this.input.search(/(\\u00A0)/) : -1;\n  },\n\n  /*\n   * Produce the next raw token or return 'null' if no tokens can be matched.\n   * This method skips over all space characters.\n   */\n  next: function(checks) {\n    this.from = this.char;\n\n    // Move to the next non-space character.\n    while (reg.whitespace.test(this.peek())) {\n      this.from += 1;\n      this.skip();\n    }\n\n    // Methods that work with multi-line structures and move the\n    // character pointer.\n\n    var match = this.scanComments(checks) ||\n      this.scanStringLiteral(checks) ||\n      this.scanTemplateLiteral(checks);\n\n    if (match) {\n      return match;\n    }\n\n    // Methods that don't move the character pointer.\n\n    match =\n      this.scanRegExp(checks) ||\n      this.scanPunctuator() ||\n      this.scanKeyword() ||\n      this.scanIdentifier(checks) ||\n      this.scanNumericLiteral(checks);\n\n    if (match) {\n      this.skip(match.tokenLength || match.value.length);\n      return match;\n    }\n\n    // No token could be matched, give up.\n\n    return null;\n  },\n\n  /*\n   * Switch to the next line and reset all char pointers. Once\n   * switched, this method also checks for other minor warnings.\n   */\n  nextLine: function(checks) {\n    var char;\n\n    if (this.line >= this.getLines().length) {\n      return false;\n    }\n\n    this.input = this.getLines()[this.line];\n    this.line += 1;\n    this.char = 1;\n    this.from = 1;\n\n    var inputTrimmed = this.input.trim();\n\n    var startsWith = function() {\n      return _.some(arguments, function(prefix) {\n        return inputTrimmed.indexOf(prefix) === 0;\n      });\n    };\n\n    var endsWith = function() {\n      return _.some(arguments, function(suffix) {\n        return inputTrimmed.indexOf(suffix, inputTrimmed.length - suffix.length) !== -1;\n      });\n    };\n\n    // If we are ignoring linter errors, replace the input with empty string\n    // if it doesn't already at least start or end a multi-line comment\n    if (this.ignoringLinterErrors === true) {\n      if (!startsWith(\"/*\", \"//\") && !(this.inComment && endsWith(\"*/\"))) {\n        this.input = \"\";\n      }\n    }\n\n    char = this.scanNonBreakingSpaces();\n    if (char >= 0) {\n      this.triggerAsync(\n        \"warning\",\n        { code: \"W125\", line: this.line, character: char + 1 },\n        checks,\n        function() { return true; }\n      );\n    }\n\n    this.input = this.input.replace(/\\t/g, state.tab);\n\n    // If there is a limit on line length, warn when lines get too\n    // long.\n\n    if (!this.ignoringLinterErrors && state.option.maxlen &&\n      state.option.maxlen < this.input.length) {\n      var inComment = this.inComment ||\n        startsWith.call(inputTrimmed, \"//\") ||\n        startsWith.call(inputTrimmed, \"/*\");\n\n      var shouldTriggerError = !inComment || !reg.maxlenException.test(inputTrimmed);\n\n      if (shouldTriggerError) {\n        this.triggerAsync(\n          \"warning\",\n          { code: \"W101\", line: this.line, character: this.input.length },\n          checks,\n          function() { return true; }\n        );\n      }\n    }\n\n    return true;\n  },\n\n  /*\n   * Produce the next token. This function is called by advance() to get\n   * the next token. It returns a token in a JSLint-compatible format.\n   */\n  token: function() {\n    /*jshint loopfunc:true */\n    var checks = asyncTrigger();\n    var token;\n\n    // Produce a token object.\n    var create = function(type, value, isProperty, token) {\n      /*jshint validthis:true */\n      var obj;\n\n      if (type !== \"(endline)\" && type !== \"(end)\") {\n        this.prereg = false;\n      }\n\n      if (type === \"(punctuator)\") {\n        switch (value) {\n        case \".\":\n        case \")\":\n        case \"~\":\n        case \"#\":\n        case \"]\":\n        case \"}\":\n        case \"++\":\n        case \"--\":\n          this.prereg = false;\n          break;\n        default:\n          this.prereg = true;\n        }\n\n        obj = Object.create(state.syntax[value] || state.syntax[\"(error)\"]);\n      }\n\n      if (type === \"(identifier)\") {\n        if (value === \"return\" || value === \"case\" || value === \"yield\" ||\n            value === \"typeof\" || value === \"instanceof\" || value === \"void\" ||\n            value === \"await\" || value === \"new\" || value === \"delete\" ||\n            value === \"default\" || value === \"extends\") {\n          this.prereg = true;\n        }\n\n        if (_.has(state.syntax, value)) {\n          obj = Object.create(state.syntax[value] || state.syntax[\"(error)\"]);\n        }\n      }\n\n      if (type === \"(template)\" || type === \"(template middle)\") {\n        this.prereg = true;\n      }\n\n      if (!obj) {\n        obj = Object.create(state.syntax[type]);\n      }\n\n      obj.identifier = (type === \"(identifier)\");\n      obj.type = obj.type || type;\n      obj.value = value;\n      obj.line = this.line;\n      obj.character = this.char;\n      obj.from = this.from;\n      if (obj.identifier && token) obj.raw_text = token.text || token.value;\n      if (token && token.startLine && token.startLine !== this.line) {\n        obj.startLine = token.startLine;\n      }\n      if (token && token.context) {\n        // Context of current token\n        obj.context = token.context;\n      }\n      if (token && token.depth) {\n        // Nested template depth\n        obj.depth = token.depth;\n      }\n      if (token && token.isUnclosed) {\n        // Mark token as unclosed string / template literal\n        obj.isUnclosed = token.isUnclosed;\n      }\n\n      if (isProperty && obj.identifier) {\n        obj.isProperty = isProperty;\n      }\n\n      obj.check = checks.check;\n\n      return obj;\n    }.bind(this);\n\n    for (;;) {\n      if (!this.input.length) {\n        if (this.nextLine(checks)) {\n          return create(\"(endline)\", \"\");\n        }\n\n        if (this.exhausted) {\n          return null;\n        }\n\n        this.exhausted = true;\n        return create(\"(end)\", \"\");\n      }\n\n      token = this.next(checks);\n\n      if (!token) {\n        if (this.input.length) {\n          // Unexpected character.\n          this.trigger(\"error\", {\n            code: \"E024\",\n            line: this.line,\n            character: this.char,\n            data: [ this.peek() ]\n          });\n\n          this.input = \"\";\n        }\n\n        continue;\n      }\n\n      switch (token.type) {\n      case Token.StringLiteral:\n        this.triggerAsync(\"String\", {\n          line: this.line,\n          char: this.char,\n          from: this.from,\n          startLine: token.startLine,\n          startChar: token.startChar,\n          value: token.value,\n          quote: token.quote\n        }, checks, function() { return true; });\n\n        return create(\"(string)\", token.value, null, token);\n\n      case Token.TemplateHead:\n        this.trigger(\"TemplateHead\", {\n          line: this.line,\n          char: this.char,\n          from: this.from,\n          startLine: token.startLine,\n          startChar: token.startChar,\n          value: token.value\n        });\n        return create(\"(template)\", token.value, null, token);\n\n      case Token.TemplateMiddle:\n        this.trigger(\"TemplateMiddle\", {\n          line: this.line,\n          char: this.char,\n          from: this.from,\n          startLine: token.startLine,\n          startChar: token.startChar,\n          value: token.value\n        });\n        return create(\"(template middle)\", token.value, null, token);\n\n      case Token.TemplateTail:\n        this.trigger(\"TemplateTail\", {\n          line: this.line,\n          char: this.char,\n          from: this.from,\n          startLine: token.startLine,\n          startChar: token.startChar,\n          value: token.value\n        });\n        return create(\"(template tail)\", token.value, null, token);\n\n      case Token.NoSubstTemplate:\n        this.trigger(\"NoSubstTemplate\", {\n          line: this.line,\n          char: this.char,\n          from: this.from,\n          startLine: token.startLine,\n          startChar: token.startChar,\n          value: token.value\n        });\n        return create(\"(no subst template)\", token.value, null, token);\n\n      case Token.Identifier:\n        this.triggerAsync(\"Identifier\", {\n          line: this.line,\n          char: this.char,\n          from: this.from,\n          name: token.value,\n          raw_name: token.text,\n          isProperty: state.tokens.curr.id === \".\"\n        }, checks, function() { return true; });\n\n        /* falls through */\n      case Token.Keyword:\n        return create(\"(identifier)\", token.value, state.tokens.curr.id === \".\", token);\n\n      case Token.NumericLiteral:\n        if (token.isMalformed) {\n          this.trigger(\"error\", {\n            code: \"E067\",\n            line: this.line,\n            character: this.char,\n            data: [ token.value ]\n          });\n        }\n\n        this.triggerAsync(\"warning\", {\n          code: \"W114\",\n          line: this.line,\n          character: this.char,\n          data: [ \"0x-\" ]\n        }, checks, function() { return token.base === 16 && state.jsonMode; });\n\n        this.triggerAsync(\"warning\", {\n          code: \"W115\",\n          line: this.line,\n          character: this.char\n        }, checks, function() {\n          return state.isStrict() && token.base === 8 && token.isLegacy;\n        });\n\n        this.triggerAsync(\"error\", {\n          code: \"E068\",\n          line: this.line,\n          character: this.char\n        }, checks, function() {\n          return state.isStrict() && token.isNonOctal;\n        });\n\n        this.trigger(\"Number\", {\n          line: this.line,\n          char: this.char,\n          from: this.from,\n          value: token.value,\n          base: token.base,\n          isMalformed: token.isMalformed\n        });\n\n        return create(\"(number)\", token.value);\n\n      case Token.RegExp:\n        return create(\"(regexp)\", token.value);\n\n      case Token.Comment:\n        if (token.isSpecial) {\n          return {\n            id: '(comment)',\n            value: token.value,\n            body: token.body,\n            type: token.commentType,\n            isSpecial: token.isSpecial,\n            line: this.line,\n            character: this.char,\n            from: this.from\n          };\n        }\n\n        break;\n\n      default:\n        return create(\"(punctuator)\", token.value);\n      }\n    }\n  }\n};\n\nexports.Lexer = Lexer;\nexports.Context = Context;\n\n},{\"../data/ascii-identifier-data.js\":1,\"../data/es5-identifier-names.js\":2,\"../data/non-ascii-identifier-part-only.js\":3,\"../data/non-ascii-identifier-start.js\":4,\"./reg.js\":22,\"./state.js\":24,\"./validate-unicode-escape-sequence\":26,\"events\":11,\"lodash\":12}],18:[function(require,module,exports){\n\"use strict\";\n\nvar _ = require(\"lodash\");\n\nvar errors = {\n  // JSHint options\n  E001: \"Bad {a}option: '{b}'.\",\n  E002: \"Bad option value.\",\n\n  // JSHint input\n  E003: \"Expected a JSON value.\",\n  E004: \"Input is neither a string nor an array of strings.\",\n  E005: \"Input is empty.\",\n  E006: \"Unexpected early end of program.\",\n\n  // Strict mode\n  E007: \"Missing \\\"use strict\\\" statement.\",\n  E008: \"Strict violation.\",\n  E009: \"Option 'validthis' can't be used in a global scope.\",\n  E010: \"'with' is not allowed in strict mode.\",\n\n  // Constants\n  E011: \"'{a}' has already been declared.\",\n  E012: \"const '{a}' is initialized to 'undefined'.\",\n  E013: \"Attempting to override '{a}' which is a constant.\",\n\n  // Regular expressions\n  E014: \"A regular expression literal can be confused with '/='.\",\n  E015: \"Unclosed regular expression.\",\n  E016: \"Invalid regular expression.\",\n\n  // Tokens\n  E017: \"Unclosed comment.\",\n  E018: \"Unbegun comment.\",\n  E019: \"Unmatched '{a}'.\",\n  E020: \"Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'.\",\n  E021: \"Expected '{a}' and instead saw '{b}'.\",\n  E022: \"Line breaking error '{a}'.\",\n  E023: \"Missing '{a}'.\",\n  E024: \"Unexpected '{a}'.\",\n  E025: \"Missing ':' on a case clause.\",\n  E026: \"Missing '}' to match '{' from line {a}.\",\n  E027: \"Missing ']' to match '[' from line {a}.\",\n  E028: \"Illegal comma.\",\n  E029: \"Unclosed string.\",\n\n  // Everything else\n  E030: \"Expected an identifier and instead saw '{a}'.\",\n  E031: \"Bad assignment.\", // FIXME: Rephrase\n  E032: \"Expected a small integer or 'false' and instead saw '{a}'.\",\n  E033: \"Expected an operator and instead saw '{a}'.\",\n  E034: \"get/set are ES5 features.\",\n  E035: \"Missing property name.\",\n  E036: \"Expected to see a statement and instead saw a block.\",\n  E037: null,\n  E038: null,\n  E039: \"Function declarations are not invocable. Wrap the whole function invocation in parens.\",\n  E040: \"Each value should have its own case label.\",\n  E041: \"Unrecoverable syntax error.\",\n  E042: \"Stopping.\",\n  E043: \"Too many errors.\",\n  E044: null,\n  E045: \"Invalid for each loop.\",\n  E046: \"Yield expressions may only occur within generator functions.\",\n  E047: null,\n  E048: \"{a} declaration not directly within block.\",\n  E049: \"A {a} cannot be named '{b}'.\",\n  E050: \"Mozilla requires the yield expression to be parenthesized here.\",\n  E051: null,\n  E052: \"Unclosed template literal.\",\n  E053: \"{a} declarations are only allowed at the top level of module scope.\",\n  E054: \"Class properties must be methods. Expected '(' but instead saw '{a}'.\",\n  E055: \"The '{a}' option cannot be set after any executable code.\",\n  E056: \"'{a}' was used before it was declared, which is illegal for '{b}' variables.\",\n  E057: \"Invalid meta property: '{a}.{b}'.\",\n  E058: \"Missing semicolon.\",\n  E059: \"Incompatible values for the '{a}' and '{b}' linting options.\",\n  E060: \"Non-callable values cannot be used as the second operand to instanceof.\",\n  E061: \"Invalid position for 'yield' expression (consider wrapping in parenthesis).\",\n  E062: \"Rest parameter does not a support default value.\",\n  E063: \"Super property may only be used within method bodies.\",\n  E064: \"Super call may only be used within class method bodies.\",\n  E065: \"Functions defined outside of strict mode with non-simple parameter lists may not \" +\n    \"enable strict mode.\",\n  E066: \"Asynchronous iteration is only available with for-of loops.\",\n  E067: \"Malformed numeric literal: '{a}'.\",\n  E068: \"Decimals with leading zeros are not allowed in strict mode.\",\n  E069: \"Duplicate exported binding: '{a}'.\",\n  E070: \"import.meta may only be used in module code.\"\n};\n\nvar warnings = {\n  W001: \"'hasOwnProperty' is a really bad name.\",\n  W002: \"Value of '{a}' may be overwritten in IE 8 and earlier.\",\n  W003: \"'{a}' was used before it was defined.\",\n  W004: \"'{a}' is already defined.\",\n  W005: \"A dot following a number can be confused with a decimal point.\",\n  W006: \"Confusing minuses.\",\n  W007: \"Confusing plusses.\",\n  W008: \"A leading decimal point can be confused with a dot: '{a}'.\",\n  W009: \"The array literal notation [] is preferable.\",\n  W010: \"The object literal notation {} is preferable.\",\n  W011: null,\n  W012: null,\n  W013: null,\n  W014: \"Misleading line break before '{a}'; readers may interpret this as an expression boundary.\",\n  W015: null,\n  W016: \"Unexpected use of '{a}'.\",\n  W017: \"Bad operand.\",\n  W018: \"Confusing use of '{a}'.\",\n  W019: \"Use the isNaN function to compare with NaN.\",\n  W020: \"Read only.\",\n  W021: \"Reassignment of '{a}', which is a {b}. \" +\n    \"Use 'var' or 'let' to declare bindings that may change.\",\n  W022: \"Do not assign to the exception parameter.\",\n  W023: null,\n  W024: \"Expected an identifier and instead saw '{a}' (a reserved word).\",\n  W025: \"Missing name in function declaration.\",\n  W026: \"Inner functions should be listed at the top of the outer function.\",\n  W027: \"Unreachable '{a}' after '{b}'.\",\n  W028: \"Label '{a}' on {b} statement.\",\n  W030: \"Expected an assignment or function call and instead saw an expression.\",\n  W031: \"Do not use 'new' for side effects.\",\n  W032: \"Unnecessary semicolon.\",\n  W033: \"Missing semicolon.\",\n  W034: \"Unnecessary directive \\\"{a}\\\".\",\n  W035: \"Empty block.\",\n  W036: \"Unexpected /*member '{a}'.\",\n  W037: \"'{a}' is a statement label.\",\n  W038: \"'{a}' used out of scope.\",\n  W039: null,\n  W040: \"If a strict mode function is executed using function invocation, \" +\n    \"its 'this' value will be undefined.\",\n  W041: null,\n  W042: \"Avoid EOL escaping.\",\n  W043: \"Bad escaping of EOL. Use option multistr if needed.\",\n  W044: \"Bad or unnecessary escaping.\", /* TODO(caitp): remove W044 */\n  W045: \"Value described by numeric literal cannot be accurately \" +\n    \"represented with a number value: '{a}'.\",\n  W046: \"Don't use extra leading zeros '{a}'.\",\n  W047: \"A trailing decimal point can be confused with a dot: '{a}'.\",\n  W048: \"Unexpected control character in regular expression.\",\n  W049: \"Unexpected escaped character '{a}' in regular expression.\",\n  W050: \"JavaScript URL.\",\n  W051: \"Variables should not be deleted.\",\n  W052: \"Unexpected '{a}'.\",\n  W053: \"Do not use {a} as a constructor.\",\n  W054: \"The Function constructor is a form of eval.\",\n  W055: \"A constructor name should start with an uppercase letter.\",\n  W056: \"Bad constructor.\",\n  W057: \"Weird construction. Is 'new' necessary?\",\n  W058: \"Missing '()' invoking a constructor.\",\n  W059: \"Avoid arguments.{a}.\",\n  W060: \"document.write can be a form of eval.\",\n  W061: \"eval can be harmful.\",\n  W062: \"Wrap an immediate function invocation in parens \" +\n    \"to assist the reader in understanding that the expression \" +\n    \"is the result of a function, and not the function itself.\",\n  W063: \"Math is not a function.\",\n  W064: \"Missing 'new' prefix when invoking a constructor.\",\n  W065: \"Missing radix parameter.\",\n  W066: \"Implied eval. Consider passing a function instead of a string.\",\n  W067: \"Unorthodox function invocation.\",\n  W068: \"Wrapping non-IIFE function literals in parens is unnecessary.\",\n  W069: \"['{a}'] is better written in dot notation.\",\n  W070: \"Extra comma. (it breaks older versions of IE)\",\n  W071: \"This function has too many statements. ({a})\",\n  W072: \"This function has too many parameters. ({a})\",\n  W073: \"Blocks are nested too deeply. ({a})\",\n  W074: \"This function's cyclomatic complexity is too high. ({a})\",\n  W075: \"Duplicate {a} '{b}'.\",\n  W076: \"Unexpected parameter '{a}' in get {b} function.\",\n  W077: \"Expected a single parameter in set {a} function.\",\n  W078: \"Setter is defined without getter.\",\n  W079: \"Redefinition of '{a}'.\",\n  W080: \"It's not necessary to initialize '{a}' to 'undefined'.\",\n  W081: null,\n  W082: \"Function declarations should not be placed in blocks. \" +\n    \"Use a function expression or move the statement to the top of \" +\n    \"the outer function.\",\n  W083: \"Functions declared within loops referencing an outer scoped \" +\n    \"variable may lead to confusing semantics. ({a})\",\n  W084: \"Expected a conditional expression and instead saw an assignment.\",\n  W085: \"Don't use 'with'.\",\n  W086: \"Expected a 'break' statement before '{a}'.\",\n  W087: \"Forgotten 'debugger' statement?\",\n  W088: \"Creating global 'for' variable. Should be 'for (var {a} ...'.\",\n  W089: \"The body of a for in should be wrapped in an if statement to filter \" +\n    \"unwanted properties from the prototype.\",\n  W090: \"'{a}' is not a statement label.\",\n  W091: null,\n  W093: \"Did you mean to return a conditional instead of an assignment?\",\n  W094: \"Unexpected comma.\",\n  W095: \"Expected a string and instead saw {a}.\",\n  W096: \"The '{a}' key may produce unexpected results.\",\n  W097: \"Use the function form of \\\"use strict\\\".\",\n  W098: \"'{a}' is defined but never used.\",\n  W099: null,\n  W100: null,\n  W101: \"Line is too long.\",\n  W102: null,\n  W103: \"The '{a}' property is deprecated.\",\n  W104: \"'{a}' is available in ES{b} (use 'esversion: {b}') or Mozilla JS extensions (use moz).\",\n  W105: null,\n  W106: \"Identifier '{a}' is not in camel case.\",\n  W107: \"Script URL.\",\n  W108: \"Strings must use doublequote.\",\n  W109: \"Strings must use singlequote.\",\n  W110: \"Mixed double and single quotes.\",\n  W112: \"Unclosed string.\",\n  W113: \"Control character in string: {a}.\",\n  W114: \"Avoid {a}.\",\n  W115: \"Octal literals are not allowed in strict mode.\",\n  W116: \"Expected '{a}' and instead saw '{b}'.\",\n  W117: \"'{a}' is not defined.\",\n  W118: \"'{a}' is only available in Mozilla JavaScript extensions (use moz option).\",\n  W119: \"'{a}' is only available in ES{b} (use 'esversion: {b}').\",\n  W120: \"You might be leaking a variable ({a}) here.\",\n  W121: \"Extending prototype of native object: '{a}'.\",\n  W122: \"Invalid typeof value '{a}'\",\n  W123: \"'{a}' is already defined in outer scope.\",\n  W124: \"A generator function should contain at least one yield expression.\",\n  W125: \"This line contains non-breaking spaces: http://jshint.com/docs/options/#nonbsp\",\n  W126: \"Unnecessary grouping operator.\",\n  W127: \"Unexpected use of a comma operator.\",\n  W128: \"Empty array elements require elision=true.\",\n  W129: \"'{a}' is defined in a future version of JavaScript. Use a \" +\n    \"different variable name to avoid migration issues.\",\n  W130: \"Invalid element after rest element.\",\n  W131: \"Invalid parameter after rest parameter.\",\n  W132: \"`var` declarations are forbidden. Use `let` or `const` instead.\",\n  W133: \"Invalid for-{a} loop left-hand-side: {b}.\",\n  W134: \"The '{a}' option is only available when linting ECMAScript {b} code.\",\n  W135: \"{a} may not be supported by non-browser environments.\",\n  W136: \"'{a}' must be in function scope.\",\n  W137: \"Empty destructuring: this is unnecessary and can be removed.\",\n  W138: \"Regular parameters should not come after default parameters.\",\n  W139: \"Function expressions should not be used as the second operand to instanceof.\",\n  W140: \"Missing comma.\",\n  W141: \"Empty {a}: this is unnecessary and can be removed.\",\n  W142: \"Empty {a}: consider replacing with `import '{b}';`.\",\n  W143: \"Assignment to properties of a mapped arguments object may cause \" +\n    \"unexpected changes to formal parameters.\",\n  W144: \"'{a}' is a non-standard language feature. Enable it using the '{b}' unstable option.\",\n  W145: \"Superfluous 'case' clause.\",\n  W146: \"Unnecessary `await` expression.\",\n  W147: \"Regular expressions should include the 'u' flag.\",\n  W148: \"Unnecessary RegExp 's' flag.\"\n};\n\nvar info = {\n  I001: \"Comma warnings can be turned off with 'laxcomma'.\",\n  I002: null,\n  I003: \"ES5 option is now set per default\"\n};\n\nexports.errors = {};\nexports.warnings = {};\nexports.info = {};\n\n_.each(errors, function(desc, code) {\n  exports.errors[code] = { code: code, desc: desc };\n});\n\n_.each(warnings, function(desc, code) {\n  exports.warnings[code] = { code: code, desc: desc };\n});\n\n_.each(info, function(desc, code) {\n  exports.info[code] = { code: code, desc: desc };\n});\n\n},{\"lodash\":12}],19:[function(require,module,exports){\n/**\n * The NameStack class is used to approximate function name inference as\n * introduced by ECMAScript 2015. In that edition, the `name` property of\n * function objects is set according to the function's syntactic form. For\n * certain forms, this value depends on values available to the runtime during\n * execution. For example:\n *\n *     var fnName = function() {};\n *\n * In the program code above, the function object's `name` property is set to\n * `\"fnName\"` during execution.\n *\n * This general \"name inference\" behavior extends to a number of additional\n * syntactic forms, not all of which can be implemented statically. `NameStack`\n * is a support class representing a \"best-effort\" attempt to implement the\n * specified behavior in cases where this may be done statically.\n *\n * For more information on this behavior, see the following blog post:\n * https://bocoup.com/blog/whats-in-a-function-name\n */\n\"use strict\";\n\nfunction NameStack() {\n  this._stack = [];\n}\n\nObject.defineProperty(NameStack.prototype, \"length\", {\n  get: function() {\n    return this._stack.length;\n  }\n});\n\n/**\n * Create a new entry in the stack. Useful for tracking names across\n * expressions.\n */\nNameStack.prototype.push = function() {\n  this._stack.push(null);\n};\n\n/**\n * Discard the most recently-created name on the stack.\n */\nNameStack.prototype.pop = function() {\n  this._stack.pop();\n};\n\n/**\n * Update the most recent name on the top of the stack.\n *\n * @param {object} token The token to consider as the source for the most\n *                       recent name.\n */\nNameStack.prototype.set = function(token) {\n  this._stack[this.length - 1] = token;\n};\n\n/**\n * Generate a string representation of the most recent name.\n *\n * @returns {string}\n */\nNameStack.prototype.infer = function() {\n  var nameToken = this._stack[this.length - 1];\n  var prefix = \"\";\n  var type;\n\n  // During expected operation, the topmost entry on the stack will only\n  // reflect the current function's name when the function is declared without\n  // the `function` keyword (i.e. for in-line accessor methods). In other\n  // cases, the `function` expression itself will introduce an empty entry on\n  // the top of the stack, and this should be ignored.\n  if (!nameToken || nameToken.type === \"class\") {\n    nameToken = this._stack[this.length - 2];\n  }\n\n  if (!nameToken) {\n    return \"(empty)\";\n  }\n\n  type = nameToken.type;\n\n  if (type !== \"(string)\" && type !== \"(number)\" && type !== \"(identifier)\" && type !== \"default\") {\n    return \"(expression)\";\n  }\n\n  if (nameToken.accessorType) {\n    prefix = nameToken.accessorType + \" \";\n  }\n\n  return prefix + nameToken.value;\n};\n\nmodule.exports = NameStack;\n\n},{}],20:[function(require,module,exports){\n\"use strict\";\n\n// These are the JSHint boolean options.\nexports.bool = {\n  enforcing: {\n\n    /**\n     * This option prohibits the use of bitwise operators such as `^` (XOR),\n     * `|` (OR) and others. Bitwise operators are very rare in JavaScript\n     * programs and quite often `&` is simply a mistyped `&&`.\n     */\n    bitwise     : true,\n\n    /**\n     *\n     * This options prohibits overwriting prototypes of native objects such as\n     * `Array`, `Date` and so on.\n     *\n     *     // jshint freeze:true\n     *     Array.prototype.count = function (value) { return 4; };\n     *     // -> Warning: Extending prototype of native object: 'Array'.\n     */\n    freeze      : true,\n\n    /**\n     * This option allows you to force all variable names to use either\n     * camelCase style or UPPER_CASE with underscores.\n     *\n     * @deprecated JSHint is limiting its scope to issues of code correctness.\n     *             If you would like to enforce rules relating to code style,\n     *             check out [the JSCS\n     *             project](https://github.com/jscs-dev/node-jscs).\n     */\n    camelcase   : true,\n\n    /**\n     * This option requires you to always put curly braces around blocks in\n     * loops and conditionals. JavaScript allows you to omit curly braces when\n     * the block consists of only one statement, for example:\n     *\n     *     while (day)\n     *       shuffle();\n     *\n     * However, in some circumstances, it can lead to bugs (you'd think that\n     * `sleep()` is a part of the loop while in reality it is not):\n     *\n     *     while (day)\n     *       shuffle();\n     *       sleep();\n     */\n    curly       : true,\n\n    /**\n     * This options prohibits the use of `==` and `!=` in favor of `===` and\n     * `!==`. The former try to coerce values before comparing them which can\n     * lead to some unexpected results. The latter don't do any coercion so\n     * they are generally safer. If you would like to learn more about type\n     * coercion in JavaScript, we recommend [Truth, Equality and\n     * JavaScript](http://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/)\n     * by Angus Croll.\n     */\n    eqeqeq      : true,\n\n    /**\n     * This option enables warnings about the use of identifiers which are\n     * defined in future versions of JavaScript. Although overwriting them has\n     * no effect in contexts where they are not implemented, this practice can\n     * cause issues when migrating codebases to newer versions of the language.\n     */\n    futurehostile: true,\n\n    /**\n     * This option tells JSHint that your code needs to adhere to ECMAScript 3\n     * specification. Use this option if you need your program to be executable\n     * in older browsers—such as Internet Explorer 6/7/8/9—and other legacy\n     * JavaScript environments.\n     *\n     * @deprecated Use `esversion: 3` instead.\n     */\n    es3         : true,\n\n    /**\n     * This option enables syntax first defined in [the ECMAScript 5.1\n     * specification](http://es5.github.io/). This includes allowing reserved\n     * keywords as object properties.\n     *\n     * @deprecated Use `esversion: 5` instead.\n     */\n    es5         : true,\n\n    /**\n     * This option requires all `for in` loops to filter object's items. The\n     * for in statement allows for looping through the names of all of the\n     * properties of an object including those inherited through the prototype\n     * chain. This behavior can lead to unexpected items in your object so it\n     * is generally safer to always filter inherited properties out as shown in\n     * the example:\n     *\n     *     for (key in obj) {\n     *       if (obj.hasOwnProperty(key)) {\n     *         // We are sure that obj[key] belongs to the object and was not inherited.\n     *       }\n     *     }\n     *\n     * For more in-depth understanding of `for in` loops in JavaScript, read\n     * [Exploring JavaScript for-in\n     * loops](http://javascriptweblog.wordpress.com/2011/01/04/exploring-javascript-for-in-loops/)\n     * by Angus Croll.\n     */\n    forin       : true,\n\n    /**\n     * This option prohibits the use of immediate function invocations without\n     * wrapping them in parentheses. Wrapping parentheses assists readers of\n     * your code in understanding that the expression is the result of a\n     * function, and not the function itself.\n     *\n     * @deprecated JSHint is limiting its scope to issues of code correctness.\n     *             If you would like to enforce rules relating to code style,\n     *             check out [the JSCS\n     *             project](https://github.com/jscs-dev/node-jscs).\n     */\n    immed       : true,\n\n    /**\n     * This option prohibits unnecessary clauses within `switch` statements,\n     * e.g.\n     *\n     *     switch (x) {\n     *       case 1:\n     *       default:\n     *         z();\n     *     }\n     *\n     * While clauses like these are techincally valid, they do not effect\n     * program behavior and may indicate an erroneous refactoring.\n     */\n    leanswitch  : true,\n\n    /**\n     * This option requires you to capitalize names of constructor functions.\n     * Capitalizing functions that are intended to be used with `new` operator\n     * is just a convention that helps programmers to visually distinguish\n     * constructor functions from other types of functions to help spot\n     * mistakes when using `this`.\n     *\n     * Not doing so won't break your code in any browsers or environments but\n     * it will be a bit harder to figure out—by reading the code—if the\n     * function was supposed to be used with or without new. And this is\n     * important because when the function that was intended to be used with\n     * `new` is used without it, `this` will point to the global object instead\n     * of a new object.\n     *\n     * @deprecated JSHint is limiting its scope to issues of code correctness.\n     *             If you would like to enforce rules relating to code style,\n     *             check out [the JSCS\n     *             project](https://github.com/jscs-dev/node-jscs).\n     */\n    newcap      : true,\n\n    /**\n     * This option prohibits the use of `arguments.caller` and\n     * `arguments.callee`.  Both `.caller` and `.callee` make quite a few\n     * optimizations impossible so they were deprecated in future versions of\n     * JavaScript. In fact, ECMAScript 5 forbids the use of `arguments.callee`\n     * in strict mode.\n     */\n    noarg       : true,\n\n    /**\n     * This option prohibits the use of the comma operator. When misused, the\n     * comma operator can obscure the value of a statement and promote\n     * incorrect code.\n     */\n    nocomma     : true,\n\n    /**\n     * This option warns when you have an empty block in your code. JSLint was\n     * originally warning for all empty blocks and we simply made it optional.\n     * There were no studies reporting that empty blocks in JavaScript break\n     * your code in any way.\n     *\n     * @deprecated JSHint is limiting its scope to issues of code correctness.\n     *             If you would like to enforce rules relating to code style,\n     *             check out [the JSCS\n     *             project](https://github.com/jscs-dev/node-jscs).\n     */\n    noempty     : true,\n\n    /**\n     * This option warns about \"non-breaking whitespace\" characters. These\n     * characters can be entered with option-space on Mac computers and have a\n     * potential of breaking non-UTF8 web pages.\n     */\n    nonbsp      : true,\n\n    /**\n     * This option prohibits the use of constructor functions for side-effects.\n     * Some people like to call constructor functions without assigning its\n     * result to any variable:\n     *\n     *     new MyConstructor();\n     *\n     * There is no advantage in this approach over simply calling\n     * `MyConstructor` since the object that the operator `new` creates isn't\n     * used anywhere so you should generally avoid constructors like this one.\n     */\n    nonew       : true,\n\n\n    /**\n     * Async functions resolve on their return value. In most cases, this makes\n     * returning the result of an AwaitExpression (which is itself a Promise\n     * instance) unnecessary. For clarity, it's often preferable to return the\n     * result of the asynchronous operation directly. The notable exception is\n     * within the `try` clause of a TryStatement--for more, see \"await vs\n     * return vs return await\":\n     *\n     * https://jakearchibald.com/2017/await-vs-return-vs-return-await/\n     */\n    noreturnawait: true,\n\n    /**\n     * This option enables warnings for regular expressions which do not\n     * include the \"u\" flag. The \"u\" flag extends support for Unicode and also\n     * enables more strict parsing rules. JSHint will enforce these rules even\n     * if it is executed in a JavaScript engine which does not support the \"u\"\n     * flag.\n     */\n    regexpu     : true,\n\n    /**\n     * This option prohibits the use of explicitly undeclared variables. This\n     * option is very useful for spotting leaking and mistyped variables.\n     *\n     *     // jshint undef:true\n     *\n     *     function test() {\n     *       var myVar = 'Hello, World';\n     *       console.log(myvar); // Oops, typoed here. JSHint with undef will complain\n     *     }\n     *\n     * If your variable is defined in another file, you can use the `global`\n     * directive to tell JSHint about it.\n     */\n    undef       : true,\n\n    /**\n     * This option prohibits the use of the grouping operator when it is not\n     * strictly required. Such usage commonly reflects a misunderstanding of\n     * unary operators, for example:\n     *\n     *     // jshint singleGroups: true\n     *\n     *     delete(obj.attr); // Warning: Unnecessary grouping operator.\n     */\n    singleGroups: false,\n\n    /**\n     * When set to true, the use of VariableStatements are forbidden.\n     * For example:\n     *\n     *     // jshint varstmt: true\n     *\n     *     var a; // Warning: `var` declarations are forbidden. Use `let` or `const` instead.\n     */\n    varstmt: false,\n\n    /**\n     * This option is a short hand for the most strict JSHint configuration as\n     * available in JSHint version 2.6.3. It enables all enforcing options and\n     * disables all relaxing options that were defined in that release.\n     *\n     * @deprecated The option cannot be maintained without automatically opting\n     *             users in to new features. This can lead to unexpected\n     *             warnings/errors in when upgrading between minor versions of\n     *             JSHint.\n     */\n    enforceall : false,\n\n    /**\n     * This option warns when a comma is not placed after the last element in an\n     * array or object literal. Due to bugs in old versions of IE, trailing\n     * commas used to be discouraged, but since ES5 their semantics were\n     * standardized. (See\n     * [#11.1.4](http://www.ecma-international.org/ecma-262/5.1/#sec-11.1.4) and\n     * [#11.1.5](http://www.ecma-international.org/ecma-262/5.1/#sec-11.1.5).)\n     * Now, they help to prevent the same [visual\n     * ambiguities](http://www.ecma-international.org/ecma-262/5.1/#sec-7.9.2)\n     * that the strict usage of semicolons helps prevent.\n     *\n     * For example, this code might have worked last Tuesday:\n     *\n     *     [\n     *         b + c\n     *     ].forEach(print);\n     *\n     * But if one adds an element to the array and forgets to compensate for the\n     * missing comma, no syntax error is thrown, and a linter cannot determine\n     * if this was a mistake or an intentional function invocation.\n     *\n     *     [\n     *         b + c\n     *         (d + e)\n     *     ].forEach(print);\n     *\n     * If one always appends a list item with a comma, this ambiguity cannot\n     * occur:\n     *\n     *     [\n     *         b + c,\n     *     ].forEach(print);\n     *\n     *     [\n     *         b + c,\n     *         (d + e),\n     *     ].forEach(print);\n     */\n    trailingcomma: false\n  },\n  relaxing: {\n\n    /**\n     * This option suppresses warnings about missing semicolons. There is a lot\n     * of FUD about semicolon spread by quite a few people in the community.\n     * The common myths are that semicolons are required all the time (they are\n     * not) and that they are unreliable. JavaScript has rules about semicolons\n     * which are followed by *all* browsers so it is up to you to decide\n     * whether you should or should not use semicolons in your code.\n     *\n     * For more information about semicolons in JavaScript read [An Open Letter\n     * to JavaScript Leaders Regarding\n     * Semicolons](http://blog.izs.me/post/2353458699/an-open-letter-to-javascript-leaders-regarding)\n     * by Isaac Schlueter and [JavaScript Semicolon\n     * Insertion](http://inimino.org/~inimino/blog/javascript_semicolons).\n     */\n    asi         : true,\n\n    /**\n     * This option suppresses warnings about multi-line strings. Multi-line\n     * strings can be dangerous in JavaScript because all hell breaks loose if\n     * you accidentally put a whitespace in between the escape character (`\\`)\n     * and a new line.\n     *\n     * Note that even though this option allows correct multi-line strings, it\n     * still warns about multi-line strings without escape characters or with\n     * anything in between the escape character and a whitespace.\n     *\n     *     // jshint multistr:true\n     *\n     *     var text = \"Hello\\\n     *     World\"; // All good.\n     *\n     *     text = \"Hello\n     *     World\"; // Warning, no escape character.\n     *\n     *     text = \"Hello\\\n     *     World\"; // Warning, there is a space after \\\n     *\n     * @deprecated JSHint is limiting its scope to issues of code correctness.\n     *             If you would like to enforce rules relating to code style,\n     *             check out [the JSCS\n     *             project](https://github.com/jscs-dev/node-jscs).\n     */\n    multistr    : true,\n\n    /**\n     * This option suppresses warnings about the `debugger` statements in your\n     * code.\n     */\n    debug       : true,\n\n    /**\n     * This option suppresses warnings about the use of assignments in cases\n     * where comparisons are expected. More often than not, code like `if (a =\n     * 10) {}` is a typo. However, it can be useful in cases like this one:\n     *\n     *     for (var i = 0, person; person = people[i]; i++) {}\n     *\n     * You can silence this error on a per-use basis by surrounding the assignment\n     * with parenthesis, such as:\n     *\n     *     for (var i = 0, person; (person = people[i]); i++) {}\n     */\n    boss        : true,\n\n    /**\n     * This option suppresses warnings about the use of `eval`. The use of\n     * `eval` is discouraged because it can make your code vulnerable to\n     * various injection attacks and it makes it hard for JavaScript\n     * interpreter to do certain optimizations.\n    */\n    evil        : true,\n\n    /**\n     * This option suppresses warnings about declaring variables inside\n     * of control structures while accessing them later from the outside.\n     * Even though identifiers declared with `var` have two real scopes—global\n     * and function—such practice leads to confusion among people new to\n     * the language and hard-to-debug bugs. This is why, by default, JSHint\n     * warns about variables that are used outside of their intended scope.\n     *\n     *     function test() {\n     *       if (true) {\n     *         var x = 0;\n     *       }\n     *\n     *       x += 1; // Default: 'x' used out of scope.\n     *                 // No warning when funcscope:true\n     *     }\n     */\n    funcscope   : true,\n\n    /**\n     * This option suppresses warnings about the use of global strict mode.\n     * Global strict mode can break third-party widgets so it is not\n     * recommended.\n     *\n     * For more info about strict mode see the `strict` option.\n     *\n     * @deprecated Use `strict: \"global\"`.\n     */\n    globalstrict: true,\n\n    /**\n     * This option suppresses warnings about the `__iterator__` property. This\n     * property is not supported by all browsers so use it carefully.\n     */\n    iterator    : true,\n\n     /**\n     * This option suppresses warnings about invalid `typeof` operator values.\n     * This operator has only [a limited set of possible return\n     * values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof).\n     * By default, JSHint warns when you compare its result with an invalid\n     * value which often can be a typo.\n     *\n     *     // 'fuction' instead of 'function'\n     *     if (typeof a == \"fuction\") { // Invalid typeof value 'fuction'\n     *       // ...\n     *     }\n     *\n     * Do not use this option unless you're absolutely sure you don't want\n     * these checks.\n     */\n    notypeof    : true,\n\n    /**\n     * This option prohibits the use of unary increment and decrement\n     * operators.  Some people think that `++` and `--` reduces the quality of\n     * their coding styles and there are programming languages—such as\n     * Python—that go completely without these operators.\n     */\n    plusplus    : true,\n\n    /**\n     * This option suppresses warnings about the `__proto__` property.\n     */\n    proto       : true,\n\n    /**\n     * This option suppresses warnings about the use of script-targeted\n     * URLs—such as `javascript:...`.\n     */\n    scripturl   : true,\n\n    /**\n     * This option suppresses warnings about using `[]` notation when it can be\n     * expressed in dot notation: `person['name']` vs. `person.name`.\n     *\n     * @deprecated JSHint is limiting its scope to issues of code correctness.\n     *             If you would like to enforce rules relating to code style,\n     *             check out [the JSCS\n     *             project](https://github.com/jscs-dev/node-jscs).\n     */\n    sub         : true,\n\n    /**\n     * This option suppresses warnings about \"weird\" constructions like\n     * `new function () { ... }` and `new Object;`. Such constructions are\n     * sometimes used to produce singletons in JavaScript:\n     *\n     *     var singleton = new function() {\n     *       var privateVar;\n     *\n     *       this.publicMethod  = function () {}\n     *       this.publicMethod2 = function () {}\n     *     };\n     */\n    supernew    : true,\n\n    /**\n     * This option suppresses most of the warnings about possibly unsafe line\n     * breakings in your code. It doesn't suppress warnings about comma-first\n     * coding style. To suppress those you have to use `laxcomma` (see below).\n     *\n     * @deprecated JSHint is limiting its scope to issues of code correctness.\n     *             If you would like to enforce rules relating to code style,\n     *             check out [the JSCS\n     *             project](https://github.com/jscs-dev/node-jscs).\n     */\n    laxbreak    : true,\n\n    /**\n     * This option suppresses warnings about comma-first coding style:\n     *\n     *     var obj = {\n     *         name: 'Anton'\n     *       , handle: 'valueof'\n     *       , role: 'SW Engineer'\n     *     };\n     *\n     * @deprecated JSHint is limiting its scope to issues of code correctness.\n     *             If you would like to enforce rules relating to code style,\n     *             check out [the JSCS\n     *             project](https://github.com/jscs-dev/node-jscs).\n     */\n    laxcomma    : true,\n\n    /**\n     * This option suppresses warnings about possible strict violations when\n     * the code is running in strict mode and you use `this` in a\n     * non-constructor function. You should use this option—in a function scope\n     * only—when you are positive that your use of `this` is valid in the\n     * strict mode (for example, if you call your function using\n     * `Function.call`).\n     *\n     * **Note:** This option can be used only inside of a function scope.\n     * JSHint will fail with an error if you will try to set this option\n     * globally.\n     */\n    validthis   : true,\n\n    /**\n     * This option suppresses warnings about the use of the `with` statement.\n     * The semantics of the `with` statement can cause confusion among\n     * developers and accidental definition of global variables.\n     *\n     * More info:\n     *\n     * * [with Statement Considered\n     *   Harmful](http://yuiblog.com/blog/2006/04/11/with-statement-considered-harmful/)\n     */\n    withstmt    : true,\n\n    /**\n     * This options tells JSHint that your code uses Mozilla JavaScript\n     * extensions. Unless you develop specifically for the Firefox web browser\n     * you don't need this option.\n     *\n     * More info:\n     *\n     * * [New in JavaScript\n     *   1.7](https://developer.mozilla.org/en-US/docs/JavaScript/New_in_JavaScript/1.7)\n     */\n    moz         : true,\n\n    /**\n     * This option suppresses warnings about generator functions with no\n     * `yield` statement in them.\n     */\n    noyield     : true,\n\n    /**\n     * This option suppresses warnings about `== null` comparisons. Such\n     * comparisons are often useful when you want to check if a variable is\n     * `null` or `undefined`.\n     */\n    eqnull      : true,\n\n    /**\n     * This option suppresses warnings about missing semicolons, but only when\n     * the semicolon is omitted for the last statement in a one-line block:\n     *\n     *     var name = (function() { return 'Anton' }());\n     *\n     * This is a very niche use case that is useful only when you use automatic\n     * JavaScript code generators.\n     */\n    lastsemic   : true,\n\n    /**\n     * This option suppresses warnings about functions inside of loops.\n     * Defining functions inside of loops can lead to bugs such as this one:\n     *\n     *     var nums = [];\n     *\n     *     for (var i = 0; i < 10; i++) {\n     *       nums[i] = function (j) {\n     *         return i + j;\n     *       };\n     *     }\n     *\n     *     nums[0](2); // Prints 12 instead of 2\n     *\n     * To fix the code above you need to copy the value of `i`:\n     *\n     *     var nums = [];\n     *\n     *     for (var i = 0; i < 10; i++) {\n     *       (function (i) {\n     *         nums[i] = function (j) {\n     *             return i + j;\n     *         };\n     *       }(i));\n     *     }\n     */\n    loopfunc    : true,\n\n    /**\n     * This option suppresses warnings about the use of expressions where\n     * normally you would expect to see assignments or function calls. Most of\n     * the time, such code is a typo. However, it is not forbidden by the spec\n     * and that's why this warning is optional.\n     */\n    expr        : true,\n\n    /**\n     * This option tells JSHint that your code uses ECMAScript 6 specific\n     * syntax. Note that not all browsers implement these features.\n     *\n     * More info:\n     *\n     * * [Specification for ECMAScript\n     *   6](http://www.ecma-international.org/ecma-262/6.0/index.html)\n     *\n     * @deprecated Use `esversion: 6` instead.\n     */\n    esnext      : true,\n\n    /**\n     * This option tells JSHint that your code uses ES3 array elision elements,\n     * or empty elements (for example, `[1, , , 4, , , 7]`).\n     */\n    elision     : true,\n  },\n\n  // Third party globals\n  environments: {\n\n    /**\n     * This option defines globals exposed by the\n     * [MooTools](http://mootools.net/) JavaScript framework.\n     */\n    mootools    : true,\n\n    /**\n     * This option defines globals exposed by\n     * [CouchDB](http://couchdb.apache.org/). CouchDB is a document-oriented\n     * database that can be queried and indexed in a MapReduce fashion using\n     * JavaScript.\n     */\n    couch       : true,\n\n    /**\n     * This option defines globals exposed by [the Jasmine unit testing\n     * framework](https://jasmine.github.io/).\n     */\n    jasmine     : true,\n\n    /**\n     * This option defines globals exposed by the [jQuery](http://jquery.com/)\n     * JavaScript library.\n     */\n    jquery      : true,\n\n    /**\n     * This option defines globals available when your code is running inside\n     * of the Node runtime environment. [Node.js](http://nodejs.org/) is a\n     * server-side JavaScript environment that uses an asynchronous\n     * event-driven model. This option also skips some warnings that make sense\n     * in the browser environments but don't make sense in Node such as\n     * file-level `use strict` pragmas and `console.log` statements.\n     */\n    node        : true,\n\n    /**\n     * This option defines globals exposed by [the QUnit unit testing\n     * framework](http://qunitjs.com/).\n     */\n    qunit       : true,\n\n    /**\n     * This option defines globals available when your code is running inside\n     * of the Rhino runtime environment. [Rhino](http://www.mozilla.org/rhino/)\n     * is an open-source implementation of JavaScript written entirely in Java.\n     */\n    rhino       : true,\n\n    /**\n     * This option defines globals exposed by [the ShellJS\n     * library](http://documentup.com/arturadib/shelljs).\n     */\n    shelljs     : true,\n\n    /**\n     * This option defines globals exposed by the\n     * [Prototype](http://www.prototypejs.org/) JavaScript framework.\n     */\n    prototypejs : true,\n\n    /**\n     * This option defines globals exposed by the [YUI](http://yuilibrary.com/)\n     * JavaScript framework.\n     */\n    yui         : true,\n\n    /**\n     * This option defines globals exposed by the \"BDD\" and \"TDD\" UIs of the\n     * [Mocha unit testing framework](http://mochajs.org/).\n     */\n    mocha       : true,\n\n    /**\n     * This option informs JSHint that the input code describes an ECMAScript 6\n     * module. All module code is interpreted as strict mode code.\n     */\n    module      : true,\n\n    /**\n     * This option defines globals available when your code is running as a\n     * script for the [Windows Script\n     * Host](http://en.wikipedia.org/wiki/Windows_Script_Host).\n     */\n    wsh         : true,\n\n    /**\n     * This option defines globals available when your code is running inside\n     * of a Web Worker. [Web\n     * Workers](https://developer.mozilla.org/en/Using_web_workers) provide a\n     * simple means for web content to run scripts in background threads.\n     */\n    worker      : true,\n\n    /**\n     * This option defines non-standard but widely adopted globals such as\n     * `escape` and `unescape`.\n     */\n    nonstandard : true,\n\n    /**\n     * This option defines globals exposed by modern browsers: all the way from\n     * good old `document` and `navigator` to the HTML5 `FileReader` and other\n     * new developments in the browser world.\n     *\n     * **Note:** This option doesn't expose variables like `alert` or\n     * `console`. See option `devel` for more information.\n     */\n    browser     : true,\n\n    /**\n     * This option defines globals available when using [the Browserify\n     * tool](http://browserify.org/) to build a project.\n     */\n    browserify  : true,\n\n    /**\n     * This option defines globals that are usually used for logging poor-man's\n     * debugging: `console`, `alert`, etc. It is usually a good idea to not\n     * ship them in production because, for example, `console.log` breaks in\n     * legacy versions of Internet Explorer.\n     */\n    devel       : true,\n\n    /**\n     * This option defines globals exposed by the [Dojo\n     * Toolkit](http://dojotoolkit.org/).\n     */\n    dojo        : true,\n\n    /**\n     * This option defines globals for typed array constructors.\n     *\n     * More info:\n     *\n     * * [JavaScript typed\n     *   arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays)\n     */\n    typed       : true,\n\n    /**\n     * This option defines globals available when your core is running inside\n     * of the PhantomJS runtime environment. [PhantomJS](http://phantomjs.org/)\n     * is a headless WebKit scriptable with a JavaScript API. It has fast and\n     * native support for various web standards: DOM handling, CSS selector,\n     * JSON, Canvas, and SVG.\n     */\n    phantom     : true\n  },\n\n  // Obsolete options\n  obsolete: {\n    onecase     : true, // if one case switch statements should be allowed\n    regexp      : true, // if the . should not be allowed in regexp literals\n    regexdash   : true  // if unescaped first/last dash (-) inside brackets\n                        // should be tolerated\n  }\n};\n\n// These are the JSHint options that can take any value\n// (we use this object to detect invalid options)\nexports.val = {\n\n  /**\n   * This option lets you set the maximum length of a line.\n   *\n   * @deprecated JSHint is limiting its scope to issues of code correctness. If\n   *             you would like to enforce rules relating to code style, check\n   *             out [the JSCS project](https://github.com/jscs-dev/node-jscs).\n   */\n  maxlen       : false,\n\n  /**\n   * This option sets a specific tab width for your code.\n   *\n   * @deprecated JSHint is limiting its scope to issues of code correctness. If\n   *             you would like to enforce rules relating to code style, check\n   *             out [the JSCS project](https://github.com/jscs-dev/node-jscs).\n   */\n  indent       : false,\n\n  /**\n   * This options allows you to set the maximum amount of errors JSHint will\n   * produce before giving up. Default is 50.\n   */\n  maxerr       : false,\n\n  /**\n   * This option allows you to control which variables JSHint considers to be\n   * implicitly defined in the environment. Configure it with an array of\n   * string values. Prefixing a variable name with a hyphen (-) character will\n   * remove that name from the collection of predefined variables.\n   *\n   * JSHint will consider variables declared in this way to be read-only.\n   *\n   * This option cannot be specified in-line; it may only be used via the\n   * JavaScript API or from an external configuration file.\n   */\n  predef       : false,\n\n  /**\n   * This option can be used to specify a white list of global variables that\n   * are not formally defined in the source code. This is most useful when\n   * combined with the `undef` option in order to suppress warnings for\n   * project-specific global variables.\n   *\n   * Setting an entry to `true` enables reading and writing to that variable.\n   * Setting it to `false` will trigger JSHint to consider that variable\n   * read-only.\n   *\n   * See also the \"environment\" options: a set of options to be used as short\n   * hand for enabling global variables defined in common JavaScript\n   * environments.\n   *\n   * To configure `globals` within an individual file, see [Inline\n   * Configuration](http://jshint.com/docs/#inline-configuration).\n   */\n  globals      : false,\n\n  /**\n   * This option enforces the consistency of quotation marks used throughout\n   * your code. It accepts three values: `true` if you don't want to enforce\n   * one particular style but want some consistency, `\"single\"` if you want to\n   * allow only single quotes and `\"double\"` if you want to allow only double\n   * quotes.\n   *\n   * @deprecated JSHint is limiting its scope to issues of code correctness. If\n   *             you would like to enforce rules relating to code style, check\n   *             out [the JSCS project](https://github.com/jscs-dev/node-jscs).\n   */\n  quotmark     : false,\n\n  scope        : false,\n\n  /**\n   * This option lets you set the max number of statements allowed per function:\n   *\n   *     // jshint maxstatements:4\n   *\n   *     function main() {\n   *       var i = 0;\n   *       var j = 0;\n   *\n   *       // Function declarations count as one statement. Their bodies\n   *       // don't get taken into account for the outer function.\n   *       function inner() {\n   *         var i2 = 1;\n   *         var j2 = 1;\n   *\n   *         return i2 + j2;\n   *       }\n   *\n   *       j = i + j;\n   *       return j; // JSHint: Too many statements per function. (5)\n   *     }\n   */\n  maxstatements: false,\n\n  /**\n   * This option lets you control how nested do you want your blocks to be:\n   *\n   *     // jshint maxdepth:2\n   *\n   *     function main(meaning) {\n   *       var day = true;\n   *\n   *       if (meaning === 42) {\n   *         while (day) {\n   *           shuffle();\n   *\n   *           if (tired) { // JSHint: Blocks are nested too deeply (3).\n   *               sleep();\n   *           }\n   *         }\n   *       }\n   *     }\n   */\n  maxdepth     : false,\n\n  /**\n   * This option lets you set the max number of formal parameters allowed per\n   * function:\n   *\n   *     // jshint maxparams:3\n   *\n   *     function login(request, onSuccess) {\n   *       // ...\n   *     }\n   *\n   *     // JSHint: Too many parameters per function (4).\n   *     function logout(request, isManual, whereAmI, onSuccess) {\n   *       // ...\n   *     }\n   */\n  maxparams    : false,\n\n  /**\n   * This option lets you control cyclomatic complexity throughout your code.\n   * Cyclomatic complexity measures the number of linearly independent paths\n   * through a program's source code. Read more about [cyclomatic complexity on\n   * Wikipedia](http://en.wikipedia.org/wiki/Cyclomatic_complexity).\n   */\n  maxcomplexity: false,\n\n  /**\n   * This option suppresses warnings about variable shadowing i.e. declaring a\n   * variable that had been already declared somewhere in the outer scope.\n   *\n   * - \"inner\"  - check for variables defined in the same scope only\n   * - \"outer\"  - check for variables defined in outer scopes as well\n   * - false    - same as inner\n   * - true     - allow variable shadowing\n   */\n  shadow       : false,\n\n  /**\n   * This option requires the code to run in ECMAScript 5's strict mode.\n   * [Strict mode](https://developer.mozilla.org/en/JavaScript/Strict_mode)\n   * is a way to opt in to a restricted variant of JavaScript. Strict mode\n   * eliminates some JavaScript pitfalls that didn't cause errors by changing\n   * them to produce errors.  It also fixes mistakes that made it difficult\n   * for the JavaScript engines to perform certain optimizations.\n   *\n   * - \"global\"  - there must be a `\"use strict\";` directive at global level\n   * - \"implied\" - lint the code as if there is the `\"use strict\";` directive\n   * - false     - disable warnings about strict mode\n   * - true      - there must be a `\"use strict\";` directive at function level;\n   *               this is preferable for scripts intended to be loaded in web\n   *               browsers directly because enabling strict mode globally\n   *               could adversely effect other scripts running on the same\n   *               page\n   */\n  strict      : true,\n\n  /**\n   * This option warns when you define and never use your variables. It is very\n   * useful for general code cleanup, especially when used in addition to\n   * `undef`.\n   *\n   *     // jshint unused:true\n   *\n   *     function test(a, b) {\n   *       var c, d = 2;\n   *\n   *       return a + d;\n   *     }\n   *\n   *     test(1, 2);\n   *\n   *     // Line 3: 'b' was defined but never used.\n   *     // Line 4: 'c' was defined but never used.\n   *\n   * In addition to that, this option will warn you about unused global\n   * variables declared via the `global` directive.\n   *\n   * When set to `true`, unused parameters that are followed by a used\n   * parameter will not produce warnings. This option can be set to `vars` to\n   * only check for variables, not function parameters, or `strict` to check\n   * all variables and parameters.\n   */\n  unused       : true,\n\n  /**\n   * This option prohibits the use of a variable before it was defined.\n   * JavaScript has function scope only and, in addition to that, all variables\n   * are always moved—or hoisted— to the top of the function. This behavior can\n   * lead to some very nasty bugs and that's why it is safer to always use\n   * variable only after they have been explicitly defined.\n   *\n   * Setting this option to \"nofunc\" will allow function declarations to be\n   * ignored.\n   *\n   * For more in-depth understanding of scoping and hoisting in JavaScript,\n   * read [JavaScript Scoping and\n   * Hoisting](http://www.adequatelygood.com/2010/2/JavaScript-Scoping-and-Hoisting)\n   * by Ben Cherry.\n   */\n  latedef      : false,\n\n  ignore       : false, // start/end ignoring lines of code, bypassing the lexer\n                        //   start    - start ignoring lines, including the current line\n                        //   end      - stop ignoring lines, starting on the next line\n                        //   line     - ignore warnings / errors for just a single line\n                        //              (this option does not bypass the lexer)\n\n  ignoreDelimiters: false, // array of start/end delimiters used to ignore\n                           // certain chunks from code\n\n  /**\n   * This option is used to specify the ECMAScript version to which the code\n   * must adhere. It can assume one of the following values:\n   *  - `3` - If you need your program to be executable\n   *    in older browsers—such as Internet Explorer 6/7/8/9—and other legacy\n   *    JavaScript environments\n   *  - `5` - To enable syntax first defined in [the ECMAScript 5.1\n   *    specification](http://www.ecma-international.org/ecma-262/5.1/index.html).\n   *    This includes allowing reserved keywords as object properties.\n   *  - `6` - To tell JSHint that your code uses [ECMAScript\n   *    6](http://www.ecma-international.org/ecma-262/6.0/index.html) specific\n   *    syntax. Note that not all browsers implement them.\n   *  - `7` - To enable language features introduced by [ECMAScript\n   *    7](https://www.ecma-international.org/ecma-262/7.0/index.html). Notable\n   *    additions: the exponentiation operator.\n   *  - `8` - To enable language features introduced by [ECMAScript\n   *    8](https://www.ecma-international.org/ecma-262/8.0/index.html). Notable\n   *    additions: async functions, shared memory, and atomics\n   *  - `9` - To enable language features introduced by [ECMAScript\n   *    9](https://www.ecma-international.org/ecma-262/9.0/index.html). Notable\n   *    additions: asynchronous iteration, rest/spread properties, and various\n   *    RegExp extensions\n   *  - `10` - To enable language features introduced by [ECMAScript\n   *    10](https://www.ecma-international.org/ecma-262/10.0/index.html).\n   *    Notable additions: optional catch bindings.\n   *  - `11` - To enable language features introduced by ECMAScript 11. Notable\n   *    additions: \"export * as ns from 'module'\", `import.meta`, the nullish\n   *    coalescing operator, and optional chaining, and dynamic import.\n   */\n  esversion: 5\n};\n\n/**\n * Unstable options allow control for parsing and linting of proposed additions\n * to the JavaScript language. Just like the language features they describe,\n * the presence and behavior of these options is volatile; JSHint reserves the\n * right to remove or modify them between major version releases.\n */\nexports.unstable = {\n};\n\n// These are JSHint boolean options which are shared with JSLint\n// where the definition in JSHint is opposite JSLint\nexports.inverted = {\n  bitwise : true,\n  forin   : true,\n  newcap  : true,\n  plusplus: true,\n  regexp  : true,\n  undef   : true,\n\n  // Inverted and renamed, use JSHint name here\n  eqeqeq  : true,\n  strict  : true\n};\n\nexports.validNames = Object.keys(exports.val)\n  .concat(Object.keys(exports.bool.relaxing))\n  .concat(Object.keys(exports.bool.enforcing))\n  .concat(Object.keys(exports.bool.obsolete))\n  .concat(Object.keys(exports.bool.environments))\n  .concat([\"unstable\"]);\n\nexports.unstableNames = Object.keys(exports.unstable);\n\n// These are JSHint boolean options which are shared with JSLint\n// where the name has been changed but the effect is unchanged\nexports.renamed = {\n  eqeq   : \"eqeqeq\",\n  windows: \"wsh\",\n  sloppy : \"strict\"\n};\n\nexports.removed = {\n  nomen: true,\n  onevar: true,\n  passfail: true,\n  white: true,\n  gcl: true,\n  smarttabs: true,\n  trailing: true\n};\n\n// Add options here which should not be automatically enforced by\n// `enforceall`.\nexports.noenforceall = {\n  varstmt: true,\n  strict: true,\n  regexpu: true\n};\n\n},{}],21:[function(require,module,exports){\n/**\n * This module defines a set of enum-like values intended for use as bit\n * \"flags\" during parsing. The ECMAScript grammar defines a number of such\n * \"production parameters\" to control how certain forms are parsed in context.\n * JSHint implements additional parameters to facilitate detection of lint\n * warnings.\n *\n * An equivalent implementation which described the context in terms of a\n * \"lookup table\" object would be more idiomatic for a JavaScript project like\n * JSHint. However, because the number of contexts scales with the number of\n * expressions in the input program, this would have non-negligible impact on\n * the process's memory footprint.\n */\nmodule.exports = {\n  /**\n   * Enabled when parsing expressions within ES2015 \"export\" declarations,\n   * allowing otherwise-unreferenced bindings to be considered \"used\".\n   */\n  export: 1,\n\n  /**\n   * Enabled when parsing expressions within the head of `for` statements,\n   * allowing to distinguish between `for-in` and \"C-style\" `for` statements.\n   */\n  noin: 2,\n\n  /**\n   * Enabled when the expression begins the statement, allowing the parser to\n   * correctly select between the null denotation (\"nud\") and first null\n   * denotation (\"fud\") parsing strategy.\n   */\n  initial: 4,\n\n  preAsync: 8,\n\n  async: 16,\n\n  /**\n   * Enabled when any exception thrown by the expression will be caught by a\n   * TryStatement.\n   */\n  tryClause: 32,\n\n  /**\n   * Enabled when parsing the body of a generator function.\n   */\n  yield: 64\n};\n\n},{}],22:[function(require,module,exports){\n/*\n * Regular expressions. Some of these are stupidly long.\n */\n\n/*jshint maxlen:1000 */\n\n\"use strict\";\n\n// Unsafe comment or string (ax)\nexports.unsafeString =\n  /@cc|<\\/?|script|\\]\\s*\\]|<\\s*!|&lt/i;\n\n// Characters in strings that need escaping (nx and nxg)\nexports.needEsc =\n  /[\\u0000-\\u001f&<\"\\/\\\\\\u007f-\\u009f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/;\n\nexports.needEscGlobal =\n  /[\\u0000-\\u001f&<\"\\/\\\\\\u007f-\\u009f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g;\n\n// Star slash (lx)\nexports.starSlash = /\\*\\//;\n\n// Identifier (ix)\nexports.identifier = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/;\n\n// JavaScript URL (jx)\nexports.javascriptURL = /^(?:javascript|jscript|ecmascript|vbscript|livescript)\\s*:/i;\n\n// Catches /* falls through */ comments (ft)\nexports.fallsThrough = /^\\s*falls?\\sthrough\\s*$/;\n\n// very conservative rule (eg: only one space between the start of the comment and the first character)\n// to relax the maxlen option\nexports.maxlenException = /^(?:(?:\\/\\/|\\/\\*|\\*) ?)?[^ ]+$/;\n\n// Node.js releases prior to version 8 include a version of the V8 engine which\n// incorrectly interprets the character class escape `\\s`. The following\n// regular expression may be replaced with `/\\s/` when JSHint removes support\n// for Node.js versions prior to 8.\n// Source:\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp\nexports.whitespace = /[ \\f\\n\\r\\t\\v\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]/;\n\nexports.nonzeroDigit = /^[1-9]$/;\n\nexports.decimalDigit = /^[0-9]$/;\n\nexports.regexpSyntaxChars = /[\\^$\\\\.*+?()[\\]{}|]/;\n\nexports.regexpQuantifiers = /[*+?{]/;\n\nexports.regexpControlEscapes = /[fnrtv]/;\n\nexports.regexpCharClasses = /[dDsSwWpP]/;\n\n// Identifies the \"dot\" atom in regular expressions\nexports.regexpDot = /(^|[^\\\\])(\\\\\\\\)*\\./;\n\n},{}],23:[function(require,module,exports){\n\"use strict\";\n/**\n * A note on `__proto__`:\n *\n * This file uses ordinary objects to track identifiers that are observed in\n * the input source code. It creates these objects using `Object.create` so\n * that the tracking objects have no prototype, allowing the `__proto__`\n * property to be used to store a value *without* triggering the invocation of\n * the built-in `Object.prototype.__proto__` accessor method. Some environments\n * (e.g. PhantomJS) do not implement the correct semantics for property\n * enumeration. In those environments, methods like `Object.keys` and Lodash's\n * `values` do not include the property name. This file includes a number of\n * branches which ensure that JSHint behaves consistently in those\n * environments. The branches must be ignored by the test coverage verification\n * system because the workaround is not necessary in the environment where\n * coverage is verified (i.e. Node.js).\n */\n\nvar _      = require(\"lodash\");\nvar events = require(\"events\");\n\n// Used to denote membership in lookup tables (a primitive value such as `true`\n// would be silently rejected for the property name \"__proto__\" in some\n// environments)\nvar marker = {};\n\n/**\n * A factory function for creating scope managers. A scope manager tracks\n * bindings, detecting when variables are referenced (through \"usages\").\n *\n * @param {object} state - the global state object (see `state.js`)\n * @param {Array} predefined - a set of binding names for built-in bindings\n *                             provided by the environment\n * @param {object} exported - a hash for binding names that are intended to be\n *                            referenced in contexts beyond the current program\n *                            code\n * @param {object} declared - a hash for binding names that were defined as\n *                            global bindings via linting configuration\n *\n * @returns {object} - a scope manager\n */\nvar scopeManager = function(state, predefined, exported, declared) {\n\n  var _current;\n  var _scopeStack = [];\n\n  function _newScope(type) {\n    _current = {\n      \"(bindings)\": Object.create(null),\n      \"(usages)\": Object.create(null),\n      \"(labels)\": Object.create(null),\n      \"(parent)\": _current,\n      \"(type)\": type,\n      \"(params)\": (type === \"functionparams\" || type === \"catchparams\") ? [] : null\n    };\n    _scopeStack.push(_current);\n  }\n\n  _newScope(\"global\");\n  _current[\"(predefined)\"] = predefined;\n\n  var _currentFunctBody = _current; // this is the block after the params = function\n\n  var usedPredefinedAndGlobals = Object.create(null);\n  var impliedGlobals = Object.create(null);\n  var unuseds = [];\n  var esModuleExports = [];\n  var emitter = new events.EventEmitter();\n\n  function warning(code, token) {\n    emitter.emit(\"warning\", {\n      code: code,\n      token: token,\n      data: _.slice(arguments, 2)\n    });\n  }\n\n  function error(code, token) {\n    emitter.emit(\"warning\", {\n      code: code,\n      token: token,\n      data: _.slice(arguments, 2)\n    });\n  }\n\n  function _setupUsages(bindingName) {\n    if (!_current[\"(usages)\"][bindingName]) {\n      _current[\"(usages)\"][bindingName] = {\n        \"(modified)\": [],\n        \"(reassigned)\": [],\n        \"(tokens)\": []\n      };\n    }\n  }\n\n  var _getUnusedOption = function(unused_opt) {\n    if (unused_opt === undefined) {\n      unused_opt = state.option.unused;\n    }\n\n    if (unused_opt === true) {\n      unused_opt = \"last-param\";\n    }\n\n    return unused_opt;\n  };\n\n  var _warnUnused = function(name, tkn, type, unused_opt) {\n    var line = tkn.line;\n    var chr  = tkn.from;\n    var raw_name = tkn.raw_text || name;\n\n    unused_opt = _getUnusedOption(unused_opt);\n\n    var warnable_types = {\n      \"vars\": [\"var\"],\n      \"last-param\": [\"var\", \"param\"],\n      \"strict\": [\"var\", \"param\", \"last-param\"]\n    };\n\n    if (unused_opt) {\n      if (warnable_types[unused_opt] && warnable_types[unused_opt].indexOf(type) !== -1) {\n        warning(\"W098\", { line: line, from: chr }, raw_name);\n      }\n    }\n\n    // inconsistent - see gh-1894\n    if (unused_opt || type === \"var\") {\n      unuseds.push({\n        name: name,\n        line: line,\n        character: chr\n      });\n    }\n  };\n\n  /**\n   * Check the current scope for unused identifiers\n   */\n  function _checkForUnused() {\n    if (_current[\"(type)\"] !== \"functionparams\") {\n      var currentBindings = _current[\"(bindings)\"];\n      for (var bindingName in currentBindings) {\n        if (currentBindings[bindingName][\"(type)\"] !== \"exception\" &&\n          currentBindings[bindingName][\"(unused)\"]) {\n          _warnUnused(bindingName, currentBindings[bindingName][\"(token)\"], \"var\");\n        }\n      }\n      return;\n    }\n\n    // Check the current scope for unused parameters and issue warnings as\n    // necessary.\n    var params = _current[\"(params)\"];\n\n    var param = params.pop();\n    var unused_opt;\n\n    while (param) {\n      var binding = _current[\"(bindings)\"][param];\n\n      unused_opt = _getUnusedOption(state.funct[\"(unusedOption)\"]);\n\n      // 'undefined' is a special case for the common pattern where `undefined`\n      // is used as a formal parameter name to defend against global\n      // re-assignment, e.g.\n      //\n      //     (function(window, undefined) {\n      //     })();\n      if (param === \"undefined\")\n        return;\n\n      if (binding[\"(unused)\"]) {\n        _warnUnused(param, binding[\"(token)\"], \"param\", state.funct[\"(unusedOption)\"]);\n      } else if (unused_opt === \"last-param\") {\n        return;\n      }\n\n      param = params.pop();\n    }\n  }\n\n  /**\n   * Find the relevant binding's scope. The owning scope is located by first\n   * inspecting the current scope and then moving \"downward\" through the stack\n   * of scopes.\n   *\n   * @param {string} bindingName - the value of the identifier\n   *\n   * @returns {Object} - the scope in which the binding was found\n   */\n  function _getBinding(bindingName) {\n    for (var i = _scopeStack.length - 1 ; i >= 0; --i) {\n      var scopeBindings = _scopeStack[i][\"(bindings)\"];\n      if (scopeBindings[bindingName]) {\n        return scopeBindings;\n      }\n    }\n  }\n\n  /**\n   * Determine if a given binding name has been referenced within the current\n   * function or any function defined within.\n   *\n   * @param {string} bindingName - the value of the identifier\n   *\n   * @returns {boolean}\n   */\n  function usedSoFarInCurrentFunction(bindingName) {\n    for (var i = _scopeStack.length - 1; i >= 0; i--) {\n      var current = _scopeStack[i];\n      if (current[\"(usages)\"][bindingName]) {\n        return current[\"(usages)\"][bindingName];\n      }\n      if (current === _currentFunctBody) {\n        break;\n      }\n    }\n    return false;\n  }\n\n  function _checkOuterShadow(bindingName, token) {\n\n    // only check if shadow is outer\n    if (state.option.shadow !== \"outer\") {\n      return;\n    }\n\n    var isGlobal = _currentFunctBody[\"(type)\"] === \"global\",\n      isNewFunction = _current[\"(type)\"] === \"functionparams\";\n\n    var outsideCurrentFunction = !isGlobal;\n    for (var i = 0; i < _scopeStack.length; i++) {\n      var stackItem = _scopeStack[i];\n\n      if (!isNewFunction && _scopeStack[i + 1] === _currentFunctBody) {\n        outsideCurrentFunction = false;\n      }\n      if (outsideCurrentFunction && stackItem[\"(bindings)\"][bindingName]) {\n        warning(\"W123\", token, bindingName);\n      }\n      if (stackItem[\"(labels)\"][bindingName]) {\n        warning(\"W123\", token, bindingName);\n      }\n    }\n  }\n\n  function _latedefWarning(type, bindingName, token) {\n    var isFunction;\n\n    if (state.option.latedef) {\n      isFunction = type === \"function\" || type === \"generator function\" ||\n        type === \"async function\";\n\n      // if either latedef is strict and this is a function\n      //    or this is not a function\n      if ((state.option.latedef === true && isFunction) || !isFunction) {\n        warning(\"W003\", token, bindingName);\n      }\n    }\n  }\n\n  var scopeManagerInst = {\n\n    on: function(names, listener) {\n      names.split(\" \").forEach(function(name) {\n        emitter.on(name, listener);\n      });\n    },\n\n    isPredefined: function(bindingName) {\n      return !this.has(bindingName) && _.has(_scopeStack[0][\"(predefined)\"], bindingName);\n    },\n\n    /**\n     * Create a new scope within the current scope. As the topmost value, the\n     * new scope will be interpreted as the current scope until it is\n     * exited--see the `unstack` method.\n     *\n     * @param {string} [type] - The type of the scope. Valid values are\n     *                          \"functionparams\", \"catchparams\" and\n     *                          \"functionouter\"\n     */\n    stack: function(type) {\n      var previousScope = _current;\n      _newScope(type);\n\n      if (!type && previousScope[\"(type)\"] === \"functionparams\") {\n\n        _current[\"(isFuncBody)\"] = true;\n        _currentFunctBody = _current;\n      }\n    },\n\n    /**\n     * Valldate all binding references and declarations in the current scope\n     * and set the next scope on the stack as the active scope.\n     */\n    unstack: function() {\n      // jshint proto: true\n      var subScope = _scopeStack.length > 1 ? _scopeStack[_scopeStack.length - 2] : null;\n      var isUnstackingFunctionBody = _current === _currentFunctBody,\n        isUnstackingFunctionParams = _current[\"(type)\"] === \"functionparams\",\n        isUnstackingFunctionOuter = _current[\"(type)\"] === \"functionouter\";\n\n      var i, j, isImmutable, isFunction;\n      var currentUsages = _current[\"(usages)\"];\n      var currentBindings = _current[\"(bindings)\"];\n      var usedBindingNameList = Object.keys(currentUsages);\n\n      // See comment, \"A note on `__proto__`\"\n      /* istanbul ignore if */\n      if (currentUsages.__proto__ && usedBindingNameList.indexOf(\"__proto__\") === -1) {\n        usedBindingNameList.push(\"__proto__\");\n      }\n\n      for (i = 0; i < usedBindingNameList.length; i++) {\n        var usedBindingName = usedBindingNameList[i];\n\n        var usage = currentUsages[usedBindingName];\n        var usedBinding = currentBindings[usedBindingName];\n        if (usedBinding) {\n          var usedBindingType = usedBinding[\"(type)\"];\n          isImmutable = usedBindingType === \"const\" || usedBindingType === \"import\";\n\n          if (usedBinding[\"(useOutsideOfScope)\"] && !state.option.funcscope) {\n            var usedTokens = usage[\"(tokens)\"];\n            for (j = 0; j < usedTokens.length; j++) {\n              // Keep the consistency of https://github.com/jshint/jshint/issues/2409\n              if (usedBinding[\"(function)\"] === usedTokens[j][\"(function)\"]) {\n                error(\"W038\", usedTokens[j], usedBindingName);\n              }\n            }\n          }\n\n          // mark the binding used\n          _current[\"(bindings)\"][usedBindingName][\"(unused)\"] = false;\n\n          // check for modifying a const\n          if (isImmutable && usage[\"(modified)\"]) {\n            for (j = 0; j < usage[\"(modified)\"].length; j++) {\n              error(\"E013\", usage[\"(modified)\"][j], usedBindingName);\n            }\n          }\n\n          isFunction = usedBindingType === \"function\" ||\n            usedBindingType === \"generator function\" ||\n            usedBindingType === \"async function\";\n\n          // check for re-assigning a function declaration\n          if ((isFunction || usedBindingType === \"class\") && usage[\"(reassigned)\"]) {\n            for (j = 0; j < usage[\"(reassigned)\"].length; j++) {\n              if (!usage[\"(reassigned)\"][j].ignoreW021) {\n                warning(\"W021\", usage[\"(reassigned)\"][j], usedBindingName, usedBindingType);\n              }\n            }\n          }\n          continue;\n        }\n\n        if (subScope) {\n          var bindingType = this.bindingtype(usedBindingName);\n          isImmutable = bindingType === \"const\" ||\n            (bindingType === null && _scopeStack[0][\"(predefined)\"][usedBindingName] === false);\n          if (isUnstackingFunctionOuter && !isImmutable) {\n            if (!state.funct[\"(outerMutables)\"]) {\n              state.funct[\"(outerMutables)\"] = [];\n            }\n            state.funct[\"(outerMutables)\"].push(usedBindingName);\n          }\n\n          // not exiting the global scope, so copy the usage down in case its an out of scope usage\n          if (!subScope[\"(usages)\"][usedBindingName]) {\n            subScope[\"(usages)\"][usedBindingName] = usage;\n            if (isUnstackingFunctionBody) {\n              subScope[\"(usages)\"][usedBindingName][\"(onlyUsedSubFunction)\"] = true;\n            }\n          } else {\n            var subScopeUsage = subScope[\"(usages)\"][usedBindingName];\n            subScopeUsage[\"(modified)\"] = subScopeUsage[\"(modified)\"].concat(usage[\"(modified)\"]);\n            subScopeUsage[\"(tokens)\"] = subScopeUsage[\"(tokens)\"].concat(usage[\"(tokens)\"]);\n            subScopeUsage[\"(reassigned)\"] =\n              subScopeUsage[\"(reassigned)\"].concat(usage[\"(reassigned)\"]);\n          }\n        } else {\n          // this is exiting global scope, so we finalise everything here - we are at the end of the file\n          if (typeof _current[\"(predefined)\"][usedBindingName] === \"boolean\") {\n\n            // remove the declared token, so we know it is used\n            delete declared[usedBindingName];\n\n            // note it as used so it can be reported\n            usedPredefinedAndGlobals[usedBindingName] = marker;\n\n            // check for re-assigning a read-only (set to false) predefined\n            if (_current[\"(predefined)\"][usedBindingName] === false && usage[\"(reassigned)\"]) {\n              for (j = 0; j < usage[\"(reassigned)\"].length; j++) {\n                if (!usage[\"(reassigned)\"][j].ignoreW020) {\n                  warning(\"W020\", usage[\"(reassigned)\"][j]);\n                }\n              }\n            }\n          }\n          else {\n            // binding usage is not predefined and we have not found a declaration\n            // so report as undeclared\n            for (j = 0; j < usage[\"(tokens)\"].length; j++) {\n              var undefinedToken = usage[\"(tokens)\"][j];\n              // if its not a forgiven undefined (e.g. typof x)\n              if (!undefinedToken.forgiveUndef) {\n                // if undef is on and undef was on when the token was defined\n                if (state.option.undef && !undefinedToken.ignoreUndef) {\n                  warning(\"W117\", undefinedToken, usedBindingName);\n                }\n                if (impliedGlobals[usedBindingName]) {\n                  impliedGlobals[usedBindingName].line.push(undefinedToken.line);\n                } else {\n                  impliedGlobals[usedBindingName] = {\n                    name: usedBindingName,\n                    line: [undefinedToken.line]\n                  };\n                }\n              }\n            }\n          }\n        }\n      }\n\n      // if exiting the global scope, we can warn about declared globals that haven't been used yet\n      if (!subScope) {\n        Object.keys(declared)\n          .forEach(function(bindingNotUsed) {\n            _warnUnused(bindingNotUsed, declared[bindingNotUsed], \"var\");\n          });\n      }\n\n      // If this is not a function boundary, transfer function-scoped bindings to\n      // the parent block (a rough simulation of variable hoisting). Previously\n      // existing bindings in the parent block should take precedence so that\n      // prior usages are not discarded.\n      if (subScope && !isUnstackingFunctionBody &&\n        !isUnstackingFunctionParams && !isUnstackingFunctionOuter) {\n        var bindingNames = Object.keys(currentBindings);\n        for (i = 0; i < bindingNames.length; i++) {\n\n          var defBindingName = bindingNames[i];\n          var defBinding = currentBindings[defBindingName];\n\n          if (!defBinding[\"(blockscoped)\"] && defBinding[\"(type)\"] !== \"exception\") {\n            var shadowed = subScope[\"(bindings)\"][defBindingName];\n\n            // Do not overwrite a binding if it exists in the parent scope\n            // because it is shared by adjacent blocks. Copy the `unused`\n            // property so that any references found within the current block\n            // are counted toward that higher-level declaration.\n            if (shadowed) {\n              shadowed[\"(unused)\"] &= defBinding[\"(unused)\"];\n\n            // \"Hoist\" the variable to the parent block, decorating the binding\n            // so that future references, though technically valid, can be\n            // reported as \"out-of-scope\" in the absence of the `funcscope`\n            // option.\n            } else {\n              defBinding[\"(useOutsideOfScope)\"] =\n                // Do not warn about out-of-scope usages in the global scope\n                _currentFunctBody[\"(type)\"] !== \"global\" &&\n                // When a higher scope contains a binding for the binding, the\n                // binding is a re-declaration and should not prompt \"used\n                // out-of-scope\" warnings.\n                !this.funct.has(defBindingName, { excludeCurrent: true });\n\n              subScope[\"(bindings)\"][defBindingName] = defBinding;\n            }\n\n            delete currentBindings[defBindingName];\n          }\n        }\n      }\n\n      _checkForUnused();\n\n      _scopeStack.pop();\n      if (isUnstackingFunctionBody) {\n        _currentFunctBody = _scopeStack[_.findLastIndex(_scopeStack, function(scope) {\n          // if function or if global (which is at the bottom so it will only return true if we call back)\n          return scope[\"(isFuncBody)\"] || scope[\"(type)\"] === \"global\";\n        })];\n      }\n\n      _current = subScope;\n    },\n\n    /**\n     * Add a function parameter to the current scope.\n     *\n     * @param {string} bindingName - the value of the identifier\n     * @param {Token} token\n     * @param {string} [type] - binding type; defaults to \"param\"\n     */\n    addParam: function(bindingName, token, type) {\n      type = type || \"param\";\n\n      if (type === \"exception\") {\n        // if defined in the current function\n        var previouslyDefinedBindingType = this.funct.bindingtype(bindingName);\n        if (previouslyDefinedBindingType && previouslyDefinedBindingType !== \"exception\") {\n          // and has not been used yet in the current function scope\n          if (!state.option.node) {\n            warning(\"W002\", state.tokens.next, bindingName);\n          }\n        }\n\n        if (state.isStrict() && (bindingName === \"arguments\" || bindingName === \"eval\")) {\n          warning(\"E008\", token);\n        }\n      }\n\n      // The variable was declared in the current scope\n      if (_.has(_current[\"(bindings)\"], bindingName)) {\n        _current[\"(bindings)\"][bindingName].duplicated = true;\n\n      // The variable was declared in an outer scope\n      } else {\n        // if this scope has the variable defined, it's a re-definition error\n        _checkOuterShadow(bindingName, token);\n\n        _current[\"(bindings)\"][bindingName] = {\n          \"(type)\" : type,\n          \"(token)\": token,\n          \"(unused)\": true };\n\n        _current[\"(params)\"].push(bindingName);\n      }\n\n      if (_.has(_current[\"(usages)\"], bindingName)) {\n        var usage = _current[\"(usages)\"][bindingName];\n        // if its in a sub function it is not necessarily an error, just latedef\n        if (usage[\"(onlyUsedSubFunction)\"]) {\n          _latedefWarning(type, bindingName, token);\n        } else {\n          // this is a clear illegal usage, but not a syntax error, so emit a\n          // warning and not an error\n          warning(\"W003\", token, bindingName);\n        }\n      }\n    },\n\n    validateParams: function(isArrow) {\n      var isStrict = state.isStrict();\n      var currentFunctParamScope = _currentFunctBody[\"(parent)\"];\n      // From ECMAScript 2017:\n      //\n      // > 14.1.2Static Semantics: Early Errors\n      // >\n      // > [...]\n      // > - It is a Syntax Error if IsSimpleParameterList of\n      // >   FormalParameterList is false and BoundNames of FormalParameterList\n      // >   contains any duplicate elements.\n      var isSimple = state.funct['(hasSimpleParams)'];\n      // Method definitions are defined in terms of UniqueFormalParameters, so\n      // they cannot support duplicate parameter names regardless of strict\n      // mode.\n      var isMethod = state.funct[\"(method)\"];\n\n      if (!currentFunctParamScope[\"(params)\"]) {\n        /* istanbul ignore next */\n        return;\n      }\n\n      currentFunctParamScope[\"(params)\"].forEach(function(bindingName) {\n        var binding = currentFunctParamScope[\"(bindings)\"][bindingName];\n\n        if (binding.duplicated) {\n          if (isStrict || isArrow || isMethod || !isSimple) {\n            warning(\"E011\", binding[\"(token)\"], bindingName);\n          } else if (state.option.shadow !== true) {\n            warning(\"W004\", binding[\"(token)\"], bindingName);\n          }\n        }\n\n        if (isStrict && (bindingName === \"arguments\" || bindingName === \"eval\")) {\n          warning(\"E008\", binding[\"(token)\"]);\n        }\n      });\n    },\n\n    getUsedOrDefinedGlobals: function() {\n      // jshint proto: true\n      var list = Object.keys(usedPredefinedAndGlobals);\n\n      // See comment, \"A note on `__proto__`\"\n      /* istanbul ignore if */\n      if (usedPredefinedAndGlobals.__proto__ === marker &&\n        list.indexOf(\"__proto__\") === -1) {\n        list.push(\"__proto__\");\n      }\n\n      return list;\n    },\n\n    /**\n     * Get an array of implied globals\n     *\n     * @returns {Array.<{ name: string, line: Array.<number>}>}\n     */\n    getImpliedGlobals: function() {\n      // jshint proto: true\n      var values = _.values(impliedGlobals);\n      var hasProto = false;\n\n      // See comment, \"A note on `__proto__`\"\n      if (impliedGlobals.__proto__) {\n        hasProto = values.some(function(value) {\n          return value.name === \"__proto__\";\n        });\n\n        /* istanbul ignore if */\n        if (!hasProto) {\n          values.push(impliedGlobals.__proto__);\n        }\n      }\n\n      return values;\n    },\n\n    /**\n     * Get an array of objects describing unused bindings.\n     *\n     * @returns {Array<Object>}\n     */\n    getUnuseds: function() {\n      return unuseds;\n    },\n\n    /**\n     * Determine if a given name has been defined in the current scope or any\n     * lower scope.\n     *\n     * @param {string} bindingName - the value of the identifier\n     *\n     * @return {boolean}\n     */\n    has: function(bindingName) {\n      return Boolean(_getBinding(bindingName));\n    },\n\n    /**\n     * Retrieve binding described by `bindingName` or null\n     *\n     * @param {string} bindingName - the value of the identifier\n     *\n     * @returns {string|null} - the type of the binding or `null` if no such\n     *                          binding exists\n     */\n    bindingtype: function(bindingName) {\n      var scopeBindings = _getBinding(bindingName);\n      if (scopeBindings) {\n        return scopeBindings[bindingName][\"(type)\"];\n      }\n      return null;\n    },\n\n    /**\n     * For the exported options, indicating a variable is used outside the file\n     *\n     * @param {string} bindingName - the value of the identifier\n     */\n    addExported: function(bindingName) {\n      var globalBindings = _scopeStack[0][\"(bindings)\"];\n      if (_.has(declared, bindingName)) {\n        // remove the declared token, so we know it is used\n        delete declared[bindingName];\n      } else if (_.has(globalBindings, bindingName)) {\n        globalBindings[bindingName][\"(unused)\"] = false;\n      } else {\n        for (var i = 1; i < _scopeStack.length; i++) {\n          var scope = _scopeStack[i];\n          // if `scope.(type)` is not defined, it is a block scope\n          if (!scope[\"(type)\"]) {\n            if (_.has(scope[\"(bindings)\"], bindingName) &&\n                !scope[\"(bindings)\"][bindingName][\"(blockscoped)\"]) {\n              scope[\"(bindings)\"][bindingName][\"(unused)\"] = false;\n              return;\n            }\n          } else {\n            break;\n          }\n        }\n        exported[bindingName] = true;\n      }\n    },\n\n    /**\n     * Mark a binding as \"exported\" by an ES2015 module\n     *\n     * @param {string} bindingName - the value of the identifier\n     * @param {object} token\n     */\n    setExported: function(localName, exportName) {\n      if (exportName) {\n        if (esModuleExports.indexOf(exportName.value) > -1) {\n          error(\"E069\", exportName, exportName.value);\n        }\n\n        esModuleExports.push(exportName.value);\n      }\n\n      if (localName) {\n        this.block.use(localName.value, localName);\n      }\n    },\n\n    /**\n     * Mark a binding as \"initialized.\" This is necessary to enforce the\n     * \"temporal dead zone\" (TDZ) of block-scoped bindings which are not\n     * hoisted.\n     *\n     * @param {string} bindingName - the value of the identifier\n     */\n    initialize: function(bindingName) {\n      if (_current[\"(bindings)\"][bindingName]) {\n        _current[\"(bindings)\"][bindingName][\"(initialized)\"] = true;\n      }\n    },\n\n    /**\n     * Create a new binding and add it to the current scope. Delegates to the\n     * internal `block.add` or `func.add` methods depending on the type.\n     * Produces warnings and errors as necessary.\n     *\n     * @param {string} bindingName\n     * @param {Object} opts\n     * @param {String} opts.type - the type of the binding e.g. \"param\", \"var\",\n     *                             \"let, \"const\", \"import\", \"function\",\n     *                             \"generator function\", \"async function\",\n     *                             \"async generator function\"\n     * @param {object} opts.token - the token pointing at the declaration\n     * @param {boolean} opts.initialized - whether the binding should be\n     *                                     created in an \"initialized\" state.\n     */\n    addbinding: function(bindingName, opts) {\n\n      var type  = opts.type;\n      var token = opts.token;\n      var isblockscoped = type === \"let\" || type === \"const\" ||\n        type === \"class\" || type === \"import\" || type === \"generator function\" ||\n        type === \"async function\" || type === \"async generator function\";\n      var ishoisted = type === \"function\" || type === \"generator function\" ||\n        type === \"async function\" || type === \"import\";\n      var isexported    = (isblockscoped ? _current : _currentFunctBody)[\"(type)\"] === \"global\" &&\n                          _.has(exported, bindingName);\n\n      // outer shadow check (inner is only on non-block scoped)\n      _checkOuterShadow(bindingName, token);\n\n      if (state.isStrict() && (bindingName === \"arguments\" || bindingName === \"eval\")) {\n        warning(\"E008\", token);\n      }\n\n      if (isblockscoped) {\n\n        var declaredInCurrentScope = _current[\"(bindings)\"][bindingName];\n        // for block scoped variables, params are seen in the current scope as the root function\n        // scope, so check these too.\n        if (!declaredInCurrentScope && _current === _currentFunctBody &&\n          _current[\"(type)\"] !== \"global\") {\n          declaredInCurrentScope = !!_currentFunctBody[\"(parent)\"][\"(bindings)\"][bindingName];\n        }\n\n        // if its not already defined (which is an error, so ignore) and is used in TDZ\n        if (!declaredInCurrentScope && _current[\"(usages)\"][bindingName]) {\n          var usage = _current[\"(usages)\"][bindingName];\n          // if its in a sub function it is not necessarily an error, just latedef\n          if (usage[\"(onlyUsedSubFunction)\"] || ishoisted) {\n            _latedefWarning(type, bindingName, token);\n          } else if (!ishoisted) {\n            // this is a clear illegal usage for block scoped variables\n            warning(\"E056\", token, bindingName, type);\n          }\n        }\n\n        // If this scope has already declared a binding with the same name,\n        // then this represents a redeclaration error if:\n        //\n        // 1. it is a \"hoisted\" block-scoped binding within a block. For\n        //    instance: generator functions may be redeclared in the global\n        //    scope but not within block statements\n        // 2. this is not a \"hoisted\" block-scoped binding\n        if (declaredInCurrentScope &&\n          (!ishoisted || (_current[\"(type)\"] !== \"global\" || type === \"import\"))) {\n          warning(\"E011\", token, bindingName);\n        }\n        else if (state.option.shadow === \"outer\") {\n\n          // if shadow is outer, for block scope we want to detect any shadowing within this function\n          if (scopeManagerInst.funct.has(bindingName)) {\n            warning(\"W004\", token, bindingName);\n          }\n        }\n\n        scopeManagerInst.block.add(\n          bindingName, type, token, !isexported, opts.initialized\n        );\n\n      } else {\n\n        var declaredInCurrentFunctionScope = scopeManagerInst.funct.has(bindingName);\n\n        // check for late definition, ignore if already declared\n        if (!declaredInCurrentFunctionScope && usedSoFarInCurrentFunction(bindingName)) {\n          _latedefWarning(type, bindingName, token);\n        }\n\n        // defining with a var or a function when a block scope variable of the same name\n        // is in scope is an error\n        if (scopeManagerInst.funct.has(bindingName, { onlyBlockscoped: true })) {\n          warning(\"E011\", token, bindingName);\n        } else if (state.option.shadow !== true) {\n          // now since we didn't get any block scope variables, test for var/function\n          // shadowing\n          if (declaredInCurrentFunctionScope && bindingName !== \"__proto__\") {\n\n            // see https://github.com/jshint/jshint/issues/2400\n            if (_currentFunctBody[\"(type)\"] !== \"global\") {\n              warning(\"W004\", token, bindingName);\n            }\n          }\n        }\n\n        scopeManagerInst.funct.add(bindingName, type, token, !isexported);\n\n        if (_currentFunctBody[\"(type)\"] === \"global\" && !state.impliedClosure()) {\n          usedPredefinedAndGlobals[bindingName] = marker;\n        }\n      }\n    },\n\n    funct: {\n      /**\n       * Return the type of the provided binding given certain options\n       *\n       * @param {string} bindingName\n       * @param {Object=} [options]\n       * @param {boolean} [options.onlyBlockscoped] - only include block scoped\n       *                                              bindings\n       * @param {boolean} [options.excludeParams] - exclude the param scope\n       * @param {boolean} [options.excludeCurrent] - exclude the current scope\n       *\n       * @returns {String}\n       */\n      bindingtype: function(bindingName, options) {\n        var onlyBlockscoped = options && options.onlyBlockscoped;\n        var excludeParams = options && options.excludeParams;\n        var currentScopeIndex = _scopeStack.length - (options && options.excludeCurrent ? 2 : 1);\n        for (var i = currentScopeIndex; i >= 0; i--) {\n          var current = _scopeStack[i];\n          if (current[\"(bindings)\"][bindingName] &&\n            (!onlyBlockscoped || current[\"(bindings)\"][bindingName][\"(blockscoped)\"])) {\n            return current[\"(bindings)\"][bindingName][\"(type)\"];\n          }\n          var scopeCheck = excludeParams ? _scopeStack[ i - 1 ] : current;\n          if (scopeCheck && scopeCheck[\"(type)\"] === \"functionparams\") {\n            return null;\n          }\n        }\n        return null;\n      },\n\n      /**\n       * Determine whether a `break` statement label exists in the function\n       * scope.\n       *\n       * @param {string} labelName - the value of the identifier\n       *\n       * @returns {boolean}\n       */\n      hasLabel: function(labelName) {\n        for (var i = _scopeStack.length - 1; i >= 0; i--) {\n          var current = _scopeStack[i];\n\n          if (current[\"(labels)\"][labelName]) {\n            return true;\n          }\n          if (current[\"(type)\"] === \"functionparams\") {\n            return false;\n          }\n        }\n        return false;\n      },\n\n      /**\n       * Determine if a given name has been defined in the current function\n       * scope.\n       *\n       * @param {string} bindingName - the value of the identifier\n       * @param {object} options - options as supported by the\n       *                           `funct.bindingtype` method\n       *\n       * @return {boolean}\n       */\n      has: function(bindingName, options) {\n        return Boolean(this.bindingtype(bindingName, options));\n      },\n\n      /**\n       * Create a new function-scoped binding and add it to the current scope.\n       * See the `block.add` method for coresponding logic to create\n       * block-scoped bindings.\n       *\n       * @param {string} bindingName - the value of the identifier\n       * @param {string} type - the type of the binding; either \"function\" or\n       *                        \"var\"\n       * @param {object} tok - the token that triggered the definition\n       * @param {boolean} unused - `true` if the binding has not been\n       *                           referenced\n       */\n      add: function(bindingName, type, tok, unused) {\n        _current[\"(bindings)\"][bindingName] = {\n          \"(type)\" : type,\n          \"(token)\": tok,\n          \"(blockscoped)\": false,\n          \"(function)\": _currentFunctBody,\n          \"(unused)\": unused };\n      }\n    },\n\n    block: {\n\n      /**\n       * Determine whether the current block scope is the global scope.\n       *\n       * @returns Boolean\n       */\n      isGlobal: function() {\n        return _current[\"(type)\"] === \"global\";\n      },\n\n      /**\n       * Resolve a reference to a binding and mark the corresponding binding as\n       * \"used.\"\n       *\n       * @param {string} bindingName - the value of the identifier\n       * @param {object} token - the token value that triggered the reference\n       */\n      use: function(bindingName, token) {\n        // If the name resolves to a parameter of the current function, then do\n        // not store usage. This is because in cases such as the following:\n        //\n        //     function(a) {\n        //       var a;\n        //       a = a;\n        //     }\n        //\n        // the usage of `a` will resolve to the parameter, not to the unset\n        // variable binding.\n        var paramScope = _currentFunctBody[\"(parent)\"];\n        if (paramScope && paramScope[\"(bindings)\"][bindingName] &&\n          paramScope[\"(bindings)\"][bindingName][\"(type)\"] === \"param\") {\n\n          // then check its not declared by a block scope variable\n          if (!scopeManagerInst.funct.has(bindingName,\n                { excludeParams: true, onlyBlockscoped: true })) {\n            paramScope[\"(bindings)\"][bindingName][\"(unused)\"] = false;\n          }\n        }\n\n        if (token && (state.ignored.W117 || state.option.undef === false)) {\n          token.ignoreUndef = true;\n        }\n\n        _setupUsages(bindingName);\n\n        _current[\"(usages)\"][bindingName][\"(onlyUsedSubFunction)\"] = false;\n\n        if (token) {\n          token[\"(function)\"] = _currentFunctBody;\n          _current[\"(usages)\"][bindingName][\"(tokens)\"].push(token);\n        }\n\n        // Block-scoped bindings can't be used within their initializer due to\n        // \"temporal dead zone\" (TDZ) restrictions.\n        var binding = _current[\"(bindings)\"][bindingName];\n        if (binding && binding[\"(blockscoped)\"] && !binding[\"(initialized)\"]) {\n          error(\"E056\", token, bindingName, binding[\"(type)\"]);\n        }\n      },\n\n      reassign: function(bindingName, token) {\n        token.ignoreW020 = state.ignored.W020;\n        token.ignoreW021 = state.ignored.W021;\n\n        this.modify(bindingName, token);\n\n        _current[\"(usages)\"][bindingName][\"(reassigned)\"].push(token);\n      },\n\n      modify: function(bindingName, token) {\n\n        _setupUsages(bindingName);\n\n        _current[\"(usages)\"][bindingName][\"(onlyUsedSubFunction)\"] = false;\n        _current[\"(usages)\"][bindingName][\"(modified)\"].push(token);\n      },\n\n      /**\n       * Create a new block-scoped binding and add it to the current scope. See\n       * the `funct.add` method for coresponding logic to create\n       * function-scoped bindings.\n       *\n       * @param {string} bindingName - the value of the identifier\n       * @param {string} type - the type of the binding; one of \"class\",\n       *                        \"const\", \"function\", \"import\", or \"let\"\n       * @param {object} tok - the token that triggered the definition\n       * @param {boolean} unused - `true` if the binding has not been\n       *                           referenced\n       * @param {boolean} initialized - `true` if the binding has been\n       *                                initialized (as is the case with\n       *                                bindings created via `import`\n       *                                declarations)\n       */\n      add: function(bindingName, type, tok, unused, initialized) {\n        _current[\"(bindings)\"][bindingName] = {\n          \"(type)\" : type,\n          \"(token)\": tok,\n          \"(initialized)\": !!initialized,\n          \"(blockscoped)\": true,\n          \"(unused)\": unused };\n      },\n\n      addLabel: function(labelName, opts) {\n        var token = opts.token;\n        if (scopeManagerInst.funct.hasLabel(labelName)) {\n          warning(\"E011\", token, labelName);\n        }\n        else if (state.option.shadow === \"outer\") {\n          if (scopeManagerInst.funct.has(labelName)) {\n            warning(\"W004\", token, labelName);\n          } else {\n            _checkOuterShadow(labelName, token);\n          }\n        }\n        _current[\"(labels)\"][labelName] = token;\n      }\n    }\n  };\n  return scopeManagerInst;\n};\n\nmodule.exports = scopeManager;\n\n},{\"events\":11,\"lodash\":12}],24:[function(require,module,exports){\n\"use strict\";\nvar NameStack = require(\"./name-stack.js\");\n\nvar state = {\n  syntax: {},\n\n  /**\n   * Determine if the code currently being linted is strict mode code.\n   *\n   * @returns {boolean}\n   */\n  isStrict: function() {\n    return !!this.directive[\"use strict\"] || this.inClassBody ||\n      this.option.module || this.option.strict === \"implied\";\n  },\n\n  /**\n   * Determine if the current state warrants a warning for statements outside\n   * of strict mode code.\n   *\n   * While emitting warnings based on function scope would be more intuitive\n   * (and less noisy), JSHint observes statement-based semantics in order to\n   * preserve legacy behavior.\n   *\n   * This method does not take the state of the parser into account, making no\n   * distinction between global code and function code. Because the \"missing\n   * 'use strict'\" warning is *also* reported at function boundaries, this\n   * function interprets `strict` option values `true` and `undefined` as\n   * equivalent.\n   */\n  stmtMissingStrict: function() {\n    if (this.option.strict === \"global\") {\n      return true;\n    }\n\n    if (this.option.strict === false) {\n      return false;\n    }\n\n    if (this.option.globalstrict) {\n      return true;\n    }\n\n    return false;\n  },\n\n  allowsGlobalUsd: function() {\n    return this.option.strict === \"global\" || this.option.globalstrict ||\n      this.option.module || this.impliedClosure();\n  },\n\n  /**\n   * Determine if the current configuration describes an environment that is\n   * wrapped in an immediately-invoked function expression prior to evaluation.\n   *\n   * @returns {boolean}\n   */\n  impliedClosure: function() {\n    return this.option.node || this.option.phantom || this.option.browserify;\n  },\n\n  // Assumption: chronologically ES3 < ES5 < ES6 < Moz\n\n  inMoz: function() {\n    return this.option.moz;\n  },\n\n  /**\n   * Determine if constructs introduced in ECMAScript 11 should be accepted.\n   *\n   * @returns {boolean}\n   */\n  inES11: function() {\n    return this.esVersion >= 11;\n  },\n\n  /**\n   * Determine if constructs introduced in ECMAScript 10 should be accepted.\n   *\n   * @returns {boolean}\n   */\n  inES10: function() {\n    return this.esVersion >= 10;\n  },\n\n  /**\n   * Determine if constructs introduced in ECMAScript 9 should be accepted.\n   *\n   * @returns {boolean}\n   */\n  inES9: function() {\n    return this.esVersion >= 9;\n  },\n\n  /**\n   * Determine if constructs introduced in ECMAScript 8 should be accepted.\n   *\n   * @returns {boolean}\n   */\n  inES8: function() {\n    return this.esVersion >= 8;\n  },\n\n  /**\n   * Determine if constructs introduced in ECMAScript 7 should be accepted.\n   *\n   * @returns {boolean}\n   */\n  inES7: function() {\n    return this.esVersion >= 7;\n  },\n\n  /**\n   * Determine if constructs introduced in ECMAScript 6 should be accepted.\n   *\n   * @param {boolean} strict - When `true`, do not interpret the `moz` option\n   *                           as ECMAScript 6\n   *\n   * @returns {boolean}\n   */\n  inES6: function(strict) {\n    if (!strict && this.option.moz) {\n      return true;\n    }\n\n    return this.esVersion >= 6;\n  },\n\n  /**\n   * Determine if constructs introduced in ECMAScript 5 should be accepted.\n   *\n   * @returns {boolean}\n   */\n  inES5: function() {\n    return !this.esVersion || this.esVersion >= 5 || this.option.moz;\n  },\n\n  /**\n   * Determine the current version of the input language by inspecting the\n   * value of all ECMAScript-version-related options. This logic is necessary\n   * to ensure compatibility with deprecated options `es3`, `es5`, and\n   * `esnext`, and it may be drastically simplified when those options are\n   * removed.\n   *\n   * @returns {string|null} - the name of any incompatible option detected,\n   *                          `null` otherwise\n   */\n  inferEsVersion: function() {\n    var badOpt = null;\n\n    if (this.option.esversion) {\n      if (this.option.es3) {\n        badOpt = \"es3\";\n      } else if (this.option.es5) {\n        badOpt = \"es5\";\n      } else if (this.option.esnext) {\n        badOpt = \"esnext\";\n      }\n\n      if (badOpt) {\n        return badOpt;\n      }\n\n      if (this.option.esversion === 2015) {\n        this.esVersion = 6;\n      } else {\n        this.esVersion = this.option.esversion;\n      }\n    } else if (this.option.es3) {\n      this.esVersion = 3;\n    } else if (this.option.esnext) {\n      this.esVersion = 6;\n    }\n\n    return null;\n  },\n\n  reset: function() {\n    this.tokens = {\n      prev: null,\n      next: null,\n      curr: null\n    };\n\n    this.option = { unstable: {} };\n    this.esVersion = 5;\n    this.funct = null;\n    this.ignored = {};\n    /**\n     * A lookup table for active directives whose keys are the value of the\n     * directives and whose values are the tokens which enabled the directives.\n     */\n    this.directive = Object.create(null);\n    this.jsonMode = false;\n    this.lines = [];\n    this.tab = \"\";\n    this.cache = {}; // Node.JS doesn't have Map. Sniff.\n    this.ignoredLines = {};\n    this.forinifcheckneeded = false;\n    this.nameStack = new NameStack();\n    this.inClassBody = false;\n  }\n};\n\nexports.state = state;\n\n},{\"./name-stack.js\":19}],25:[function(require,module,exports){\n\"use strict\";\n\nexports.register = function(linter) {\n  // Check for properties named __proto__. This special property was\n  // deprecated and then re-introduced for ES6.\n\n  linter.on(\"Identifier\", function style_scanProto(data) {\n    if (linter.getOption(\"proto\")) {\n      return;\n    }\n\n    if (data.name === \"__proto__\") {\n      linter.warn(\"W103\", {\n        line: data.line,\n        char: data.char,\n        data: [ data.name, \"6\" ]\n      });\n    }\n  });\n\n  // Check for properties named __iterator__. This is a special property\n  // available only in browsers with JavaScript 1.7 implementation, but\n  // it is deprecated for ES6\n\n  linter.on(\"Identifier\", function style_scanIterator(data) {\n    if (linter.getOption(\"iterator\")) {\n      return;\n    }\n\n    if (data.name === \"__iterator__\") {\n      linter.warn(\"W103\", {\n        line: data.line,\n        char: data.char,\n        data: [ data.name ]\n      });\n    }\n  });\n\n  // Check that all identifiers are using camelCase notation.\n  // Exceptions: names like MY_VAR and _myVar.\n\n  linter.on(\"Identifier\", function style_scanCamelCase(data) {\n    if (!linter.getOption(\"camelcase\")) {\n      return;\n    }\n\n    if (data.name.replace(/^_+|_+$/g, \"\").indexOf(\"_\") > -1 && !data.name.match(/^[A-Z0-9_]*$/)) {\n      linter.warn(\"W106\", {\n        line: data.line,\n        char: data.char,\n        data: [ data.name ]\n      });\n    }\n  });\n\n  // Enforce consistency in style of quoting.\n\n  linter.on(\"String\", function style_scanQuotes(data) {\n    var quotmark = linter.getOption(\"quotmark\");\n    var code;\n\n    if (!quotmark) {\n      return;\n    }\n\n    // If quotmark is set to 'single' warn about all double-quotes.\n\n    if (quotmark === \"single\" && data.quote !== \"'\") {\n      code = \"W109\";\n    }\n\n    // If quotmark is set to 'double' warn about all single-quotes.\n\n    if (quotmark === \"double\" && data.quote !== \"\\\"\") {\n      code = \"W108\";\n    }\n\n    // If quotmark is set to true, remember the first quotation style\n    // and then warn about all others.\n\n    if (quotmark === true) {\n      if (!linter.getCache(\"quotmark\")) {\n        linter.setCache(\"quotmark\", data.quote);\n      }\n\n      if (linter.getCache(\"quotmark\") !== data.quote) {\n        code = \"W110\";\n      }\n    }\n\n    if (code) {\n      linter.warn(code, {\n        line: data.line,\n        char: data.char,\n      });\n    }\n  });\n\n  linter.on(\"Number\", function style_scanNumbers(data) {\n    if (data.value.charAt(0) === \".\") {\n      // Warn about a leading decimal point.\n      linter.warn(\"W008\", {\n        line: data.line,\n        char: data.char,\n        data: [ data.value ]\n      });\n    }\n\n    if (data.value.substr(data.value.length - 1) === \".\") {\n      // Warn about a trailing decimal point.\n      linter.warn(\"W047\", {\n        line: data.line,\n        char: data.char,\n        data: [ data.value ]\n      });\n    }\n\n    if (/^00+/.test(data.value)) {\n      // Multiple leading zeroes.\n      linter.warn(\"W046\", {\n        line: data.line,\n        char: data.char,\n        data: [ data.value ]\n      });\n    }\n  });\n\n  // Warn about script URLs.\n\n  linter.on(\"String\", function style_scanJavaScriptURLs(data) {\n    var re = /^(?:javascript|jscript|ecmascript|vbscript|livescript)\\s*:/i;\n\n    if (linter.getOption(\"scripturl\")) {\n      return;\n    }\n\n    if (re.test(data.value)) {\n      linter.warn(\"W107\", {\n        line: data.line,\n        char: data.char\n      });\n    }\n  });\n};\n\n},{}],26:[function(require,module,exports){\n/*\n * Determine whether a given string is a valid UnicodePropertyValueExpression.\n */\n\n\"use strict\";\n\nmodule.exports = function validate(sequence) {\n  var equalSignIndex = sequence.indexOf(\"=\");\n\n  if (equalSignIndex === -1) {\n    return sequence in names.binary || sequence in values.general;\n  }\n\n  var name = sequence.substr(0, equalSignIndex);\n  var value = sequence.substr(equalSignIndex + 1);\n\n  if (name === \"General_Category\" || name === \"gc\") {\n    return value in values.general;\n  } if (name === \"Script\" || name === \"sc\" || name === \"Script_Extensions\" || name === \"scx\") {\n    return value in values.script;\n  }\n\n  return false;\n};\n\n\nvar names = {\n  nonBinary: Object.create(null),\n  binary: Object.create(null)\n};\nvar values = {\n  general: Object.create(null),\n  script: Object.create(null)\n};\n\nvar nb = names.nonBinary;\nnb.General_Category = true;\nnb.gc = true;\nnb.Script = true;\nnb.sc = true;\nnb.Script_Extensions = true;\nnb.scx = true;\n\nvar b = names.binary;\nb.ASCII = true;\nb.ASCII_Hex_Digit = true;\nb.AHex = true;\nb.Alphabetic = true;\nb.Alpha = true;\nb.Any = true;\nb.Assigned = true;\nb.Bidi_Control = true;\nb.Bidi_C = true;\nb.Bidi_Mirrored = true;\nb.Bidi_M = true;\nb.Case_Ignorable = true;\nb.CI = true;\nb.Cased = true;\nb.Changes_When_Casefolded = true;\nb.CWCF = true;\nb.Changes_When_Casemapped = true;\nb.CWCM = true;\nb.Changes_When_Lowercased = true;\nb.CWL = true;\nb.Changes_When_NFKC_Casefolded = true;\nb.CWKCF = true;\nb.Changes_When_Titlecased = true;\nb.CWT = true;\nb.Changes_When_Uppercased = true;\nb.CWU = true;\nb.Dash = true;\nb.Default_Ignorable_Code_Point = true;\nb.DI = true;\nb.Deprecated = true;\nb.Dep = true;\nb.Diacritic = true;\nb.Dia = true;\nb.Emoji = true;\nb.Emoji_Component = true;\nb.EComp = true;\nb.Emoji_Modifier = true;\nb.EMod = true;\nb.Emoji_Modifier_Base = true;\nb.EBase = true;\nb.Emoji_Presentation = true;\nb.EPres = true;\nb.Extended_Pictographic = true;\nb.ExtPict = true;\nb.Extender = true;\nb.Ext = true;\nb.Grapheme_Base = true;\nb.Gr_Base = true;\nb.Grapheme_Extend = true;\nb.Gr_Ext = true;\nb.Hex_Digit = true;\nb.Hex = true;\nb.IDS_Binary_Operator = true;\nb.IDSB = true;\nb.IDS_Trinary_Operator = true;\nb.IDST = true;\nb.ID_Continue = true;\nb.IDC = true;\nb.ID_Start = true;\nb.IDS = true;\nb.Ideographic = true;\nb.Ideo = true;\nb.Join_Control = true;\nb.Join_C = true;\nb.Logical_Order_Exception = true;\nb.LOE = true;\nb.Lowercase = true;\nb.Lower = true;\nb.Math = true;\nb.Noncharacter_Code_Point = true;\nb.NChar = true;\nb.Pattern_Syntax = true;\nb.Pat_Syn = true;\nb.Pattern_White_Space = true;\nb.Pat_WS = true;\nb.Quotation_Mark = true;\nb.QMark = true;\nb.Radical = true;\nb.Regional_Indicator = true;\nb.RI = true;\nb.Sentence_Terminal = true;\nb.STerm = true;\nb.Soft_Dotted = true;\nb.SD = true;\nb.Terminal_Punctuation = true;\nb.Term = true;\nb.Unified_Ideograph = true;\nb.UIdeo = true;\nb.Uppercase = true;\nb.Upper = true;\nb.Variation_Selector = true;\nb.VS = true;\nb.White_Space = true;\nb.space = true;\nb.XID_Continue = true;\nb.XIDC = true;\nb.XID_Start = true;\nb.XIDS = true;\n\nvar g = values.general;\ng.Cased_Letter = true;\ng.LC = true;\ng.Close_Punctuation = true;\ng.Pe = true;\ng.Connector_Punctuation = true;\ng.Pc = true;\ng.Control = true;\ng.Cc = true;\ng.cntrl = true;\ng.Currency_Symbol = true;\ng.Sc = true;\ng.Dash_Punctuation = true;\ng.Pd = true;\ng.Decimal_Number = true;\ng.Nd = true;\ng.digit = true;\ng.Enclosing_Mark = true;\ng.Me = true;\ng.Final_Punctuation = true;\ng.Pf = true;\ng.Format = true;\ng.Cf = true;\ng.Initial_Punctuation = true;\ng.Pi = true;\ng.Letter = true;\ng.L = true;\ng.Letter_Number = true;\ng.Nl = true;\ng.Line_Separator = true;\ng.Zl = true;\ng.Lowercase_Letter = true;\ng.Ll = true;\ng.Mark = true;\ng.M = true;\ng.Combining_Mark = true;\ng.Math_Symbol = true;\ng.Sm = true;\ng.Modifier_Letter = true;\ng.Lm = true;\ng.Modifier_Symbol = true;\ng.Sk = true;\ng.Nonspacing_Mark = true;\ng.Mn = true;\ng.Number = true;\ng.N = true;\ng.Open_Punctuation = true;\ng.Ps = true;\ng.Other = true;\ng.C = true;\ng.Other_Letter = true;\ng.Lo = true;\ng.Other_Number = true;\ng.No = true;\ng.Other_Punctuation = true;\ng.Po = true;\ng.Other_Symbol = true;\ng.So = true;\ng.Paragraph_Separator = true;\ng.Zp = true;\ng.Private_Use = true;\ng.Co = true;\ng.Punctuation = true;\ng.P = true;\ng.punct = true;\ng.Separator = true;\ng.Z = true;\ng.Space_Separator = true;\ng.Zs = true;\ng.Spacing_Mark = true;\ng.Mc = true;\ng.Surrogate = true;\ng.Cs = true;\ng.Symbol = true;\ng.S = true;\ng.Titlecase_Letter = true;\ng.Lt = true;\ng.Unassigned = true;\ng.Cn = true;\ng.Uppercase_Letter = true;\ng.Lu = true;\n\nvar s = values.script;\ns.Adlam = true;\ns.Adlm = true;\ns.Ahom = true;\ns.Anatolian_Hieroglyphs = true;\ns.Hluw = true;\ns.Arabic = true;\ns.Arab = true;\ns.Armenian = true;\ns.Armn = true;\ns.Avestan = true;\ns.Avst = true;\ns.Balinese = true;\ns.Bali = true;\ns.Bamum = true;\ns.Bamu = true;\ns.Bassa_Vah = true;\ns.Bass = true;\ns.Batak = true;\ns.Batk = true;\ns.Bengali = true;\ns.Beng = true;\ns.Bhaiksuki = true;\ns.Bhks = true;\ns.Bopomofo = true;\ns.Bopo = true;\ns.Brahmi = true;\ns.Brah = true;\ns.Braille = true;\ns.Brai = true;\ns.Buginese = true;\ns.Bugi = true;\ns.Buhid = true;\ns.Buhd = true;\ns.Canadian_Aboriginal = true;\ns.Cans = true;\ns.Carian = true;\ns.Cari = true;\ns.Caucasian_Albanian = true;\ns.Aghb = true;\ns.Chakma = true;\ns.Cakm = true;\ns.Cham = true;\ns.Chorasmian = true;\ns.Chrs = true;\ns.Cherokee = true;\ns.Cher = true;\ns.Common = true;\ns.Zyyy = true;\ns.Coptic = true;\ns.Copt = true;\ns.Qaac = true;\ns.Cuneiform = true;\ns.Xsux = true;\ns.Cypriot = true;\ns.Cprt = true;\ns.Cyrillic = true;\ns.Cyrl = true;\ns.Deseret = true;\ns.Dsrt = true;\ns.Devanagari = true;\ns.Deva = true;\ns.Dives_Akuru = true;\ns.Diak = true;\ns.Dogra = true;\ns.Dogr = true;\ns.Duployan = true;\ns.Dupl = true;\ns.Egyptian_Hieroglyphs = true;\ns.Egyp = true;\ns.Elbasan = true;\ns.Elba = true;\ns.Elymaic = true;\ns.Elym = true;\ns.Ethiopic = true;\ns.Ethi = true;\ns.Georgian = true;\ns.Geor = true;\ns.Glagolitic = true;\ns.Glag = true;\ns.Gothic = true;\ns.Goth = true;\ns.Grantha = true;\ns.Gran = true;\ns.Greek = true;\ns.Grek = true;\ns.Gujarati = true;\ns.Gujr = true;\ns.Gunjala_Gondi = true;\ns.Gong = true;\ns.Gurmukhi = true;\ns.Guru = true;\ns.Han = true;\ns.Hani = true;\ns.Hangul = true;\ns.Hang = true;\ns.Hanifi_Rohingya = true;\ns.Rohg = true;\ns.Hanunoo = true;\ns.Hano = true;\ns.Hatran = true;\ns.Hatr = true;\ns.Hebrew = true;\ns.Hebr = true;\ns.Hiragana = true;\ns.Hira = true;\ns.Imperial_Aramaic = true;\ns.Armi = true;\ns.Inherited = true;\ns.Zinh = true;\ns.Qaai = true;\ns.Inscriptional_Pahlavi = true;\ns.Phli = true;\ns.Inscriptional_Parthian = true;\ns.Prti = true;\ns.Javanese = true;\ns.Java = true;\ns.Kaithi = true;\ns.Kthi = true;\ns.Kannada = true;\ns.Knda = true;\ns.Katakana = true;\ns.Kana = true;\ns.Kayah_Li = true;\ns.Kali = true;\ns.Kharoshthi = true;\ns.Khar = true;\ns.Khitan_Small_Script = true;\ns.Kits = true;\ns.Khmer = true;\ns.Khmr = true;\ns.Khojki = true;\ns.Khoj = true;\ns.Khudawadi = true;\ns.Sind = true;\ns.Lao = true;\ns.Laoo = true;\ns.Latin = true;\ns.Latn = true;\ns.Lepcha = true;\ns.Lepc = true;\ns.Limbu = true;\ns.Limb = true;\ns.Linear_A = true;\ns.Lina = true;\ns.Linear_B = true;\ns.Linb = true;\ns.Lisu = true;\ns.Lycian = true;\ns.Lyci = true;\ns.Lydian = true;\ns.Lydi = true;\ns.Mahajani = true;\ns.Mahj = true;\ns.Makasar = true;\ns.Maka = true;\ns.Malayalam = true;\ns.Mlym = true;\ns.Mandaic = true;\ns.Mand = true;\ns.Manichaean = true;\ns.Mani = true;\ns.Marchen = true;\ns.Marc = true;\ns.Medefaidrin = true;\ns.Medf = true;\ns.Masaram_Gondi = true;\ns.Gonm = true;\ns.Meetei_Mayek = true;\ns.Mtei = true;\ns.Mende_Kikakui = true;\ns.Mend = true;\ns.Meroitic_Cursive = true;\ns.Merc = true;\ns.Meroitic_Hieroglyphs = true;\ns.Mero = true;\ns.Miao = true;\ns.Plrd = true;\ns.Modi = true;\ns.Mongolian = true;\ns.Mong = true;\ns.Mro = true;\ns.Mroo = true;\ns.Multani = true;\ns.Mult = true;\ns.Myanmar = true;\ns.Mymr = true;\ns.Nabataean = true;\ns.Nbat = true;\ns.Nandinagari = true;\ns.Nand = true;\ns.New_Tai_Lue = true;\ns.Talu = true;\ns.Newa = true;\ns.Nko = true;\ns.Nkoo = true;\ns.Nushu = true;\ns.Nshu = true;\ns.Nyiakeng_Puachue_Hmong = true;\ns.Hmnp = true;\ns.Ogham = true;\ns.Ogam = true;\ns.Ol_Chiki = true;\ns.Olck = true;\ns.Old_Hungarian = true;\ns.Hung = true;\ns.Old_Italic = true;\ns.Ital = true;\ns.Old_North_Arabian = true;\ns.Narb = true;\ns.Old_Permic = true;\ns.Perm = true;\ns.Old_Persian = true;\ns.Xpeo = true;\ns.Old_Sogdian = true;\ns.Sogo = true;\ns.Old_South_Arabian = true;\ns.Sarb = true;\ns.Old_Turkic = true;\ns.Orkh = true;\ns.Oriya = true;\ns.Orya = true;\ns.Osage = true;\ns.Osge = true;\ns.Osmanya = true;\ns.Osma = true;\ns.Pahawh_Hmong = true;\ns.Hmng = true;\ns.Palmyrene = true;\ns.Palm = true;\ns.Pau_Cin_Hau = true;\ns.Pauc = true;\ns.Phags_Pa = true;\ns.Phag = true;\ns.Phoenician = true;\ns.Phnx = true;\ns.Psalter_Pahlavi = true;\ns.Phlp = true;\ns.Rejang = true;\ns.Rjng = true;\ns.Runic = true;\ns.Runr = true;\ns.Samaritan = true;\ns.Samr = true;\ns.Saurashtra = true;\ns.Saur = true;\ns.Sharada = true;\ns.Shrd = true;\ns.Shavian = true;\ns.Shaw = true;\ns.Siddham = true;\ns.Sidd = true;\ns.SignWriting = true;\ns.Sgnw = true;\ns.Sinhala = true;\ns.Sinh = true;\ns.Sogdian = true;\ns.Sogd = true;\ns.Sora_Sompeng = true;\ns.Sora = true;\ns.Soyombo = true;\ns.Soyo = true;\ns.Sundanese = true;\ns.Sund = true;\ns.Syloti_Nagri = true;\ns.Sylo = true;\ns.Syriac = true;\ns.Syrc = true;\ns.Tagalog = true;\ns.Tglg = true;\ns.Tagbanwa = true;\ns.Tagb = true;\ns.Tai_Le = true;\ns.Tale = true;\ns.Tai_Tham = true;\ns.Lana = true;\ns.Tai_Viet = true;\ns.Tavt = true;\ns.Takri = true;\ns.Takr = true;\ns.Tamil = true;\ns.Taml = true;\ns.Tangut = true;\ns.Tang = true;\ns.Telugu = true;\ns.Telu = true;\ns.Thaana = true;\ns.Thaa = true;\ns.Thai = true;\ns.Tibetan = true;\ns.Tibt = true;\ns.Tifinagh = true;\ns.Tfng = true;\ns.Tirhuta = true;\ns.Tirh = true;\ns.Ugaritic = true;\ns.Ugar = true;\ns.Vai = true;\ns.Vaii = true;\ns.Wancho = true;\ns.Wcho = true;\ns.Warang_Citi = true;\ns.Wara = true;\ns.Yezidi = true;\ns.Yezi = true;\ns.Yi = true;\ns.Yiii = true;\ns.Zanabazar_Square = true;\ns.Zanb = true;\n\n},{}],27:[function(require,module,exports){\n// jshint -W001\n\n\"use strict\";\n\n// Identifiers provided by the ECMAScript standard.\n\nexports.reservedVars = {\n  NaN       : false,\n  undefined : false\n};\n\nexports.ecmaIdentifiers = {\n  3: {\n    Array              : false,\n    Boolean            : false,\n    Date               : false,\n    decodeURI          : false,\n    decodeURIComponent : false,\n    encodeURI          : false,\n    encodeURIComponent : false,\n    Error              : false,\n    \"eval\"             : false,\n    EvalError          : false,\n    Function           : false,\n    hasOwnProperty     : false,\n    Infinity           : false,\n    isFinite           : false,\n    isNaN              : false,\n    Math               : false,\n    Number             : false,\n    Object             : false,\n    parseInt           : false,\n    parseFloat         : false,\n    RangeError         : false,\n    ReferenceError     : false,\n    RegExp             : false,\n    String             : false,\n    SyntaxError        : false,\n    TypeError          : false,\n    URIError           : false\n  },\n  5: {\n    JSON               : false\n  },\n  6: {\n    ArrayBuffer        : false,\n    DataView           : false,\n    Float32Array       : false,\n    Float64Array       : false,\n    Int8Array          : false,\n    Int16Array         : false,\n    Int32Array         : false,\n    Map                : false,\n    Promise            : false,\n    Proxy              : false,\n    Reflect            : false,\n    Set                : false,\n    Symbol             : false,\n    Uint8Array         : false,\n    Uint16Array        : false,\n    Uint32Array        : false,\n    Uint8ClampedArray  : false,\n    WeakMap            : false,\n    WeakSet            : false\n  },\n  8: {\n    Atomics            : false,\n    SharedArrayBuffer  : false\n  },\n  11: {\n    BigInt             : false\n  }\n};\n\n// Global variables commonly provided by a web browser environment.\n\nexports.browser = {\n  Audio                : false,\n  Blob                 : false,\n  addEventListener     : false, // EventTarget\n  applicationCache     : false,\n  atob                 : false, // WindowOrWorkerGlobalScope\n  blur                 : false,\n  btoa                 : false, // WindowOrWorkerGlobalScope\n  cancelAnimationFrame : false,\n  CanvasGradient       : false,\n  CanvasPattern        : false,\n  CanvasRenderingContext2D: false,\n  CSS                  : false,\n  CSSImportRule        : false,\n  CSSGroupingRule      : false,\n  CSSMarginRule        : false,\n  CSSMediaRule         : false,\n  CSSNamespaceRule     : false,\n  CSSPageRule          : false,\n  CSSRule              : false,\n  CSSRuleList          : false,\n  CSSStyleDeclaration  : false,\n  CSSStyleRule         : false,\n  CSSStyleSheet        : false,\n  clearInterval        : false, // WindowOrWorkerGlobalScope\n  clearTimeout         : false, // WindowOrWorkerGlobalScope\n  close                : false,\n  closed               : false,\n  Comment              : false,\n  CompositionEvent     : false,\n  createImageBitmap    : false, // WindowOrWorkerGlobalScope\n  CustomEvent          : false,\n  DOMParser            : false,\n  defaultStatus        : false,\n  dispatchEvent        : false, // EventTarget\n  Document             : false,\n  document             : false,\n  DocumentFragment     : false,\n  Element              : false,\n  ElementTimeControl   : false,\n  Event                : false,\n  event                : false,\n  fetch                : false,\n  File                 : false,\n  FileList             : false,\n  FileReader           : false,\n  FormData             : false,\n  focus                : false,\n  frames               : false,\n  getComputedStyle     : false,\n  Headers              : false,\n  HTMLAnchorElement    : false,\n  HTMLAreaElement      : false,\n  HTMLAudioElement     : false,\n  HTMLBaseElement      : false,\n  HTMLBlockquoteElement: false,\n  HTMLBodyElement      : false,\n  HTMLBRElement        : false,\n  HTMLButtonElement    : false,\n  HTMLCanvasElement    : false,\n  HTMLCollection       : false,\n  HTMLDataElement      : false,\n  HTMLDataListElement  : false,\n  HTMLDetailsElement   : false,\n  HTMLDialogElement    : false,\n  HTMLDirectoryElement : false,\n  HTMLDivElement       : false,\n  HTMLDListElement     : false,\n  HTMLElement          : false,\n  HTMLEmbedElement     : false,\n  HTMLFieldSetElement  : false,\n  HTMLFontElement      : false,\n  HTMLFormElement      : false,\n  HTMLFrameElement     : false,\n  HTMLFrameSetElement  : false,\n  HTMLHeadElement      : false,\n  HTMLHeadingElement   : false,\n  HTMLHRElement        : false,\n  HTMLHtmlElement      : false,\n  HTMLIFrameElement    : false,\n  HTMLImageElement     : false,\n  HTMLInputElement     : false,\n/* HTMLIsIndexElement was removed from the WHATWG HTML spec;\n   see https://github.com/whatwg/html/pull/1095.\n   HTMLIsIndexElement has been removed from browsers; see:\n   • Chromium Removal: https://codereview.chromium.org/96653004/\n   • Gecko Removal: https://bugzilla.mozilla.org/show_bug.cgi?id=1266495\n   • WebKit Removal: https://bugs.webkit.org/show_bug.cgi?id=7139.\n   See also the discussion at https://github.com/jshint/jshint/pull/3222. */\n  HTMLIsIndexElement   : false,\n  HTMLLabelElement     : false,\n  HTMLLayerElement     : false,\n  HTMLLegendElement    : false,\n  HTMLLIElement        : false,\n  HTMLLinkElement      : false,\n  HTMLMapElement       : false,\n  HTMLMarqueeElement   : false,\n  HTMLMediaElement     : false,\n  HTMLMenuElement      : false,\n  HTMLMetaElement      : false,\n  HTMLMeterElement     : false,\n  HTMLModElement       : false,\n  HTMLObjectElement    : false,\n  HTMLOListElement     : false,\n  HTMLOptGroupElement  : false,\n  HTMLOptionElement    : false,\n  HTMLParagraphElement : false,\n  HTMLParamElement     : false,\n  HTMLPictureElement   : false,\n  HTMLPreElement       : false,\n  HTMLProgressElement  : false,\n  HTMLQuoteElement     : false,\n  HTMLScriptElement    : false,\n  HTMLSelectElement    : false,\n  HTMLSlotElement      : false,\n  HTMLSourceElement    : false,\n  HTMLStyleElement     : false,\n  HTMLTableCaptionElement: false,\n  HTMLTableCellElement : false,\n  HTMLTableColElement  : false,\n  HTMLTableElement     : false,\n  HTMLTableRowElement  : false,\n  HTMLTableSectionElement: false,\n  HTMLTemplateElement  : false,\n  HTMLTextAreaElement  : false,\n  HTMLTimeElement      : false,\n  HTMLTitleElement     : false,\n  HTMLTrackElement     : false,\n  HTMLUListElement     : false,\n  HTMLVideoElement     : false,\n  history              : false,\n  Image                : false,\n  IntersectionObserver : false,\n  Intl                 : false,\n  length               : false,\n  localStorage         : false,\n  location             : false,\n  matchMedia           : false,\n  MediaList            : false,\n  MediaRecorder        : false,\n  MessageChannel       : false,\n  MessageEvent         : false,\n  MessagePort          : false,\n  MouseEvent           : false,\n  moveBy               : false,\n  moveTo               : false,\n  MutationObserver     : false,\n  name                 : false,\n  Node                 : false,\n  NodeFilter           : false,\n  NodeList             : false,\n  Notification         : false,\n  navigator            : false,\n  onbeforeunload       : true,\n  onblur               : true,\n  onerror              : true,\n  onfocus              : true,\n  onload               : true,\n  onresize             : true,\n  onunload             : true,\n  open                 : false,\n  openDatabase         : false,\n  opener               : false,\n  Option               : false,\n  origin               : false, // WindowOrWorkerGlobalScope\n  parent               : false,\n  performance          : false,\n  print                : false,\n  queueMicrotask       : false, // WindowOrWorkerGlobalScope\n  Range                : false,\n  requestAnimationFrame : false,\n  removeEventListener  : false, // EventTarget\n  Request              : false,\n  resizeBy             : false,\n  resizeTo             : false,\n  Response             : false,\n  screen               : false,\n  scroll               : false,\n  scrollBy             : false,\n  scrollTo             : false,\n  sessionStorage       : false,\n  setInterval          : false, // WindowOrWorkerGlobalScope\n  setTimeout           : false, // WindowOrWorkerGlobalScope\n  SharedWorker         : false,\n  status               : false,\n  Storage              : false,\n  StyleSheet           : false,\n  SVGAElement          : false,\n  SVGAltGlyphDefElement: false,\n  SVGAltGlyphElement   : false,\n  SVGAltGlyphItemElement: false,\n  SVGAngle             : false,\n  SVGAnimateColorElement: false,\n  SVGAnimateElement    : false,\n  SVGAnimateMotionElement: false,\n  SVGAnimateTransformElement: false,\n  SVGAnimatedAngle     : false,\n  SVGAnimatedBoolean   : false,\n  SVGAnimatedEnumeration: false,\n  SVGAnimatedInteger   : false,\n  SVGAnimatedLength    : false,\n  SVGAnimatedLengthList: false,\n  SVGAnimatedNumber    : false,\n  SVGAnimatedNumberList: false,\n  SVGAnimatedPathData  : false,\n  SVGAnimatedPoints    : false,\n  SVGAnimatedPreserveAspectRatio: false,\n  SVGAnimatedRect      : false,\n  SVGAnimatedString    : false,\n  SVGAnimatedTransformList: false,\n  SVGAnimationElement  : false,\n  SVGCSSRule           : false,\n  SVGCircleElement     : false,\n  SVGClipPathElement   : false,\n  SVGColor             : false,\n  SVGColorProfileElement: false,\n  SVGColorProfileRule  : false,\n  SVGComponentTransferFunctionElement: false,\n  SVGCursorElement     : false,\n  SVGDefsElement       : false,\n  SVGDescElement       : false,\n  SVGDocument          : false,\n  SVGElement           : false,\n  SVGElementInstance   : false,\n  SVGElementInstanceList: false,\n  SVGEllipseElement    : false,\n  SVGExternalResourcesRequired: false,\n  SVGFEBlendElement    : false,\n  SVGFEColorMatrixElement: false,\n  SVGFEComponentTransferElement: false,\n  SVGFECompositeElement: false,\n  SVGFEConvolveMatrixElement: false,\n  SVGFEDiffuseLightingElement: false,\n  SVGFEDisplacementMapElement: false,\n  SVGFEDistantLightElement: false,\n  SVGFEFloodElement    : false,\n  SVGFEFuncAElement    : false,\n  SVGFEFuncBElement    : false,\n  SVGFEFuncGElement    : false,\n  SVGFEFuncRElement    : false,\n  SVGFEGaussianBlurElement: false,\n  SVGFEImageElement    : false,\n  SVGFEMergeElement    : false,\n  SVGFEMergeNodeElement: false,\n  SVGFEMorphologyElement: false,\n  SVGFEOffsetElement   : false,\n  SVGFEPointLightElement: false,\n  SVGFESpecularLightingElement: false,\n  SVGFESpotLightElement: false,\n  SVGFETileElement     : false,\n  SVGFETurbulenceElement: false,\n  SVGFilterElement     : false,\n  SVGFilterPrimitiveStandardAttributes: false,\n  SVGFitToViewBox      : false,\n  SVGFontElement       : false,\n  SVGFontFaceElement   : false,\n  SVGFontFaceFormatElement: false,\n  SVGFontFaceNameElement: false,\n  SVGFontFaceSrcElement: false,\n  SVGFontFaceUriElement: false,\n  SVGForeignObjectElement: false,\n  SVGGElement          : false,\n  SVGGlyphElement      : false,\n  SVGGlyphRefElement   : false,\n  SVGGradientElement   : false,\n  SVGHKernElement      : false,\n  SVGICCColor          : false,\n  SVGImageElement      : false,\n  SVGLangSpace         : false,\n  SVGLength            : false,\n  SVGLengthList        : false,\n  SVGLineElement       : false,\n  SVGLinearGradientElement: false,\n  SVGLocatable         : false,\n  SVGMPathElement      : false,\n  SVGMarkerElement     : false,\n  SVGMaskElement       : false,\n  SVGMatrix            : false,\n  SVGMetadataElement   : false,\n  SVGMissingGlyphElement: false,\n  SVGNumber            : false,\n  SVGNumberList        : false,\n  SVGPaint             : false,\n  SVGPathElement       : false,\n  SVGPathSeg           : false,\n  SVGPathSegArcAbs     : false,\n  SVGPathSegArcRel     : false,\n  SVGPathSegClosePath  : false,\n  SVGPathSegCurvetoCubicAbs: false,\n  SVGPathSegCurvetoCubicRel: false,\n  SVGPathSegCurvetoCubicSmoothAbs: false,\n  SVGPathSegCurvetoCubicSmoothRel: false,\n  SVGPathSegCurvetoQuadraticAbs: false,\n  SVGPathSegCurvetoQuadraticRel: false,\n  SVGPathSegCurvetoQuadraticSmoothAbs: false,\n  SVGPathSegCurvetoQuadraticSmoothRel: false,\n  SVGPathSegLinetoAbs  : false,\n  SVGPathSegLinetoHorizontalAbs: false,\n  SVGPathSegLinetoHorizontalRel: false,\n  SVGPathSegLinetoRel  : false,\n  SVGPathSegLinetoVerticalAbs: false,\n  SVGPathSegLinetoVerticalRel: false,\n  SVGPathSegList       : false,\n  SVGPathSegMovetoAbs  : false,\n  SVGPathSegMovetoRel  : false,\n  SVGPatternElement    : false,\n  SVGPoint             : false,\n  SVGPointList         : false,\n  SVGPolygonElement    : false,\n  SVGPolylineElement   : false,\n  SVGPreserveAspectRatio: false,\n  SVGRadialGradientElement: false,\n  SVGRect              : false,\n  SVGRectElement       : false,\n  SVGRenderingIntent   : false,\n  SVGSVGElement        : false,\n  SVGScriptElement     : false,\n  SVGSetElement        : false,\n  SVGStopElement       : false,\n  SVGStringList        : false,\n  SVGStylable          : false,\n  SVGStyleElement      : false,\n  SVGSwitchElement     : false,\n  SVGSymbolElement     : false,\n  SVGTRefElement       : false,\n  SVGTSpanElement      : false,\n  SVGTests             : false,\n  SVGTextContentElement: false,\n  SVGTextElement       : false,\n  SVGTextPathElement   : false,\n  SVGTextPositioningElement: false,\n  SVGTitleElement      : false,\n  SVGTransform         : false,\n  SVGTransformList     : false,\n  SVGTransformable     : false,\n  SVGURIReference      : false,\n  SVGUnitTypes         : false,\n  SVGUseElement        : false,\n  SVGVKernElement      : false,\n  SVGViewElement       : false,\n  SVGViewSpec          : false,\n  SVGZoomAndPan        : false,\n  Text                 : false,\n  TextDecoder          : false,\n  TextEncoder          : false,\n  TimeEvent            : false,\n  top                  : false,\n  URL                  : false,\n  URLSearchParams      : false,\n  WebGLActiveInfo      : false,\n  WebGLBuffer          : false,\n  WebGLContextEvent    : false,\n  WebGLFramebuffer     : false,\n  WebGLProgram         : false,\n  WebGLRenderbuffer    : false,\n  WebGLRenderingContext: false,\n  WebGLShader          : false,\n  WebGLShaderPrecisionFormat: false,\n  WebGLTexture         : false,\n  WebGLUniformLocation : false,\n  WebSocket            : false,\n  window               : false,\n  Window               : false,\n  Worker               : false,\n  XDomainRequest       : false,\n  XMLDocument          : false,\n  XMLHttpRequest       : false,\n  XMLSerializer        : false,\n  XPathEvaluator       : false,\n  XPathException       : false,\n  XPathExpression      : false,\n  XPathNamespace       : false,\n  XPathNSResolver      : false,\n  XPathResult          : false\n};\n\nexports.devel = {\n  alert  : false,\n  confirm: false,\n  console: false,\n  Debug  : false,\n  opera  : false,\n  prompt : false\n};\n\nexports.worker = {\n  addEventListener    : true, // EventTarget\n  atob                : true, // WindowOrWorkerGlobalScope\n  btoa                : true, // WindowOrWorkerGlobalScope\n  clearInterval       : true, // WindowOrWorkerGlobalScope\n  clearTimeout        : true, // WindowOrWorkerGlobalScope\n  createImageBitmap   : true, // WindowOrWorkerGlobalScope\n  dispatchEvent       : true, // EventTarget\n  importScripts       : true,\n  onmessage           : true,\n  origin              : true, // WindowOrWorkerGlobalScope\n  postMessage         : true,\n  queueMicrotask      : true, // WindowOrWorkerGlobalScope\n  removeEventListener : true, // EventTarget\n  self                : true,\n  setInterval         : true, // WindowOrWorkerGlobalScope\n  setTimeout          : true, // WindowOrWorkerGlobalScope\n  FileReaderSync      : true\n};\n\n// Widely adopted global names that are not part of ECMAScript standard\nexports.nonstandard = {\n  escape  : false,\n  unescape: false\n};\n\n// Globals provided by popular JavaScript environments.\n\nexports.couch = {\n  \"require\" : false,\n  respond   : false,\n  getRow    : false,\n  emit      : false,\n  send      : false,\n  start     : false,\n  sum       : false,\n  log       : false,\n  exports   : false,\n  module    : false,\n  provides  : false\n};\n\nexports.node = {\n  __filename    : false,\n  __dirname     : false,\n  arguments     : false,\n  GLOBAL        : false,\n  global        : false,\n  module        : false,\n  require       : false,\n  Intl          : false,\n\n  // These globals are writeable because Node allows the following\n  // usage pattern: var Buffer = require(\"buffer\").Buffer;\n\n  Buffer         : true,\n  console        : true,\n  exports        : true,\n  process        : true,\n  setTimeout     : true,\n  clearTimeout   : true,\n  setInterval    : true,\n  clearInterval  : true,\n  setImmediate   : true, // v0.9.1+\n  clearImmediate : true, // v0.9.1+\n  URL            : true,  // v6.13.0+\n  URLSearchParams: true  // v6.13.0+\n};\n\nexports.browserify = {\n  __filename    : false,\n  __dirname     : false,\n  global        : false,\n  module        : false,\n  require       : false,\n  Buffer        : true,\n  exports       : true,\n  process       : true\n};\n\nexports.phantom = {\n  phantom      : true,\n  require      : true,\n  WebPage      : true,\n  console      : true, // in examples, but undocumented\n  exports      : true  // v1.7+\n};\n\nexports.qunit = {\n  asyncTest      : false,\n  deepEqual      : false,\n  equal          : false,\n  expect         : false,\n  module         : false,\n  notDeepEqual   : false,\n  notEqual       : false,\n  notOk          : false,\n  notPropEqual   : false,\n  notStrictEqual : false,\n  ok             : false,\n  propEqual      : false,\n  QUnit          : false,\n  raises         : false,\n  start          : false,\n  stop           : false,\n  strictEqual    : false,\n  test           : false,\n  \"throws\"       : false\n};\n\nexports.rhino = {\n  arguments    : false,\n  defineClass  : false,\n  deserialize  : false,\n  gc           : false,\n  help         : false,\n  importClass  : false,\n  importPackage: false,\n  \"java\"       : false,\n  load         : false,\n  loadClass    : false,\n  Packages     : false,\n  print        : false,\n  quit         : false,\n  readFile     : false,\n  readUrl      : false,\n  runCommand   : false,\n  seal         : false,\n  serialize    : false,\n  spawn        : false,\n  sync         : false,\n  toint32      : false,\n  version      : false\n};\n\nexports.shelljs = {\n  target       : false,\n  echo         : false,\n  exit         : false,\n  cd           : false,\n  pwd          : false,\n  ls           : false,\n  find         : false,\n  cp           : false,\n  rm           : false,\n  mv           : false,\n  mkdir        : false,\n  test         : false,\n  cat          : false,\n  sed          : false,\n  grep         : false,\n  which        : false,\n  dirs         : false,\n  pushd        : false,\n  popd         : false,\n  env          : false,\n  exec         : false,\n  chmod        : false,\n  config       : false,\n  error        : false,\n  tempdir      : false\n};\n\nexports.typed = {\n  ArrayBuffer         : false,\n  ArrayBufferView     : false,\n  DataView            : false,\n  Float32Array        : false,\n  Float64Array        : false,\n  Int16Array          : false,\n  Int32Array          : false,\n  Int8Array           : false,\n  Uint16Array         : false,\n  Uint32Array         : false,\n  Uint8Array          : false,\n  Uint8ClampedArray   : false\n};\n\nexports.wsh = {\n  ActiveXObject            : true,\n  Enumerator               : true,\n  GetObject                : true,\n  ScriptEngine             : true,\n  ScriptEngineBuildVersion : true,\n  ScriptEngineMajorVersion : true,\n  ScriptEngineMinorVersion : true,\n  VBArray                  : true,\n  WSH                      : true,\n  WScript                  : true,\n  XDomainRequest           : true\n};\n\n// Globals provided by popular JavaScript libraries.\n\nexports.dojo = {\n  dojo     : false,\n  dijit    : false,\n  dojox    : false,\n  define   : false,\n  \"require\": false\n};\n\nexports.jquery = {\n  \"$\"    : false,\n  jQuery : false\n};\n\nexports.mootools = {\n  \"$\"           : false,\n  \"$$\"          : false,\n  Asset         : false,\n  Browser       : false,\n  Chain         : false,\n  Class         : false,\n  Color         : false,\n  Cookie        : false,\n  Core          : false,\n  Document      : false,\n  DomReady      : false,\n  DOMEvent      : false,\n  DOMReady      : false,\n  Drag          : false,\n  Element       : false,\n  Elements      : false,\n  Event         : false,\n  Events        : false,\n  Fx            : false,\n  Group         : false,\n  Hash          : false,\n  HtmlTable     : false,\n  IFrame        : false,\n  IframeShim    : false,\n  InputValidator: false,\n  instanceOf    : false,\n  Keyboard      : false,\n  Locale        : false,\n  Mask          : false,\n  MooTools      : false,\n  Native        : false,\n  Options       : false,\n  OverText      : false,\n  Request       : false,\n  Scroller      : false,\n  Slick         : false,\n  Slider        : false,\n  Sortables     : false,\n  Spinner       : false,\n  Swiff         : false,\n  Tips          : false,\n  Type          : false,\n  typeOf        : false,\n  URI           : false,\n  Window        : false\n};\n\nexports.prototypejs = {\n  \"$\"               : false,\n  \"$$\"              : false,\n  \"$A\"              : false,\n  \"$F\"              : false,\n  \"$H\"              : false,\n  \"$R\"              : false,\n  \"$break\"          : false,\n  \"$continue\"       : false,\n  \"$w\"              : false,\n  Abstract          : false,\n  Ajax              : false,\n  Class             : false,\n  Enumerable        : false,\n  Element           : false,\n  Event             : false,\n  Field             : false,\n  Form              : false,\n  Hash              : false,\n  Insertion         : false,\n  ObjectRange       : false,\n  PeriodicalExecuter: false,\n  Position          : false,\n  Prototype         : false,\n  Selector          : false,\n  Template          : false,\n  Toggle            : false,\n  Try               : false,\n  Autocompleter     : false,\n  Builder           : false,\n  Control           : false,\n  Draggable         : false,\n  Draggables        : false,\n  Droppables        : false,\n  Effect            : false,\n  Sortable          : false,\n  SortableObserver  : false,\n  Sound             : false,\n  Scriptaculous     : false\n};\n\nexports.yui = {\n  YUI       : false,\n  Y         : false,\n  YUI_config: false\n};\n\nexports.mocha = {\n  // Global (for config etc.)\n  mocha       : false,\n  // BDD\n  describe    : false,\n  xdescribe   : false,\n  context     : false,\n  xcontext    : false,\n  it          : false,\n  xit         : false,\n  specify     : false,\n  xspecify    : false,\n  before      : false,\n  after       : false,\n  beforeEach  : false,\n  afterEach   : false,\n  // TDD\n  suite         : false,\n  test          : false,\n  setup         : false,\n  teardown      : false,\n  suiteSetup    : false,\n  suiteTeardown : false\n};\n\nexports.jasmine = {\n  jasmine     : false,\n  describe    : false,\n  xdescribe   : false,\n  it          : false,\n  xit         : false,\n  beforeEach  : false,\n  afterEach   : false,\n  setFixtures : false,\n  loadFixtures: false,\n  spyOn       : false,\n  expect      : false,\n  // Jasmine 1.3\n  runs        : false,\n  waitsFor    : false,\n  waits       : false,\n  // Jasmine 2.1\n  beforeAll   : false,\n  afterAll    : false,\n  fail        : false,\n  fdescribe   : false,\n  fit         : false,\n  pending     : false,\n  // Jasmine 2.6\n  spyOnProperty: false\n};\n\n},{}],\"jshint\":[function(require,module,exports){\n/*!\n * JSHint, by JSHint Community.\n *\n * Licensed under the MIT license.\n *\n * JSHint is a derivative work of JSLint:\n *\n *   Copyright (c) 2002 Douglas Crockford  (www.JSLint.com)\n *\n *   Permission is hereby granted, free of charge, to any person obtaining\n *   a copy of this software and associated documentation files (the \"Software\"),\n *   to deal in the Software without restriction, including without limitation\n *   the rights to use, copy, modify, merge, publish, distribute, sublicense,\n *   and/or sell copies of the Software, and to permit persons to whom\n *   the Software is furnished to do so, subject to the following conditions:\n *\n *   The above copyright notice and this permission notice shall be included\n *   in all copies or substantial portions of the Software.\n *\n *   THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n *   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n *   DEALINGS IN THE SOFTWARE.\n *\n */\n\n/*jshint quotmark:double */\n/*exported console */\n\nvar _            = require(\"lodash\");\nvar events       = require(\"events\");\nvar vars         = require(\"./vars.js\");\nvar messages     = require(\"./messages.js\");\nvar Lexer        = require(\"./lex.js\").Lexer;\nvar reg          = require(\"./reg.js\");\nvar state        = require(\"./state.js\").state;\nvar style        = require(\"./style.js\");\nvar options      = require(\"./options.js\");\nvar scopeManager = require(\"./scope-manager.js\");\nvar prodParams   = require(\"./prod-params.js\");\n\n// We need this module here because environments such as IE and Rhino\n// don't necessarilly expose the 'console' API and browserify uses\n// it to log things. It's a sad state of affair, really.\nvar console = require(\"console-browserify\");\n\n// We build the application inside a function so that we produce only a singleton\n// variable. That function will be invoked immediately, and its return value is\n// the JSHINT function itself.\n\nvar JSHINT = (function() {\n  \"use strict\";\n\n  var api, // Extension API\n\n    // These are operators that should not be used with the ! operator.\n    bang = {\n      \"<\"  : true,\n      \"<=\" : true,\n      \"==\" : true,\n      \"===\": true,\n      \"!==\": true,\n      \"!=\" : true,\n      \">\"  : true,\n      \">=\" : true,\n      \"+\"  : true,\n      \"-\"  : true,\n      \"*\"  : true,\n      \"/\"  : true,\n      \"%\"  : true\n    },\n\n    declared, // Globals that were declared using /*global ... */ syntax.\n\n    functions, // All of the functions\n\n    inblock,\n    indent,\n    lookahead,\n    lex,\n    member,\n    membersOnly,\n    predefined,    // Global variables defined by option\n\n    extraModules = [],\n    emitter = new events.EventEmitter();\n\n  function checkOption(name, isStable, t) {\n    var type, validNames;\n\n    if (isStable) {\n      type = \"\";\n      validNames = options.validNames;\n    } else {\n      type = \"unstable \";\n      validNames = options.unstableNames;\n    }\n\n    name = name.trim();\n\n    if (/^[+-]W\\d{3}$/g.test(name)) {\n      return true;\n    }\n\n    if (validNames.indexOf(name) === -1) {\n      if (t.type !== \"jslint\" && !_.has(options.removed, name)) {\n        error(\"E001\", t, type, name);\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  function isString(obj) {\n    return Object.prototype.toString.call(obj) === \"[object String]\";\n  }\n\n  function isIdentifier(tkn, value) {\n    if (!tkn)\n      return false;\n\n    if (!tkn.identifier || tkn.value !== value)\n      return false;\n\n    return true;\n  }\n\n  /**\n   * ES3 defined a set of \"FutureReservedWords\" in order \"to allow for the\n   * possibility of future adoption of [proposed] extensions.\"\n   *\n   * ES5 reduced the set of FutureReservedWords, in some cases by using them to\n   * define new syntactic forms (e.g. `class` and `const`) and in other cases\n   * by simply allowing their use as Identifiers (e.g. `int` and `goto`).\n   * Separately, ES5 introduced new restrictions on certain tokens, but limited\n   * the restriction to strict mode code (e.g. `let` and `yield`).\n   *\n   * This function determines if a given token describes a reserved word\n   * according to the current state of the parser.\n   *\n   * @param {number} context - the parsing context; see `prod-params.js` for\n   *                           more information\n   * @param {Token} token\n   */\n  function isReserved(context, token) {\n    if (!token.reserved) {\n      return false;\n    }\n    var meta = token.meta;\n\n    if (meta && meta.isFutureReservedWord) {\n      if (state.inES5()) {\n        // ES3 FutureReservedWord in an ES5 environment.\n        if (!meta.es5) {\n          return false;\n        }\n\n        if (token.isProperty) {\n          return false;\n        }\n      }\n    } else if (meta && meta.es5 && !state.inES5()) {\n      return false;\n    }\n\n    // Some identifiers are reserved only within a strict mode environment.\n    if (meta && meta.strictOnly && state.inES5()) {\n      if (!state.option.strict && !state.isStrict()) {\n        return false;\n      }\n    }\n\n    if (token.id === \"await\" && (!(context & prodParams.async) && !state.option.module)) {\n      return false;\n    }\n\n    if (token.id === \"yield\" && (!(context & prodParams.yield))) {\n      return state.isStrict();\n    }\n\n    return true;\n  }\n\n  function supplant(str, data) {\n    return str.replace(/\\{([^{}]*)\\}/g, function(a, b) {\n      var r = data[b];\n      return typeof r === \"string\" || typeof r === \"number\" ? r : a;\n    });\n  }\n\n  function combine(dest, src) {\n    Object.keys(src).forEach(function(name) {\n      if (_.has(JSHINT.blacklist, name)) return;\n      dest[name] = src[name];\n    });\n  }\n\n  function processenforceall() {\n    if (state.option.enforceall) {\n      for (var enforceopt in options.bool.enforcing) {\n        if (state.option[enforceopt] === undefined &&\n            !options.noenforceall[enforceopt]) {\n          state.option[enforceopt] = true;\n        }\n      }\n      for (var relaxopt in options.bool.relaxing) {\n        if (state.option[relaxopt] === undefined) {\n          state.option[relaxopt] = false;\n        }\n      }\n    }\n  }\n\n  /**\n   * Apply all linting options according to the status of the `state` object.\n   */\n  function applyOptions() {\n    var badESOpt = null;\n    processenforceall();\n\n    /**\n     * TODO: Remove in JSHint 3\n     */\n    badESOpt = state.inferEsVersion();\n    if (badESOpt) {\n      quit(\"E059\", state.tokens.next, \"esversion\", badESOpt);\n    }\n\n    if (state.inES5()) {\n      combine(predefined, vars.ecmaIdentifiers[5]);\n    }\n\n    if (state.inES6()) {\n      combine(predefined, vars.ecmaIdentifiers[6]);\n    }\n\n    if (state.inES8()) {\n      combine(predefined, vars.ecmaIdentifiers[8]);\n    }\n\n    /**\n     * Use `in` to check for the presence of any explicitly-specified value for\n     * `globalstrict` because both `true` and `false` should trigger an error.\n     */\n    if (state.option.strict === \"global\" && \"globalstrict\" in state.option) {\n      quit(\"E059\", state.tokens.next, \"strict\", \"globalstrict\");\n    }\n\n    if (state.option.module) {\n      /**\n       * TODO: Extend this restriction to *all* ES6-specific options.\n       */\n      if (!state.inES6()) {\n        warning(\"W134\", state.tokens.next, \"module\", 6);\n      }\n    }\n\n    if (state.option.regexpu) {\n      /**\n       * TODO: Extend this restriction to *all* ES6-specific options.\n       */\n      if (!state.inES6()) {\n        warning(\"W134\", state.tokens.next, \"regexpu\", 6);\n      }\n    }\n\n    if (state.option.couch) {\n      combine(predefined, vars.couch);\n    }\n\n    if (state.option.qunit) {\n      combine(predefined, vars.qunit);\n    }\n\n    if (state.option.rhino) {\n      combine(predefined, vars.rhino);\n    }\n\n    if (state.option.shelljs) {\n      combine(predefined, vars.shelljs);\n      combine(predefined, vars.node);\n    }\n    if (state.option.typed) {\n      combine(predefined, vars.typed);\n    }\n\n    if (state.option.phantom) {\n      combine(predefined, vars.phantom);\n    }\n\n    if (state.option.prototypejs) {\n      combine(predefined, vars.prototypejs);\n    }\n\n    if (state.option.node) {\n      combine(predefined, vars.node);\n      combine(predefined, vars.typed);\n    }\n\n    if (state.option.devel) {\n      combine(predefined, vars.devel);\n    }\n\n    if (state.option.dojo) {\n      combine(predefined, vars.dojo);\n    }\n\n    if (state.option.browser) {\n      combine(predefined, vars.browser);\n      combine(predefined, vars.typed);\n    }\n\n    if (state.option.browserify) {\n      combine(predefined, vars.browser);\n      combine(predefined, vars.typed);\n      combine(predefined, vars.browserify);\n    }\n\n    if (state.option.nonstandard) {\n      combine(predefined, vars.nonstandard);\n    }\n\n    if (state.option.jasmine) {\n      combine(predefined, vars.jasmine);\n    }\n\n    if (state.option.jquery) {\n      combine(predefined, vars.jquery);\n    }\n\n    if (state.option.mootools) {\n      combine(predefined, vars.mootools);\n    }\n\n    if (state.option.worker) {\n      combine(predefined, vars.worker);\n    }\n\n    if (state.option.wsh) {\n      combine(predefined, vars.wsh);\n    }\n\n    if (state.option.yui) {\n      combine(predefined, vars.yui);\n    }\n\n    if (state.option.mocha) {\n      combine(predefined, vars.mocha);\n    }\n  }\n\n  // Produce an error warning.\n  function quit(code, token, a, b) {\n    var percentage = Math.floor((token.line / state.lines.length) * 100);\n    var message = messages.errors[code].desc;\n\n    var exception = {\n      name: \"JSHintError\",\n      line: token.line,\n      character: token.from,\n      message: message + \" (\" + percentage + \"% scanned).\",\n      raw: message,\n      code: code,\n      a: a,\n      b: b\n    };\n\n    exception.reason = supplant(message, exception) + \" (\" + percentage +\n      \"% scanned).\";\n\n    throw exception;\n  }\n\n  function removeIgnoredMessages() {\n    var ignored = state.ignoredLines;\n\n    if (_.isEmpty(ignored)) return;\n    JSHINT.errors = _.reject(JSHINT.errors, function(err) { return ignored[err.line] });\n  }\n\n  function warning(code, t, a, b, c, d) {\n    var ch, l, w, msg;\n\n    if (/^W\\d{3}$/.test(code)) {\n      if (state.ignored[code])\n        return;\n\n      msg = messages.warnings[code];\n    } else if (/E\\d{3}/.test(code)) {\n      msg = messages.errors[code];\n    } else if (/I\\d{3}/.test(code)) {\n      msg = messages.info[code];\n    }\n\n    t = t || state.tokens.next || {};\n    if (t.id === \"(end)\") {  // `~\n      t = state.tokens.curr;\n    }\n\n    l = t.line;\n    ch = t.from;\n\n    w = {\n      id: \"(error)\",\n      raw: msg.desc,\n      code: msg.code,\n      evidence: state.lines[l - 1] || \"\",\n      line: l,\n      character: ch,\n      scope: JSHINT.scope,\n      a: a,\n      b: b,\n      c: c,\n      d: d\n    };\n\n    w.reason = supplant(msg.desc, w);\n    JSHINT.errors.push(w);\n\n    removeIgnoredMessages();\n\n    var errors = JSHINT.errors.filter(function(e) { return /E\\d{3}/.test(e.code); });\n    if (errors.length >= state.option.maxerr) {\n      quit(\"E043\", t);\n    }\n    return w;\n  }\n\n  function warningAt(m, l, ch, a, b, c, d) {\n    return warning(m, {\n      line: l,\n      from: ch\n    }, a, b, c, d);\n  }\n\n  function error(m, t, a, b, c, d) {\n    warning(m, t, a, b, c, d);\n  }\n\n  function errorAt(m, l, ch, a, b, c, d) {\n    return error(m, {\n      line: l,\n      from: ch\n    }, a, b, c, d);\n  }\n\n  // Tracking of \"internal\" scripts, like eval containing a static string\n  function addEvalCode(elem, token) {\n    JSHINT.internals.push({\n      id: \"(internal)\",\n      elem: elem,\n      token: token,\n      code: token.value.replace(/([^\\\\])(\\\\*)\\2\\\\n/g, \"$1\\n\")\n    });\n  }\n\n  /**\n   * Process an inline linting directive\n   *\n   * @param {Token} directiveToken - the directive-bearing comment token\n   * @param {Token} previous - the token that preceeds the directive\n   */\n  function lintingDirective(directiveToken, previous) {\n    var body = directiveToken.body.split(\",\")\n      .map(function(s) { return s.trim(); });\n    var predef = {};\n\n    if (directiveToken.type === \"falls through\") {\n      previous.caseFallsThrough = true;\n      return;\n    }\n\n    if (directiveToken.type === \"globals\") {\n      body.forEach(function(item, idx) {\n        var parts = item.split(\":\");\n        var key = parts[0].trim();\n\n        if (key === \"-\" || !key.length) {\n          // Ignore trailing comma\n          if (idx > 0 && idx === body.length - 1) {\n            return;\n          }\n          error(\"E002\", directiveToken);\n          return;\n        }\n\n        if (key.charAt(0) === \"-\") {\n          key = key.slice(1);\n\n          JSHINT.blacklist[key] = key;\n          delete predefined[key];\n        } else {\n          predef[key] = parts.length > 1 && parts[1].trim() === \"true\";\n        }\n      });\n\n      combine(predefined, predef);\n\n      for (var key in predef) {\n        if (_.has(predef, key)) {\n          declared[key] = directiveToken;\n        }\n      }\n    }\n\n    if (directiveToken.type === \"exported\") {\n      body.forEach(function(e, idx) {\n        if (!e.length) {\n          // Ignore trailing comma\n          if (idx > 0 && idx === body.length - 1) {\n            return;\n          }\n          error(\"E002\", directiveToken);\n          return;\n        }\n\n        state.funct[\"(scope)\"].addExported(e);\n      });\n    }\n\n    if (directiveToken.type === \"members\") {\n      membersOnly = membersOnly || {};\n\n      body.forEach(function(m) {\n        var ch1 = m.charAt(0);\n        var ch2 = m.charAt(m.length - 1);\n\n        if (ch1 === ch2 && (ch1 === \"\\\"\" || ch1 === \"'\")) {\n          m = m\n            .substr(1, m.length - 2)\n            .replace(\"\\\\\\\"\", \"\\\"\");\n        }\n\n        membersOnly[m] = false;\n      });\n    }\n\n    var numvals = [\n      \"maxstatements\",\n      \"maxparams\",\n      \"maxdepth\",\n      \"maxcomplexity\",\n      \"maxerr\",\n      \"maxlen\",\n      \"indent\"\n    ];\n\n    if (directiveToken.type === \"jshint\" || directiveToken.type === \"jslint\" ||\n      directiveToken.type === \"jshint.unstable\") {\n      body.forEach(function(item) {\n        var parts = item.split(\":\");\n        var key = parts[0].trim();\n        var val = parts.length > 1 ? parts[1].trim() : \"\";\n        var numberVal;\n\n        if (!checkOption(key, directiveToken.type !== \"jshint.unstable\", directiveToken)) {\n          return;\n        }\n\n        if (numvals.indexOf(key) >= 0) {\n          // GH988 - numeric options can be disabled by setting them to `false`\n          if (val !== \"false\") {\n            numberVal = +val;\n\n            if (typeof numberVal !== \"number\" || !isFinite(numberVal) ||\n              numberVal <= 0 || Math.floor(numberVal) !== numberVal) {\n              error(\"E032\", directiveToken, val);\n              return;\n            }\n\n            state.option[key] = numberVal;\n          } else {\n            state.option[key] = key === \"indent\" ? 4 : false;\n          }\n\n          return;\n        }\n\n        if (key === \"validthis\") {\n          // `validthis` is valid only within a function scope.\n\n          if (state.funct[\"(global)\"])\n            return void error(\"E009\");\n\n          if (val !== \"true\" && val !== \"false\")\n            return void error(\"E002\", directiveToken);\n\n          state.option.validthis = (val === \"true\");\n          return;\n        }\n\n        if (key === \"quotmark\") {\n          switch (val) {\n          case \"true\":\n          case \"false\":\n            state.option.quotmark = (val === \"true\");\n            break;\n          case \"double\":\n          case \"single\":\n            state.option.quotmark = val;\n            break;\n          default:\n            error(\"E002\", directiveToken);\n          }\n          return;\n        }\n\n        if (key === \"shadow\") {\n          switch (val) {\n          case \"true\":\n            state.option.shadow = true;\n            break;\n          case \"outer\":\n            state.option.shadow = \"outer\";\n            break;\n          case \"false\":\n          case \"inner\":\n            state.option.shadow = \"inner\";\n            break;\n          default:\n            error(\"E002\", directiveToken);\n          }\n          return;\n        }\n\n        if (key === \"unused\") {\n          switch (val) {\n          case \"true\":\n            state.option.unused = true;\n            break;\n          case \"false\":\n            state.option.unused = false;\n            break;\n          case \"vars\":\n          case \"strict\":\n            state.option.unused = val;\n            break;\n          default:\n            error(\"E002\", directiveToken);\n          }\n          return;\n        }\n\n        if (key === \"latedef\") {\n          switch (val) {\n          case \"true\":\n            state.option.latedef = true;\n            break;\n          case \"false\":\n            state.option.latedef = false;\n            break;\n          case \"nofunc\":\n            state.option.latedef = \"nofunc\";\n            break;\n          default:\n            error(\"E002\", directiveToken);\n          }\n          return;\n        }\n\n        if (key === \"ignore\") {\n          switch (val) {\n          case \"line\":\n            state.ignoredLines[directiveToken.line] = true;\n            removeIgnoredMessages();\n            break;\n          default:\n            error(\"E002\", directiveToken);\n          }\n          return;\n        }\n\n        if (key === \"strict\") {\n          switch (val) {\n          case \"true\":\n            state.option.strict = true;\n            break;\n          case \"false\":\n            state.option.strict = false;\n            break;\n          case \"global\":\n          case \"implied\":\n            state.option.strict = val;\n            break;\n          default:\n            error(\"E002\", directiveToken);\n          }\n          return;\n        }\n\n        if (key === \"module\") {\n          /**\n           * TODO: Extend this restriction to *all* \"environmental\" options.\n           */\n          if (!hasParsedCode(state.funct)) {\n            error(\"E055\", directiveToken, \"module\");\n          }\n        }\n\n        if (key === \"esversion\") {\n          switch (val) {\n          case \"3\":\n          case \"5\":\n          case \"6\":\n          case \"7\":\n          case \"8\":\n          case \"9\":\n          case \"10\":\n          case \"11\":\n            state.option.moz = false;\n            state.option.esversion = +val;\n            break;\n          case \"2015\":\n          case \"2016\":\n          case \"2017\":\n          case \"2018\":\n          case \"2019\":\n          case \"2020\":\n            state.option.moz = false;\n            // Translate specification publication year to version number.\n            state.option.esversion = +val - 2009;\n            break;\n          default:\n            error(\"E002\", directiveToken);\n          }\n          if (!hasParsedCode(state.funct)) {\n            error(\"E055\", directiveToken, \"esversion\");\n          }\n          return;\n        }\n\n        var match = /^([+-])(W\\d{3})$/g.exec(key);\n        if (match) {\n          // ignore for -W..., unignore for +W...\n          state.ignored[match[2]] = (match[1] === \"-\");\n          return;\n        }\n\n        var tn;\n        if (val === \"true\" || val === \"false\") {\n          if (directiveToken.type === \"jslint\") {\n            tn = options.renamed[key] || key;\n            state.option[tn] = (val === \"true\");\n\n            if (options.inverted[tn] !== undefined) {\n              state.option[tn] = !state.option[tn];\n            }\n          } else if (directiveToken.type === \"jshint.unstable\") {\n            /* istanbul ignore next */\n            state.option.unstable[key] = (val === \"true\");\n          } else {\n            state.option[key] = (val === \"true\");\n          }\n\n          return;\n        }\n\n        error(\"E002\", directiveToken);\n      });\n\n      applyOptions();\n    }\n  }\n\n  /**\n   * Return a token beyond the token available in `state.tokens.next`. If no\n   * such token exists, return the \"(end)\" token. This function is used to\n   * determine parsing strategies in cases where the value of the next token\n   * does not provide sufficient information, as is the case with `for` loops,\n   * e.g.:\n   *\n   *     for ( var i in ...\n   *\n   * versus:\n   *\n   *     for ( var i = ...\n   *\n   * @param {number} [p] - offset of desired token; defaults to 0\n   *\n   * @returns {token}\n   */\n  function peek(p) {\n    var i = p || 0, j = lookahead.length, t;\n\n    if (i < j) {\n      return lookahead[i];\n    }\n\n    while (j <= i) {\n      t = lex.token();\n\n      // When the lexer is exhausted, this function should produce the \"(end)\"\n      // token, even in cases where the requested token is beyond the end of\n      // the input stream.\n      if (!t) {\n        // If the lookahead buffer is empty, the expected \"(end)\" token was\n        // already emitted by the most recent invocation of `advance` and is\n        // available as the next token.\n        if (!lookahead.length) {\n          return state.tokens.next;\n        }\n\n        return lookahead[j - 1];\n      }\n\n      lookahead[j] = t;\n      j += 1;\n    }\n\n    return t;\n  }\n\n  function peekIgnoreEOL() {\n    var i = 0;\n    var t;\n    do {\n      t = peek(i++);\n    } while (t.id === \"(endline)\");\n    return t;\n  }\n\n  /**\n   * Consume the next token.\n   *\n   * @param {string} [expected] - the expected value of the next token's `id`\n   *                              property (in the case of punctuators) or\n   *                              `value` property (in the case of identifiers\n   *                              and literals); if unspecified, any token will\n   *                              be accepted\n   * @param {object} [relatedToken] - the token that informed the expected\n   *                                  value, if any (for example: the opening\n   *                                  brace when a closing brace is expected);\n   *                                  used to produce more meaningful errors\n   */\n  function advance(expected, relatedToken) {\n    var nextToken = state.tokens.next;\n\n    if (expected && nextToken.id !== expected) {\n      if (relatedToken) {\n        if (nextToken.id === \"(end)\") {\n          error(\"E019\", relatedToken, relatedToken.id);\n        } else {\n          error(\"E020\", nextToken, expected, relatedToken.id,\n            relatedToken.line, nextToken.value);\n        }\n      } else if (nextToken.type !== \"(identifier)\" || nextToken.value !== expected) {\n        error(\"E021\", nextToken, expected, nextToken.value);\n      }\n    }\n\n    state.tokens.prev = state.tokens.curr;\n    state.tokens.curr = state.tokens.next;\n    for (;;) {\n      state.tokens.next = lookahead.shift() || lex.token();\n\n      if (!state.tokens.next) { // No more tokens left, give up\n        quit(\"E041\", state.tokens.curr);\n      }\n\n      if (state.tokens.next.id === \"(end)\" || state.tokens.next.id === \"(error)\") {\n        return;\n      }\n\n      if (state.tokens.next.check) {\n        state.tokens.next.check();\n      }\n\n      if (state.tokens.next.isSpecial) {\n        lintingDirective(state.tokens.next, state.tokens.curr);\n      } else {\n        if (state.tokens.next.id !== \"(endline)\") {\n          break;\n        }\n      }\n    }\n  }\n\n  /**\n   * Determine whether a given token is an operator.\n   *\n   * @param {token} token\n   *\n   * @returns {boolean}\n   */\n  function isOperator(token) {\n    return token.first || token.right || token.left || token.id === \"yield\" || token.id === \"await\";\n  }\n\n  function isEndOfExpr(context, curr, next) {\n    if (arguments.length <= 1) {\n      curr = state.tokens.curr;\n      next = state.tokens.next;\n    }\n\n    if (next.id === \"in\" && context & prodParams.noin) {\n      return true;\n    }\n\n    if (next.id === \";\" || next.id === \"}\" || next.id === \":\") {\n      return true;\n    }\n\n    if (next.infix === curr.infix ||\n      // Infix operators which follow `yield` should only be consumed as part\n      // of the current expression if allowed by the syntactic grammar. In\n      // effect, this prevents automatic semicolon insertion when `yield` is\n      // followed by a newline and a comma operator (without enabling it when\n      // `yield` is followed by a newline and a `[` token).\n      (curr.id === \"yield\" && curr.rbp < next.rbp)) {\n      return !sameLine(curr, next);\n    }\n\n    return false;\n  }\n\n  /**\n   * The `expression` function is the heart of JSHint's parsing behaior. It is\n   * based on the Pratt parser, but it extends that model with a `fud` method.\n   * Short for \"first null denotation,\" it it similar to the `nud` (\"null\n   * denotation\") function, but it is only used on the first token of a\n   * statement. This simplifies usage in statement-oriented languages like\n   * JavaScript.\n   *\n   * .nud  Null denotation\n   * .fud  First null denotation\n   * .led  Left denotation\n   *  lbp  Left binding power\n   *  rbp  Right binding power\n   *\n   * They are elements of the parsing method called Top Down Operator Precedence.\n   *\n   * In addition to parsing, this function applies a number of linting patterns.\n   *\n   * @param {number} context - the parsing context (a bitfield describing\n   *                           conditions of the current parsing operation\n   *                           which can influence how the next tokens are\n   *                           interpreted); see `prod-params.js` for more\n   *                           detail)\n   * @param {number} rbp - the right-binding power of the token to be consumed\n   */\n  function expression(context, rbp) {\n    var left, isArray = false, isObject = false;\n    var initial = context & prodParams.initial;\n    var curr;\n\n    context &= ~prodParams.initial;\n\n    state.nameStack.push();\n\n    if (state.tokens.next.id === \"(end)\")\n      error(\"E006\", state.tokens.curr);\n\n    advance();\n\n    if (initial) {\n      state.funct[\"(verb)\"] = state.tokens.curr.value;\n      state.tokens.curr.beginsStmt = true;\n    }\n\n    curr = state.tokens.curr;\n\n    if (initial && curr.fud && (!curr.useFud || curr.useFud(context))) {\n      left = state.tokens.curr.fud(context);\n    } else {\n      if (state.tokens.curr.nud) {\n        left = state.tokens.curr.nud(context, rbp);\n      } else {\n        error(\"E030\", state.tokens.curr, state.tokens.curr.id);\n      }\n\n      while (rbp < state.tokens.next.lbp && !isEndOfExpr(context)) {\n        isArray = state.tokens.curr.value === \"Array\";\n        isObject = state.tokens.curr.value === \"Object\";\n\n        // #527, new Foo.Array(), Foo.Array(), new Foo.Object(), Foo.Object()\n        // Line breaks in IfStatement heads exist to satisfy the checkJSHint\n        // \"Line too long.\" error.\n        if (left && (left.value || (left.first && left.first.value))) {\n          // If the left.value is not \"new\", or the left.first.value is a \".\"\n          // then safely assume that this is not \"new Array()\" and possibly\n          // not \"new Object()\"...\n          if (left.value !== \"new\" ||\n            (left.first && left.first.value && left.first.value === \".\")) {\n            isArray = false;\n            // ...In the case of Object, if the left.value and state.tokens.curr.value\n            // are not equal, then safely assume that this not \"new Object()\"\n            if (left.value !== state.tokens.curr.value) {\n              isObject = false;\n            }\n          }\n        }\n\n        advance();\n\n        if (isArray && state.tokens.curr.id === \"(\" && state.tokens.next.id === \")\") {\n          warning(\"W009\", state.tokens.curr);\n        }\n\n        if (isObject && state.tokens.curr.id === \"(\" && state.tokens.next.id === \")\") {\n          warning(\"W010\", state.tokens.curr);\n        }\n\n        if (left && state.tokens.curr.led) {\n          left = state.tokens.curr.led(context, left);\n        } else {\n          error(\"E033\", state.tokens.curr, state.tokens.curr.id);\n        }\n      }\n    }\n\n    state.nameStack.pop();\n\n    return left;\n  }\n\n\n  // Functions for conformance of style.\n\n  function sameLine(first, second) {\n    return first.line === (second.startLine || second.line);\n  }\n\n  function nobreaknonadjacent(left, right) {\n    if (!state.option.laxbreak && !sameLine(left, right)) {\n      warning(\"W014\", right, right.value);\n    }\n  }\n\n  function nolinebreak(t) {\n    if (!sameLine(t, state.tokens.next)) {\n      warning(\"E022\", t, t.value);\n    }\n  }\n\n  /**\n   * Validate the comma token in the \"current\" position of the token stream.\n   *\n   * @param {object} [opts]\n   * @param {boolean} [opts.property] - flag indicating whether the current\n   *                                    comma token is situated directly within\n   *                                    an object initializer\n   * @param {boolean} [opts.allowTrailing] - flag indicating whether the\n   *                                         current comma token may appear\n   *                                         directly before a delimiter\n   *\n   * @returns {boolean} flag indicating the validity of the current comma\n   *                    token; `false` if the token directly causes a syntax\n   *                    error, `true` otherwise\n   */\n  function checkComma(opts) {\n    var prev = state.tokens.prev;\n    var curr = state.tokens.curr;\n    opts = opts || {};\n\n    if (!sameLine(prev, curr)) {\n      if (!state.option.laxcomma) {\n        if (checkComma.first) {\n          warning(\"I001\", curr);\n          checkComma.first = false;\n        }\n        warning(\"W014\", prev, curr.value);\n      }\n    }\n\n    if (state.tokens.next.identifier && !(opts.property && state.inES5())) {\n      // Keywords that cannot follow a comma operator.\n      switch (state.tokens.next.value) {\n      case \"break\":\n      case \"case\":\n      case \"catch\":\n      case \"continue\":\n      case \"default\":\n      case \"do\":\n      case \"else\":\n      case \"finally\":\n      case \"for\":\n      case \"if\":\n      case \"in\":\n      case \"instanceof\":\n      case \"return\":\n      case \"switch\":\n      case \"throw\":\n      case \"try\":\n      case \"var\":\n      case \"let\":\n      case \"while\":\n      case \"with\":\n        error(\"E024\", state.tokens.next, state.tokens.next.value);\n        return false;\n      }\n    }\n\n    if (state.tokens.next.type === \"(punctuator)\") {\n      switch (state.tokens.next.value) {\n      case \"}\":\n      case \"]\":\n      case \",\":\n      case \")\":\n        if (opts.allowTrailing) {\n          return true;\n        }\n\n        error(\"E024\", state.tokens.next, state.tokens.next.value);\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * Factory function for creating \"symbols\"--objects that will be inherited by\n   * tokens. The objects created by this function are stored in a symbol table\n   * and set as the prototype of the tokens generated by the lexer.\n   *\n   * Note that this definition of \"symbol\" describes an implementation detail\n   * of JSHint and is not related to the ECMAScript value type introduced in\n   * ES2015.\n   *\n   * @param {string} s - the name of the token; for keywords (e.g. `void`) and\n   *                     delimiters (e.g.. `[`), this is the token's text\n   *                     representation; for literals (e.g. numbers) and other\n   *                     \"special\" tokens (e.g. the end-of-file marker) this is\n   *                     a parenthetical value\n   * @param {number} p - the left-binding power of the token as used by the\n   *                     Pratt parsing semantics\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function symbol(s, p) {\n    var x = state.syntax[s];\n    if (!x || typeof x !== \"object\") {\n      state.syntax[s] = x = {\n        id: s,\n        lbp: p,\n        // Symbols that accept a right-hand side do so with a binding power\n        // that is commonly identical to their left-binding power. (This value\n        // is relevant when determining if the grouping operator is necessary\n        // to override the precedence of surrounding operators.) Because the\n        // exponentiation operator's left-binding power and right-binding power\n        // are distinct, the values must be encoded separately.\n        rbp: p,\n        value: s\n      };\n    }\n    return x;\n  }\n\n  /**\n   * Convenience function for defining delimiter symbols.\n   *\n   * @param {string} s - the name of the symbol\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function delim(s) {\n    var x = symbol(s, 0);\n    x.delim = true;\n    return x;\n  }\n\n  /**\n   * Convenience function for defining statement-denoting symbols.\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} f - the first null denotation function for the symbol;\n   *                       see the `expression` function for more detail\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function stmt(s, f) {\n    var x = delim(s);\n    x.identifier = x.reserved = true;\n    x.fud = f;\n    return x;\n  }\n\n  /**\n   * Convenience function for defining block-statement-denoting symbols.\n   *\n   * A block-statement-denoting symbol is one like 'if' or 'for', which will be\n   * followed by a block and will not have to end with a semicolon.\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} - the first null denotation function for the symbol; see\n   *                     the `expression` function for more detail\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function blockstmt(s, f) {\n    var x = stmt(s, f);\n    x.block = true;\n    return x;\n  }\n  /**\n   * Denote a given JSHint symbol as an identifier and a reserved keyword.\n   *\n   * @param {object} - a JSHint symbol value\n   *\n   * @returns {object} - the provided object\n   */\n  function reserveName(x) {\n    var c = x.id.charAt(0);\n    if ((c >= \"a\" && c <= \"z\") || (c >= \"A\" && c <= \"Z\")) {\n      x.identifier = x.reserved = true;\n    }\n    return x;\n  }\n\n  /**\n   * Convenience function for defining \"prefix\" symbols--operators that accept\n   * expressions as a right-hand side.\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} [f] - the first null denotation function for the symbol;\n   *                         see the `expression` function for more detail\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function prefix(s, f) {\n    var x = symbol(s, 150);\n    reserveName(x);\n\n    x.nud = (typeof f === \"function\") ? f : function(context) {\n      this.arity = \"unary\";\n      this.right = expression(context, 150);\n\n      if (this.id === \"++\" || this.id === \"--\") {\n        if (state.option.plusplus) {\n          warning(\"W016\", this, this.id);\n        }\n\n        if (this.right) {\n          checkLeftSideAssign(context, this.right, this);\n        }\n      }\n\n      return this;\n    };\n\n    return x;\n  }\n\n  /**\n   * Convenience function for defining \"type\" symbols--those that describe\n   * literal values.\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} f - the first null denotation function for the symbol;\n   *                       see the `expression` function for more detail\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function type(s, f) {\n    var x = symbol(s, 0);\n    x.type = s;\n    x.nud = f;\n    return x;\n  }\n\n  /**\n   * Convenience function for defining JSHint symbols for reserved\n   * keywords--those that are restricted from use as bindings (and as property\n   * names in ECMAScript 3 environments).\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} func - the first null denotation function for the\n   *                          symbol; see the `expression` function for more\n   *                          detail\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function reserve(name, func) {\n    var x = type(name, func);\n    x.identifier = true;\n    x.reserved = true;\n    return x;\n  }\n\n  /**\n   * Convenience function for defining JSHint symbols for keywords that are\n   * only reserved in some circumstances.\n   *\n   * @param {string} name - the name of the symbol\n   * @param {object} [meta] - a collection of optional arguments\n   * @param {function} [meta.nud] -the null denotation function for the symbol;\n   *                   see the `expression` function for more detail\n   * @param {boolean} [meta.es5] - `true` if the identifier is reserved\n   *                               in ECMAScript 5 or later\n   * @param {boolean} [meta.strictOnly] - `true` if the identifier is only\n   *                                      reserved in strict mode code.\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function FutureReservedWord(name, meta) {\n    var x = type(name, state.syntax[\"(identifier)\"].nud);\n\n    meta = meta || {};\n    meta.isFutureReservedWord = true;\n\n    x.value = name;\n    x.identifier = true;\n    x.reserved = true;\n    x.meta = meta;\n\n    return x;\n  }\n\n  /**\n   * Convenience function for defining \"infix\" symbols--operators that require\n   * operands as both \"land-hand side\" and \"right-hand side\".\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} [f] - a function to be invoked that consumes the\n   *                         right-hand side of the operator\n   * @param {number} p - the left-binding power of the token as used by the\n   *                     Pratt parsing semantics\n   * @param {boolean} [w] - if `true`\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function infix(s, f, p, w) {\n    var x = symbol(s, p);\n    reserveName(x);\n    x.infix = true;\n    x.led = function(context, left) {\n      if (!w) {\n        nobreaknonadjacent(state.tokens.prev, state.tokens.curr);\n      }\n      if ((s === \"in\" || s === \"instanceof\") && left.id === \"!\") {\n        warning(\"W018\", left, \"!\");\n      }\n      if (typeof f === \"function\") {\n        return f(context, left, this);\n      } else {\n        this.left = left;\n        this.right = expression(context, p);\n        return this;\n      }\n    };\n    return x;\n  }\n\n  /**\n   * Convenience function for defining the `=>` token as used in arrow\n   * functions.\n   *\n   * @param {string} s - the name of the symbol\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function application(s) {\n    var x = symbol(s, 42);\n\n    x.infix = true;\n    x.led = function(context, left) {\n      nobreaknonadjacent(state.tokens.prev, state.tokens.curr);\n\n      this.left = left;\n      this.right = doFunction(context, { type: \"arrow\", loneArg: left });\n      return this;\n    };\n    return x;\n  }\n\n  /**\n   * Convenience function for defining JSHint symbols for relation operators.\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} [f] - a function to be invoked to enforce any additional\n   *                         linting rules.\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function relation(s, f) {\n    var x = symbol(s, 100);\n\n    x.infix = true;\n    x.led = function(context, left) {\n      nobreaknonadjacent(state.tokens.prev, state.tokens.curr);\n      this.left = left;\n      var right = this.right = expression(context, 100);\n\n      if (isIdentifier(left, \"NaN\") || isIdentifier(right, \"NaN\")) {\n        warning(\"W019\", this);\n      } else if (f) {\n        f.apply(this, [context, left, right]);\n      }\n\n      if (!left || !right) {\n        quit(\"E041\", state.tokens.curr);\n      }\n\n      if (left.id === \"!\") {\n        warning(\"W018\", left, \"!\");\n      }\n\n      if (right.id === \"!\") {\n        warning(\"W018\", right, \"!\");\n      }\n\n      return this;\n    };\n    return x;\n  }\n\n  /**\n   * Determine if a given token marks the beginning of a UnaryExpression.\n   *\n   * @param {object} token\n   *\n   * @returns {boolean}\n   */\n  function beginsUnaryExpression(token) {\n    return token.arity === \"unary\" && token.id !== \"++\" && token.id !== \"--\";\n  }\n\n  var typeofValues = {};\n  typeofValues.legacy = [\n    // E4X extended the `typeof` operator to return \"xml\" for the XML and\n    // XMLList types it introduced.\n    // Ref: 11.3.2 The typeof Operator\n    // http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-357.pdf\n    \"xml\",\n    // IE<9 reports \"unknown\" when the `typeof` operator is applied to an\n    // object existing across a COM+ bridge. In lieu of official documentation\n    // (which does not exist), see:\n    // http://robertnyman.com/2005/12/21/what-is-typeof-unknown/\n    \"unknown\"\n  ];\n  typeofValues.es3 = [\n    \"undefined\", \"boolean\", \"number\", \"string\", \"function\", \"object\",\n  ];\n  typeofValues.es3 = typeofValues.es3.concat(typeofValues.legacy);\n  typeofValues.es6 = typeofValues.es3.concat(\"symbol\", \"bigint\");\n\n  /**\n   * Validate comparisons between the result of a `typeof` expression and a\n   * string literal.\n   *\n   * @param {token} [left] - one of the values being compared\n   * @param {token} [right] - the other value being compared\n   * @param {object} state - the global state object (see `state.js`)\n   *\n   * @returns {boolean} - `false` if the second token describes a `typeof`\n   *                       expression and the first token is a string literal\n   *                       whose value is never returned by that operator;\n   *                       `true` otherwise\n   */\n  function isTypoTypeof(left, right, state) {\n    var values;\n\n    if (state.option.notypeof)\n      return false;\n\n    if (!left || !right)\n      return false;\n\n    values = state.inES6() ? typeofValues.es6 : typeofValues.es3;\n\n    if (right.type === \"(identifier)\" && right.value === \"typeof\" && left.type === \"(string)\") {\n      if (left.value === \"bigint\") {\n        if (!state.inES11()) {\n          warning(\"W119\", left, \"BigInt\", \"11\");\n        }\n\n        return false;\n      }\n\n      return !_.includes(values, left.value);\n    }\n\n    return false;\n  }\n\n  /**\n   * Determine if a given token describes the built-in `eval` function.\n   *\n   * @param {token} left\n   * @param {object} state - the global state object (see `state.js`)\n   *\n   * @returns {boolean}\n   */\n  function isGlobalEval(left, state) {\n    var isGlobal = false;\n\n    // permit methods to refer to an \"eval\" key in their own context\n    if (left.type === \"this\" && state.funct[\"(context)\"] === null) {\n      isGlobal = true;\n    }\n    // permit use of \"eval\" members of objects\n    else if (left.type === \"(identifier)\") {\n      if (state.option.node && left.value === \"global\") {\n        isGlobal = true;\n      }\n\n      else if (state.option.browser && (left.value === \"window\" || left.value === \"document\")) {\n        isGlobal = true;\n      }\n    }\n\n    return isGlobal;\n  }\n\n  /**\n   * Determine if a given token describes a property of a built-in object.\n   *\n   * @param {token} left\n   *\n   * @returns {boolean}\n   */\n  function findNativePrototype(left) {\n    var natives = [\n      \"Array\", \"ArrayBuffer\", \"Boolean\", \"Collator\", \"DataView\", \"Date\",\n      \"DateTimeFormat\", \"Error\", \"EvalError\", \"Float32Array\", \"Float64Array\",\n      \"Function\", \"Infinity\", \"Intl\", \"Int16Array\", \"Int32Array\", \"Int8Array\",\n      \"Iterator\", \"Number\", \"NumberFormat\", \"Object\", \"RangeError\",\n      \"ReferenceError\", \"RegExp\", \"StopIteration\", \"String\", \"SyntaxError\",\n      \"TypeError\", \"Uint16Array\", \"Uint32Array\", \"Uint8Array\", \"Uint8ClampedArray\",\n      \"URIError\"\n    ];\n\n    function walkPrototype(obj) {\n      if (typeof obj !== \"object\") return;\n      return obj.right === \"prototype\" ? obj : walkPrototype(obj.left);\n    }\n\n    function walkNative(obj) {\n      while (!obj.identifier && typeof obj.left === \"object\")\n        obj = obj.left;\n\n      if (obj.identifier && natives.indexOf(obj.value) >= 0 &&\n          state.funct[\"(scope)\"].isPredefined(obj.value)) {\n        return obj.value;\n      }\n    }\n\n    var prototype = walkPrototype(left);\n    if (prototype) return walkNative(prototype);\n  }\n\n  /**\n   * Determine if the given token is a valid assignment target; emit errors\n   * and/or warnings as appropriate\n   *\n   * @param {number} context - the parsing context; see `prod-params.js` for\n   *                           more information\n   * @param {token} left - the left hand side of the assignment\n   * @param {token=} assignToken - the token for the assignment, used for\n   *                               reporting\n   * @param {object=} options - optional object\n   * @param {boolean} options.allowDestructuring - whether to allow\n   *                                               destructuring binding\n   *\n   * @returns {boolean} Whether the left hand side is OK\n   */\n  function checkLeftSideAssign(context, left, assignToken, options) {\n\n    var allowDestructuring = options && options.allowDestructuring;\n\n    assignToken = assignToken || left;\n\n    if (state.option.freeze) {\n      var nativeObject = findNativePrototype(left);\n      if (nativeObject)\n        warning(\"W121\", left, nativeObject);\n    }\n\n    if (left.identifier && !left.isMetaProperty) {\n      // The `reassign` method also calls `modify`, but we are specific in\n      // order to catch function re-assignment and globals re-assignment\n      state.funct[\"(scope)\"].block.reassign(left.value, left);\n    }\n\n    if (left.id === \".\") {\n      if (!left.left || left.left.value === \"arguments\" && !state.isStrict()) {\n        warning(\"W143\", assignToken);\n      }\n\n      state.nameStack.set(state.tokens.prev);\n      return true;\n    } else if (left.id === \"{\" || left.id === \"[\") {\n      if (!allowDestructuring || !left.destructAssign) {\n        if (left.id === \"{\" || !left.left) {\n          warning(\"E031\", assignToken);\n        } else if (left.left.value === \"arguments\" && !state.isStrict()) {\n          warning(\"W143\", assignToken);\n        }\n      }\n\n      if (left.id === \"[\") {\n        state.nameStack.set(left.right);\n      }\n\n      return true;\n    } else if (left.identifier && !isReserved(context, left) && !left.isMetaProperty) {\n      if (state.funct[\"(scope)\"].bindingtype(left.value) === \"exception\") {\n        warning(\"W022\", left);\n      }\n\n      if (left.value === \"eval\" && state.isStrict()) {\n        error(\"E031\", assignToken);\n        return false;\n      } else if (left.value === \"arguments\") {\n        if (!state.isStrict()) {\n          warning(\"W143\", assignToken);\n        } else {\n          error(\"E031\", assignToken);\n          return false;\n        }\n      }\n      state.nameStack.set(left);\n      return true;\n    }\n\n    error(\"E031\", assignToken);\n\n    return false;\n  }\n\n  /**\n   * Convenience function for defining JSHint symbols for assignment operators.\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} [f] - a function to be invoked that consumes the\n   *                         right-hand side of the operator (see the `infix`\n   *                         function)\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function assignop(s, f) {\n    var x = infix(s, typeof f === \"function\" ? f : function(context, left, that) {\n      that.left = left;\n\n      checkLeftSideAssign(context, left, that, { allowDestructuring: true });\n\n      that.right = expression(context, 10);\n\n      return that;\n    }, 20);\n\n    x.exps = true;\n    x.assign = true;\n\n    return x;\n  }\n\n  /**\n   * Convenience function for defining JSHint symbols for bitwise operators.\n   *\n   * @param {string} s - the name of the symbol\n   * @param {function} [f] - the left denotation function for the symbol; see\n   *                         the `expression` function for more detail\n   * @param {number} p - the left-binding power of the token as used by the\n   *                     Pratt parsing semantics\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function bitwise(s, f, p) {\n    var x = symbol(s, p);\n    reserveName(x);\n    x.infix = true;\n    x.led = (typeof f === \"function\") ? f : function(context, left) {\n      if (state.option.bitwise) {\n        warning(\"W016\", this, this.id);\n      }\n      this.left = left;\n      this.right = expression(context, p);\n      return this;\n    };\n    return x;\n  }\n\n  /**\n   * Convenience function for defining JSHint symbols for bitwise assignment\n   * operators. See the `assignop` function for more detail.\n   *\n   * @param {string} s - the name of the symbol\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function bitwiseassignop(s) {\n    symbol(s, 20).exps = true;\n    return infix(s, function(context, left, that) {\n      if (state.option.bitwise) {\n        warning(\"W016\", that, that.id);\n      }\n\n      checkLeftSideAssign(context, left, that);\n\n      that.right = expression(context, 10);\n\n      return that;\n    }, 20);\n  }\n\n  /**\n   * Convenience function for defining JSHint symbols for those operators which\n   * have a single operand that appears before them in the source code.\n   *\n   * @param {string} s - the name of the symbol\n   *\n   * @returns {object} - the object describing the JSHint symbol (provided to\n   *                     support cases where further refinement is necessary)\n   */\n  function suffix(s) {\n    var x = symbol(s, 150);\n\n    x.led = function(context, left) {\n      // this = suffix e.g. \"++\" punctuator\n      // left = symbol operated e.g. \"a\" identifier or \"a.b\" punctuator\n      if (state.option.plusplus) {\n        warning(\"W016\", this, this.id);\n      }\n\n      checkLeftSideAssign(context, left, this);\n\n      this.left = left;\n      return this;\n    };\n    return x;\n  }\n\n  /**\n   * Retrieve the value of the next token if it is an identifier and optionally\n   * advance the parser.\n   *\n   * @param {number} context - the parsing context; see `prod-params.js` for\n   *                           more information\n   * @param {boolean} [isName] - `true` if an IdentifierName should be consumed\n   *                             (e.g. object properties)\n   * @param {boolean} [preserve] - `true` if the token should not be consumed\n   *\n   * @returns {string|undefined} - the value of the identifier, if present\n   */\n  function optionalidentifier(context, isName, preserve) {\n    if (!state.tokens.next.identifier) {\n      return;\n    }\n\n    if (!preserve) {\n      advance();\n    }\n\n    var curr = state.tokens.curr;\n\n    if (isReserved(context, curr) && !(isName && state.inES5())) {\n      warning(\"W024\", state.tokens.curr, state.tokens.curr.id);\n    }\n\n    return curr.value;\n  }\n\n  /**\n   * Consume the \"...\" token which designates \"spread\" and \"rest\" operations if\n   * it is present. If the operator is repeated, consume every repetition, and\n   * issue a single error describing the syntax error.\n   *\n   * @param {string} operation - either \"spread\" or \"rest\"\n   *\n   * @returns {boolean} a value describing whether or not any tokens were\n   *                    consumed in this way\n   */\n  function spreadrest(operation) {\n    if (!checkPunctuator(state.tokens.next, \"...\")) {\n      return false;\n    }\n\n    if (!state.inES6(true)) {\n      warning(\"W119\", state.tokens.next, operation + \" operator\", \"6\");\n    }\n    advance();\n\n    if (checkPunctuator(state.tokens.next, \"...\")) {\n      warning(\"E024\", state.tokens.next, \"...\");\n      while (checkPunctuator(state.tokens.next, \"...\")) {\n        advance();\n      }\n    }\n\n    return true;\n  }\n\n  /**\n   * Ensure that the current token is an identifier and retrieve its value.\n   *\n   * @param {number} context - the parsing context; see `prod-params.js` for\n   *                           more information\n   * @param {boolean} [isName] - `true` if an IdentifierName should be consumed\n   *                             (e.g. object properties)\n   *\n   * @returns {string|undefined} - the value of the identifier, if present\n   */\n  function identifier(context, isName) {\n    var i = optionalidentifier(context, isName, false);\n    if (i) {\n      return i;\n    }\n\n    error(\"E030\", state.tokens.next, state.tokens.next.value);\n\n    // The token should be consumed after a warning is issued so the parser\n    // can continue as though an identifier were found. The semicolon token\n    // should not be consumed in this way so that the parser interprets it as\n    // a statement delimiter;\n    if (state.tokens.next.id !== \";\") {\n      advance();\n    }\n  }\n\n\n  /**\n   * Determine if the provided token may be evaluated and emit a linting\n   * warning if this is note the case.\n   *\n   * @param {token} controlToken\n   */\n  function reachable(controlToken) {\n    var i = 0, t;\n    if (state.tokens.next.id !== \";\" || controlToken.inBracelessBlock) {\n      return;\n    }\n    for (;;) {\n      do {\n        t = peek(i);\n        i += 1;\n      } while (t.id !== \"(end)\" && t.id === \"(comment)\");\n\n      if (t.reach) {\n        return;\n      }\n      if (t.id !== \"(endline)\") {\n        if (t.id === \"function\") {\n          if (state.option.latedef === true) {\n            warning(\"W026\", t);\n          }\n          break;\n        }\n\n        warning(\"W027\", t, t.value, controlToken.value);\n        break;\n      }\n    }\n  }\n\n  /**\n   * Consume the semicolon that delimits the statement currently being parsed,\n   * emitting relevant warnings/errors as appropriate.\n   *\n   * @param {token} stmt - token describing the statement under consideration\n   */\n  function parseFinalSemicolon(stmt) {\n    if (state.tokens.next.id !== \";\") {\n      // don't complain about unclosed templates / strings\n      if (state.tokens.next.isUnclosed) return advance();\n\n      var isSameLine = sameLine(state.tokens.curr, state.tokens.next) &&\n                       state.tokens.next.id !== \"(end)\";\n      var blockEnd = checkPunctuator(state.tokens.next, \"}\");\n\n      if (isSameLine && !blockEnd && !(stmt.id === \"do\" && state.inES6(true))) {\n        errorAt(\"E058\", state.tokens.curr.line, state.tokens.curr.character);\n      } else if (!state.option.asi) {\n\n        // If this is the last statement in a block that ends on the same line\n        // *and* option lastsemic is on, ignore the warning.  Otherwise, issue\n        // a warning about missing semicolon.\n        if (!(blockEnd && isSameLine && state.option.lastsemic)) {\n          warningAt(\"W033\", state.tokens.curr.line, state.tokens.curr.character);\n        }\n      }\n    } else {\n      advance(\";\");\n    }\n  }\n\n  /**\n   * Consume a statement.\n   *\n   * @param {number} context - the parsing context; see `prod-params.js` for\n   *                           more information\n   *\n   * @returns {token} - the token describing the statement\n   */\n  function statement(context) {\n    var i = indent, r, t = state.tokens.next, hasOwnScope = false;\n\n    context |= prodParams.initial;\n\n    if (t.id === \";\") {\n      advance(\";\");\n      return;\n    }\n\n    // Is this a labelled statement?\n    var res = isReserved(context, t);\n\n    // We're being more tolerant here: if someone uses\n    // a FutureReservedWord (that is not meant to start a statement)\n    // as a label, we warn but proceed anyway.\n\n    if (res && t.meta && t.meta.isFutureReservedWord && !t.fud) {\n      warning(\"W024\", t, t.id);\n      res = false;\n    }\n\n    if (t.identifier && !res && peek().id === \":\") {\n      advance();\n      advance(\":\");\n\n      hasOwnScope = true;\n      state.funct[\"(scope)\"].stack();\n      state.funct[\"(scope)\"].block.addLabel(t.value, { token: state.tokens.curr });\n\n      if (!state.tokens.next.labelled && state.tokens.next.value !== \"{\") {\n        warning(\"W028\", state.tokens.next, t.value, state.tokens.next.value);\n      }\n\n      t = state.tokens.next;\n    }\n\n    // Is it a lonely block?\n\n    if (t.id === \"{\") {\n      // Is it a switch case block?\n      //\n      //  switch (foo) {\n      //    case bar: { <= here.\n      //      ...\n      //    }\n      //  }\n      var iscase = (state.funct[\"(verb)\"] === \"case\" && state.tokens.curr.value === \":\");\n      block(context, true, true, false, false, iscase);\n\n      if (hasOwnScope) {\n        state.funct[\"(scope)\"].unstack();\n      }\n\n      return;\n    }\n\n    // Parse the statement.\n\n    r = expression(context, 0);\n\n    if (r && !(r.identifier && r.value === \"function\") &&\n        !(r.type === \"(punctuator)\" && r.left &&\n          r.left.identifier && r.left.value === \"function\")) {\n      if (!state.isStrict() && state.stmtMissingStrict()) {\n        warning(\"E007\");\n      }\n    }\n\n    // Look for the final semicolon.\n\n    if (!t.block) {\n      if (!state.option.expr && (!r || !r.exps)) {\n        warning(\"W030\", state.tokens.curr);\n      } else if (state.option.nonew && r && r.left && r.id === \"(\" && r.left.id === \"new\") {\n        warning(\"W031\", t);\n      }\n\n      parseFinalSemicolon(t);\n    }\n\n\n    // Restore the indentation.\n\n    indent = i;\n    if (hasOwnScope) {\n      state.funct[\"(scope)\"].unstack();\n    }\n    return r;\n  }\n\n  /**\n   * Consume a series of statements until encountering either the end of the\n   * program or a token that interrupts control flow.\n   *\n   * @param {number} context - the parsing context; see `prod-params.js` for\n   *                           more information\n   *\n   * @returns {Array<token>} - the tokens consumed\n   */\n  function statements(context) {\n    var a = [], p;\n\n    while (!state.tokens.next.reach && state.tokens.next.id !== \"(end)\") {\n      if (state.tokens.next.id === \";\") {\n        p = peek();\n\n        if (!p || (p.id !== \"(\" && p.id !== \"[\")) {\n          warning(\"W032\");\n        }\n\n        advance(\";\");\n      } else {\n        a.push(statement(context));\n      }\n    }\n    return a;\n  }\n\n\n  /**\n   * Parse any directives in a directive prologue.\n   */\n  function directives() {\n    var current = state.tokens.next;\n    while (state.tokens.next.id === \"(string)\") {\n      var next = peekIgnoreEOL();\n      if (!isEndOfExpr(0, current, next)) {\n        break;\n      }\n      current = next;\n\n      advance();\n      var directive = state.tokens.curr.value;\n      if (state.directive[directive] ||\n          (directive === \"use strict\" && state.option.strict === \"implied\")) {\n        warning(\"W034\", state.tokens.curr, directive);\n      }\n\n      // From ECMAScript 2016:\n      //\n      // > 14.1.2 Static Semantics: Early Errors\n      // >\n      // > [...]\n      // > - It is a Syntax Error if ContainsUseStrict of FunctionBody is true\n      // >   and IsSimpleParameterList of FormalParameters is false.\n      if (directive === \"use strict\" && state.inES7() &&\n        !state.funct[\"(global)\"] && state.funct[\"(hasSimpleParams)\"] === false) {\n        error(\"E065\", state.tokens.curr);\n      }\n\n      state.directive[directive] = state.tokens.curr;\n\n      parseFinalSemicolon(current);\n    }\n\n    if (state.isStrict()) {\n      state.option.undef = true;\n    }\n  }\n\n  /**\n   * Parses a single block. A block is a sequence of statements wrapped in\n   * braces.\n   *\n   * @param {number} context - parsing context\n   * @param {boolean} ordinary - `true` for everything but function bodies and\n   *                             try blocks\n   * @param {boolean} [stmt] - `true` if block can be a single statement (e.g.\n   *                           in if/for/while)\n   * @param {boolean} [isfunc] - `true` if block is a function body\n   * @param {boolean} [isfatarrow] - `true` if its a body of a fat arrow\n   *                                 function\n   * @param {boolean} [iscase] - `true` if block is a switch case block\n   *\n   * @returns {token} - the token describing the block\n   */\n  function block(context, ordinary, stmt, isfunc, isfatarrow, iscase) {\n    var a,\n      b = inblock,\n      old_indent = indent,\n      m,\n      t,\n      d;\n\n    inblock = ordinary;\n\n    t = state.tokens.next;\n\n    var metrics = state.funct[\"(metrics)\"];\n    metrics.nestedBlockDepth += 1;\n    metrics.verifyMaxNestedBlockDepthPerFunction();\n\n    if (state.tokens.next.id === \"{\") {\n      advance(\"{\");\n\n      // create a new block scope\n      state.funct[\"(scope)\"].stack();\n\n      if (state.tokens.next.id !== \"}\") {\n        indent += state.option.indent;\n        while (!ordinary && state.tokens.next.from > indent) {\n          indent += state.option.indent;\n        }\n\n        if (isfunc) {\n          m = {};\n          for (d in state.directive) {\n            m[d] = state.directive[d];\n          }\n          directives();\n\n          state.funct[\"(isStrict)\"] = state.isStrict();\n\n          if (state.option.strict && state.funct[\"(context)\"][\"(global)\"]) {\n            if (!m[\"use strict\"] && !state.isStrict()) {\n              warning(\"E007\");\n            }\n          }\n        }\n\n        a = statements(context);\n\n        metrics.statementCount += a.length;\n\n        indent -= state.option.indent;\n      } else if (isfunc) {\n        // Ensure property is set for functions with empty bodies.\n        state.funct[\"(isStrict)\"] = state.isStrict();\n      }\n\n      advance(\"}\", t);\n\n      if (isfunc) {\n        state.funct[\"(scope)\"].validateParams(isfatarrow);\n        if (m) {\n          state.directive = m;\n        }\n      }\n\n      state.funct[\"(scope)\"].unstack();\n\n      indent = old_indent;\n    } else if (!ordinary) {\n      if (isfunc) {\n        state.funct[\"(scope)\"].stack();\n\n        if (stmt && !isfatarrow && !state.inMoz()) {\n          error(\"W118\", state.tokens.curr, \"function closure expressions\");\n        }\n\n        if (isfatarrow) {\n          state.funct[\"(scope)\"].validateParams(true);\n        }\n\n        var expr = expression(context, 10);\n\n        if (state.option.noreturnawait && context & prodParams.async &&\n            expr.identifier && expr.value === \"await\") {\n          warning(\"W146\", expr);\n        }\n\n        if (state.option.strict && state.funct[\"(context)\"][\"(global)\"]) {\n          if (!state.isStrict()) {\n            warning(\"E007\");\n          }\n        }\n\n        state.funct[\"(scope)\"].unstack();\n      } else {\n        error(\"E021\", state.tokens.next, \"{\", state.tokens.next.value);\n      }\n    } else {\n\n      state.funct[\"(scope)\"].stack();\n\n      if (!stmt || state.option.curly) {\n        warning(\"W116\", state.tokens.next, \"{\", state.tokens.next.value);\n      }\n\n      // JSHint observes Annex B of the ECMAScript specification by default,\n      // where function declarations are permitted in the statement positions\n      // of IfStatements.\n      var supportsFnDecl = state.funct[\"(verb)\"] === \"if\" ||\n        state.tokens.curr.id === \"else\";\n\n      state.tokens.next.inBracelessBlock = true;\n      indent += state.option.indent;\n      // test indentation only if statement is in new line\n      a = [statement(context)];\n      indent -= state.option.indent;\n\n      if (a[0] && a[0].declaration &&\n        !(supportsFnDecl && a[0].id === \"function\")) {\n        error(\"E048\", a[0], a[0].id[0].toUpperCase() + a[0].id.slice(1));\n      }\n\n      state.funct[\"(scope)\"].unstack();\n    }\n\n    // Don't clear and let it propagate out if it is \"break\", \"return\" or\n    // similar in switch case\n    switch (state.funct[\"(verb)\"]) {\n    case \"break\":\n    case \"continue\":\n    case \"return\":\n    case \"throw\":\n      if (iscase) {\n        break;\n      }\n\n      /* falls through */\n    default:\n      state.funct[\"(verb)\"] = null;\n    }\n\n    inblock = b;\n    if (ordinary && state.option.noempty && (!a || a.length === 0)) {\n      warning(\"W035\", state.tokens.prev);\n    }\n    metrics.nestedBlockDepth -= 1;\n    return a;\n  }\n\n\n  /**\n   * Update the global state which tracks all statically-identifiable property\n   * names, and emit a warning if the `members` linting directive is in use and\n   * does not include the given name.\n   *\n   * @param {string} m - the property name\n   */\n  function countMember(m) {\n    if (membersOnly && typeof membersOnly[m] !== \"boolean\") {\n      warning(\"W036\", state.tokens.curr, m);\n    }\n    if (typeof member[m] === \"number\") {\n      member[m] += 1;\n    } else {\n      member[m] = 1;\n    }\n  }\n\n  // Build the syntax table by declaring the syntactic elements of the language.\n\n  type(\"(number)\", function() {\n    if (state.tokens.next.id === \".\") {\n      warning(\"W005\", this);\n    }\n\n    return this;\n  });\n\n  type(\"(string)\", function() {\n    return this;\n  });\n\n  state.syntax[\"(identifier)\"] = {\n    type: \"(identifier)\",\n    lbp: 0,\n    identifier: true,\n\n    nud: function(context) {\n      var v = this.value;\n      // If this identifier is the lone parameter to a shorthand \"fat arrow\"\n      // function definition, i.e.\n      //\n      //     x => x;\n      //\n      // ...it should not be considered as a variable in the current scope. It\n      // will be added to the scope of the new function when the next token is\n      // parsed, so it can be safely ignored for now.\n      var isLoneArrowParam = state.tokens.next.id === \"=>\";\n\n      if (isReserved(context, this)) {\n        warning(\"W024\", this, v);\n      } else if (!isLoneArrowParam && !state.funct[\"(comparray)\"].check(v)) {\n        state.funct[\"(scope)\"].block.use(v, state.tokens.curr);\n      }\n\n      return this;\n    },\n\n    led: function() {\n      /* istanbul ignore next */\n      error(\"E033\", state.tokens.next, state.tokens.next.value);\n    }\n  };\n\n  var baseTemplateSyntax = {\n    identifier: false,\n    template: true,\n  };\n  state.syntax[\"(template)\"] = _.extend({\n    lbp: 155,\n    type: \"(template)\",\n    nud: doTemplateLiteral,\n    led: doTemplateLiteral,\n    noSubst: false\n  }, baseTemplateSyntax);\n\n  state.syntax[\"(template middle)\"] = _.extend({\n    lbp: 0,\n    type: \"(template middle)\",\n    noSubst: false\n  }, baseTemplateSyntax);\n\n  state.syntax[\"(template tail)\"] = _.extend({\n    lbp: 0,\n    type: \"(template tail)\",\n    tail: true,\n    noSubst: false\n  }, baseTemplateSyntax);\n\n  state.syntax[\"(no subst template)\"] = _.extend({\n    lbp: 155,\n    type: \"(template)\",\n    nud: doTemplateLiteral,\n    led: doTemplateLiteral,\n    noSubst: true,\n    tail: true // mark as tail, since it's always the last component\n  }, baseTemplateSyntax);\n\n  type(\"(regexp)\", function() {\n    return this;\n  });\n\n  // ECMAScript parser\n\n  delim(\"(endline)\");\n  (function(x) {\n    x.line = x.from = 0;\n  })(delim(\"(begin)\"));\n  delim(\"(end)\").reach = true;\n  delim(\"(error)\").reach = true;\n  delim(\"}\").reach = true;\n  delim(\")\");\n  delim(\"]\");\n  delim(\"\\\"\").reach = true;\n  delim(\"'\").reach = true;\n  delim(\";\");\n  delim(\":\").reach = true;\n  delim(\"#\");\n\n  reserve(\"else\");\n  reserve(\"case\").reach = true;\n  reserve(\"catch\");\n  reserve(\"default\").reach = true;\n  reserve(\"finally\");\n  reserve(\"true\", function() { return this; });\n  reserve(\"false\", function() { return this; });\n  reserve(\"null\", function() { return this; });\n  reserve(\"this\", function() {\n    if (state.isStrict() && !isMethod() &&\n        !state.option.validthis && ((state.funct[\"(statement)\"] &&\n        state.funct[\"(name)\"].charAt(0) > \"Z\") || state.funct[\"(global)\"])) {\n      warning(\"W040\", this);\n    }\n\n    return this;\n  });\n\n  (function(superSymbol) {\n    superSymbol.rbp = 161;\n  })(reserve(\"super\", function() {\n    superNud.call(state.tokens.curr, this);\n\n    return this;\n  }));\n\n  assignop(\"=\", \"assign\");\n  assignop(\"+=\", \"assignadd\");\n  assignop(\"-=\", \"assignsub\");\n  assignop(\"*=\", \"assignmult\");\n  assignop(\"/=\", \"assigndiv\").nud = function() {\n    /* istanbul ignore next */\n    error(\"E014\");\n  };\n  assignop(\"%=\", \"assignmod\");\n  assignop(\"**=\", function(context, left, that) {\n    if (!state.inES7()) {\n      warning(\"W119\", that, \"Exponentiation operator\", \"7\");\n    }\n\n    that.left = left;\n\n    checkLeftSideAssign(context, left, that);\n\n    that.right = expression(context, 10);\n\n    return that;\n  });\n\n  bitwiseassignop(\"&=\");\n  bitwiseassignop(\"|=\");\n  bitwiseassignop(\"^=\");\n  bitwiseassignop(\"<<=\");\n  bitwiseassignop(\">>=\");\n  bitwiseassignop(\">>>=\");\n  infix(\",\", function(context, left, that) {\n    if (state.option.nocomma) {\n      warning(\"W127\", that);\n    }\n\n    that.left = left;\n\n    if (checkComma()) {\n      that.right = expression(context, 10);\n    } else {\n      that.right = null;\n    }\n\n    return that;\n  }, 10, true);\n\n  infix(\"?\", function(context, left, that) {\n    increaseComplexityCount();\n    that.left = left;\n    that.right = expression(context & ~prodParams.noin, 10);\n    advance(\":\");\n    expression(context, 10);\n    return that;\n  }, 30);\n\n  infix(\"||\", function(context, left, that) {\n    increaseComplexityCount();\n    that.left = left;\n    that.right = expression(context, 40);\n    return that;\n  }, 40);\n\n  var andPrecedence = 50;\n  infix(\"&&\", function(context, left, that) {\n    increaseComplexityCount();\n    that.left = left;\n    that.right = expression(context, andPrecedence);\n    return that;\n  }, andPrecedence);\n\n  infix(\"??\", function(context, left, that) {\n    if (!left.paren && (left.id === \"||\" || left.id === \"&&\")) {\n      error(\"E024\", that, \"??\");\n    }\n\n    if (!state.inES11()) {\n      warning(\"W119\", that, \"nullish coalescing\", \"11\");\n    }\n\n    increaseComplexityCount();\n    that.left = left;\n    var right = that.right = expression(context, 39);\n\n    if (!right) {\n      error(\"E024\", state.tokens.next, state.tokens.next.id);\n    } else if (!right.paren && (right.id === \"||\" || right.id === \"&&\")) {\n      error(\"E024\", that.right, that.right.id);\n    }\n\n    return that;\n  }, 39);\n\n  // The Exponentiation operator, introduced in ECMAScript 2016\n  //\n  // ExponentiationExpression[Yield] :\n  //   UnaryExpression[?Yield]\n  //   UpdateExpression[?Yield] ** ExponentiationExpression[?Yield]\n  infix(\"**\", function(context, left, that) {\n    if (!state.inES7()) {\n      warning(\"W119\", that, \"Exponentiation operator\", \"7\");\n    }\n\n    // Disallow UnaryExpressions which are not wrapped in parenthesis\n    if (!left.paren && beginsUnaryExpression(left)) {\n      error(\"E024\", that, \"**\");\n    }\n\n    that.left = left;\n    that.right = expression(context, that.rbp);\n    return that;\n  }, 150);\n  state.syntax[\"**\"].rbp = 140;\n  bitwise(\"|\", \"bitor\", 70);\n  bitwise(\"^\", \"bitxor\", 80);\n  bitwise(\"&\", \"bitand\", 90);\n  relation(\"==\", function(context, left, right) {\n    var eqnull = state.option.eqnull &&\n      ((left && left.value) === \"null\" || (right && right.value) === \"null\");\n\n    switch (true) {\n      case !eqnull && state.option.eqeqeq:\n        this.from = this.character;\n        warning(\"W116\", this, \"===\", \"==\");\n        break;\n      /* istanbul ignore next */\n      case isTypoTypeof(right, left, state):\n        warning(\"W122\", this, right.value);\n        break;\n      case isTypoTypeof(left, right, state):\n        warning(\"W122\", this, left.value);\n        break;\n    }\n\n    return this;\n  });\n  relation(\"===\", function(context, left, right) {\n    if (isTypoTypeof(right, left, state)) {\n      warning(\"W122\", this, right.value);\n    } else if (isTypoTypeof(left, right, state)) {\n      /* istanbul ignore next */\n      warning(\"W122\", this, left.value);\n    }\n    return this;\n  });\n  relation(\"!=\", function(context, left, right) {\n    var eqnull = state.option.eqnull &&\n        ((left && left.value) === \"null\" || (right && right.value) === \"null\");\n\n    if (!eqnull && state.option.eqeqeq) {\n      this.from = this.character;\n      warning(\"W116\", this, \"!==\", \"!=\");\n    } else if (isTypoTypeof(right, left, state)) {\n      /* istanbul ignore next */\n      warning(\"W122\", this, right.value);\n    } else if (isTypoTypeof(left, right, state)) {\n      warning(\"W122\", this, left.value);\n    }\n    return this;\n  });\n  relation(\"!==\", function(context, left, right) {\n    if (isTypoTypeof(right, left, state)) {\n      warning(\"W122\", this, right.value);\n    } else if (isTypoTypeof(left, right, state)) {\n      /* istanbul ignore next */\n      warning(\"W122\", this, left.value);\n    }\n    return this;\n  });\n  relation(\"<\");\n  relation(\">\");\n  relation(\"<=\");\n  relation(\">=\");\n  bitwise(\"<<\", \"shiftleft\", 120);\n  bitwise(\">>\", \"shiftright\", 120);\n  bitwise(\">>>\", \"shiftrightunsigned\", 120);\n  infix(\"in\", \"in\", 120);\n  infix(\"instanceof\", function(context, left, token) {\n    var right;\n    var scope = state.funct[\"(scope)\"];\n    token.left = left;\n    token.right = right = expression(context, 120);\n\n    // This condition reflects a syntax error which will be reported by the\n    // `expression` function.\n    if (!right) {\n      return token;\n    }\n\n    if (right.id === \"(number)\" ||\n        right.id === \"(string)\" ||\n        right.value === \"null\" ||\n        (right.value === \"undefined\" && !scope.has(\"undefined\")) ||\n        right.arity === \"unary\" ||\n        right.id === \"{\" ||\n        (right.id === \"[\" && !right.right) ||\n        right.id === \"(regexp)\" ||\n        (right.id === \"(template)\" && !right.tag)) {\n      error(\"E060\");\n    }\n\n    if (right.id === \"function\") {\n      warning(\"W139\");\n    }\n\n    return token;\n  }, 120);\n  infix(\"+\", function(context, left, that) {\n    var next = state.tokens.next;\n    var right;\n    that.left = left;\n    that.right = right = expression(context, 130);\n\n    if (left && right && left.id === \"(string)\" && right.id === \"(string)\") {\n      left.value += right.value;\n      left.character = right.character;\n      if (!state.option.scripturl && reg.javascriptURL.test(left.value)) {\n        warning(\"W050\", left);\n      }\n      return left;\n    }\n\n    if (next.id === \"+\" || next.id === \"++\") {\n      warning(\"W007\", that.right);\n    }\n\n    return that;\n  }, 130);\n  prefix(\"+\", function(context) {\n    var next = state.tokens.next;\n    this.arity = \"unary\";\n    this.right = expression(context, 150);\n\n    if (next.id === \"+\" || next.id === \"++\") {\n      warning(\"W007\", this.right);\n    }\n\n    return this;\n  });\n  infix(\"-\", function(context, left, that) {\n    var next = state.tokens.next;\n    that.left = left;\n    that.right = expression(context, 130);\n\n    if (next.id === \"-\" || next.id === \"--\") {\n      warning(\"W006\", that.right);\n    }\n\n    return that;\n  }, 130);\n  prefix(\"-\", function(context) {\n    var next = state.tokens.next;\n    this.arity = \"unary\";\n    this.right = expression(context, 150);\n\n    if (next.id === \"-\" || next.id === \"--\") {\n      warning(\"W006\", this.right);\n    }\n\n    return this;\n  });\n  infix(\"*\", \"mult\", 140);\n  infix(\"/\", \"div\", 140);\n  infix(\"%\", \"mod\", 140);\n\n  suffix(\"++\");\n  prefix(\"++\", \"preinc\");\n  state.syntax[\"++\"].exps = true;\n\n  suffix(\"--\");\n  prefix(\"--\", \"predec\");\n  state.syntax[\"--\"].exps = true;\n\n  prefix(\"delete\", function(context) {\n    this.arity = \"unary\";\n    var p = expression(context, 150);\n    if (!p) {\n      return this;\n    }\n\n    if (p.id !== \".\" && p.id !== \"[\") {\n      warning(\"W051\");\n    }\n    this.first = p;\n\n    // The `delete` operator accepts unresolvable references when not in strict\n    // mode, so the operand may be undefined.\n    if (p.identifier && !state.isStrict()) {\n      p.forgiveUndef = true;\n    }\n    return this;\n  }).exps = true;\n\n  prefix(\"~\", function(context) {\n    if (state.option.bitwise) {\n      warning(\"W016\", this, \"~\");\n    }\n    this.arity = \"unary\";\n    this.right = expression(context, 150);\n    return this;\n  });\n\n  infix(\"...\");\n\n  prefix(\"!\", function(context) {\n    this.arity = \"unary\";\n    this.right = expression(context, 150);\n\n    if (!this.right) { // '!' followed by nothing? Give up.\n      quit(\"E041\", this);\n    }\n\n    if (bang[this.right.id] === true) {\n      warning(\"W018\", this, \"!\");\n    }\n    return this;\n  });\n\n  prefix(\"typeof\", function(context) {\n    this.arity = \"unary\";\n    var p = expression(context, 150);\n    this.first = this.right = p;\n\n    if (!p) { // 'typeof' followed by nothing? Give up.\n      quit(\"E041\", this);\n    }\n\n    // The `typeof` operator accepts unresolvable references, so the operand\n    // may be undefined.\n    if (p.identifier) {\n      p.forgiveUndef = true;\n    }\n    return this;\n  });\n  prefix(\"new\", function(context) {\n    var mp = metaProperty(context, \"target\", function() {\n      if (!state.inES6(true)) {\n        warning(\"W119\", state.tokens.prev, \"new.target\", \"6\");\n      }\n      var inFunction, c = state.funct;\n      while (c) {\n        inFunction = !c[\"(global)\"];\n        if (!c[\"(arrow)\"]) { break; }\n        c = c[\"(context)\"];\n      }\n      if (!inFunction) {\n        warning(\"W136\", state.tokens.prev, \"new.target\");\n      }\n    });\n    if (mp) { return mp; }\n\n    var opening = state.tokens.next;\n    var c = expression(context, 155), i;\n\n    if (!c) {\n      return this;\n    }\n\n    if (!c.paren && c.rbp > 160) {\n      error(\"E024\", opening, opening.value);\n    }\n\n    if (c.id !== \"function\") {\n      if (c.identifier) {\n        switch (c.value) {\n        case \"Number\":\n        case \"String\":\n        case \"Boolean\":\n        case \"Math\":\n        case \"JSON\":\n          warning(\"W053\", state.tokens.prev, c.value);\n          break;\n        case \"Symbol\":\n          if (state.inES6()) {\n            warning(\"W053\", state.tokens.prev, c.value);\n          }\n          break;\n        case \"Function\":\n          if (!state.option.evil) {\n            warning(\"W054\");\n          }\n          break;\n        case \"Date\":\n        case \"RegExp\":\n        case \"this\":\n          break;\n        default:\n          i = c.value.substr(0, 1);\n          if (state.option.newcap && (i < \"A\" || i > \"Z\") &&\n            !state.funct[\"(scope)\"].isPredefined(c.value)) {\n            warning(\"W055\", state.tokens.curr);\n          }\n        }\n      } else {\n        if (c.id === \"?.\" && !c.paren) {\n          error(\"E024\", c, \"?.\");\n        } else if (c.id !== \".\" && c.id !== \"[\" && c.id !== \"(\") {\n          /* istanbul ignore next */\n          warning(\"W056\", state.tokens.curr);\n        }\n      }\n    } else {\n      if (!state.option.supernew)\n        warning(\"W057\", this);\n    }\n    if (state.tokens.next.id !== \"(\" && !state.option.supernew) {\n      warning(\"W058\", state.tokens.curr, state.tokens.curr.value);\n    }\n    this.first = this.right = c;\n    return this;\n  });\n  state.syntax[\"new\"].exps = true;\n\n\n  var classDeclaration = blockstmt(\"class\", function(context) {\n    var className, classNameToken;\n\n    if (!state.inES6()) {\n      warning(\"W104\", state.tokens.curr, \"class\", \"6\");\n    }\n    state.inClassBody = true;\n\n    // Class Declaration: 'class <Classname>'\n    if (state.tokens.next.identifier && state.tokens.next.value !== \"extends\") {\n      classNameToken = state.tokens.next;\n      className = classNameToken.value;\n      identifier(context);\n      // unintialized, so that the 'extends' clause is parsed while the class is in TDZ\n      state.funct[\"(scope)\"].addbinding(className, {\n        type: \"class\",\n        initialized: false,\n        token: classNameToken\n      });\n    }\n\n    // Class Declaration: 'class <Classname> extends <Superclass>'\n    if (state.tokens.next.value === \"extends\") {\n      advance(\"extends\");\n      expression(context, 0);\n    }\n\n    if (classNameToken) {\n      this.name = classNameToken;\n      state.funct[\"(scope)\"].initialize(className);\n    } else {\n      this.name = null;\n    }\n\n    state.funct[\"(scope)\"].stack();\n    classBody(this, context);\n    return this;\n  });\n  classDeclaration.exps = true;\n  classDeclaration.declaration = true;\n\n  /*\n    Class expression\n\n    The Block- and Expression- handling for \"class\" are almost identical, except for the ordering of steps.\n    In an expression:, the name should not be saved into the calling scope, but is still accessible inside the definition, so we open a new scope first, then save the name. We also mark it as used.\n  */\n  prefix(\"class\", function(context) {\n    var className, classNameToken;\n\n    if (!state.inES6()) {\n      warning(\"W104\", state.tokens.curr, \"class\", \"6\");\n    }\n    state.inClassBody = true;\n\n    // Class Declaration: 'class <Classname>'\n    if (state.tokens.next.identifier && state.tokens.next.value !== \"extends\") {\n      classNameToken = state.tokens.next;\n      className = classNameToken.value;\n      identifier(context);\n    }\n\n    // Class Declaration: 'class <Classname> extends <Superclass>'\n    if (state.tokens.next.value === \"extends\") {\n      advance(\"extends\");\n      expression(context, 0);\n    }\n\n    state.funct[\"(scope)\"].stack();\n    if (classNameToken) {\n      this.name = classNameToken;\n      state.funct[\"(scope)\"].addbinding(className, {\n        type: \"class\",\n        initialized: true,\n        token: classNameToken\n      });\n      state.funct[\"(scope)\"].block.use(className, classNameToken);\n    } else {\n      this.name = null;\n    }\n\n    classBody(this, context);\n    return this;\n  });\n\n  function classBody(classToken, context) {\n    var props = Object.create(null);\n    var name, accessorType, token, isStatic, inGenerator, hasConstructor;\n\n    /* istanbul ignore else */\n    if (state.tokens.next.value === \"{\") {\n      advance(\"{\");\n    } else {\n      warning(\"W116\", state.tokens.curr, \"identifier\", state.tokens.next.type); //?\n      advance();\n    }\n\n    while (state.tokens.next.value !== \"}\") {\n      isStatic = false;\n      inGenerator = false;\n      context &= ~prodParams.preAsync;\n\n      if (state.tokens.next.value === \"static\" &&\n        !checkPunctuator(peek(), \"(\")) {\n        isStatic = true;\n        advance();\n      }\n\n      if (state.tokens.next.value === \"async\") {\n        if (!checkPunctuator(peek(), \"(\")) {\n          context |= prodParams.preAsync;\n          advance();\n\n          nolinebreak(state.tokens.curr);\n\n          if (checkPunctuator(state.tokens.next, \"*\")) {\n            inGenerator = true;\n            advance(\"*\");\n\n            if (!state.inES9()) {\n              warning(\"W119\", state.tokens.next, \"async generators\", \"9\");\n            }\n          }\n\n          if (!state.inES8()) {\n            warning(\"W119\", state.tokens.curr, \"async functions\", \"8\");\n          }\n        }\n      }\n\n      if (state.tokens.next.value === \"*\") {\n        inGenerator = true;\n        advance();\n      }\n\n      token = state.tokens.next;\n\n      if ((token.value === \"set\" || token.value === \"get\") && !checkPunctuator(peek(), \"(\")) {\n        if (inGenerator) {\n          /* istanbul ignore next */\n          error(\"E024\", token, token.value);\n        }\n        accessorType = token.value;\n        advance();\n        token = state.tokens.next;\n\n        if (!isStatic && token.value === \"constructor\") {\n          error(\"E049\", token, \"class \" + accessorType + \"ter method\", token.value);\n        } else if (isStatic && token.value === \"prototype\") {\n          error(\"E049\", token, \"static class \" + accessorType + \"ter method\", token.value);\n        }\n      } else {\n        accessorType = null;\n      }\n\n      switch (token.value) {\n        case \";\":\n          warning(\"W032\", token);\n          advance();\n          break;\n        case \"constructor\":\n          if (isStatic) {\n            // treat like a regular method -- static methods can be called 'constructor'\n            name = propertyName(context);\n            saveProperty(props, name, token, true, isStatic);\n            doMethod(classToken, context, name, inGenerator);\n          } else {\n            if (inGenerator || context & prodParams.preAsync) {\n              error(\"E024\", token, token.value);\n            } else if (hasConstructor) {\n              /* istanbul ignore next */\n              error(\"E024\", token, token.value);\n            } else {\n              hasConstructor = !accessorType && !isStatic;\n            }\n            advance();\n            doMethod(classToken, context, state.nameStack.infer());\n          }\n          break;\n        case \"[\":\n          name = computedPropertyName(context);\n          doMethod(classToken, context, name, inGenerator);\n          // We don't check names (via calling saveProperty()) of computed expressions like [\"Symbol.iterator\"]()\n          break;\n        default:\n          name = propertyName(context);\n          if (name === undefined) {\n            error(\"E024\", token, token.value);\n            advance();\n            break;\n          }\n\n          if (accessorType) {\n            saveAccessor(accessorType, props, name, token, true, isStatic);\n            name = state.nameStack.infer();\n          } else {\n            if (isStatic && name === \"prototype\") {\n              error(\"E049\", token, \"static class method\", name);\n            }\n\n            saveProperty(props, name, token, true, isStatic);\n          }\n\n          doMethod(classToken, context, name, inGenerator);\n          break;\n      }\n    }\n    advance(\"}\");\n    checkProperties(props);\n\n    state.inClassBody = false;\n    state.funct[\"(scope)\"].unstack();\n  }\n\n  function doMethod(classToken, context, name, generator) {\n    if (generator) {\n      if (!state.inES6()) {\n        warning(\"W119\", state.tokens.curr, \"function*\", \"6\");\n      }\n    }\n\n    if (state.tokens.next.value !== \"(\") {\n      error(\"E054\", state.tokens.next, state.tokens.next.value);\n      advance();\n      if (state.tokens.next.value === \"{\") {\n        // manually cheating the test \"invalidClasses\", which asserts this particular behavior when a class is misdefined.\n        advance();\n        if (state.tokens.next.value === \"}\") {\n          warning(\"W116\", state.tokens.next, \"(\", state.tokens.next.value);\n          advance();\n          identifier(context);\n          advance();\n        }\n        /* istanbul ignore next */\n        return;\n      } else {\n        while (state.tokens.next.value !== \"(\") {\n          advance();\n        }\n      }\n    }\n\n    doFunction(context, { name: name,\n        type: generator ? \"generator\" : null,\n        isMethod: true,\n        statement: classToken });\n  }\n\n  prefix(\"void\").exps = true;\n\n  infix(\".\", function(context, left, that) {\n    var m = identifier(context, true);\n\n    if (typeof m === \"string\") {\n      countMember(m);\n    }\n\n    that.left = left;\n    that.right = m;\n\n    if (m && m === \"hasOwnProperty\" && state.tokens.next.value === \"=\") {\n      warning(\"W001\");\n    }\n\n    if (left && left.value === \"arguments\" && (m === \"callee\" || m === \"caller\")) {\n      if (state.option.noarg)\n        warning(\"W059\", left, m);\n      else if (state.isStrict())\n        error(\"E008\");\n    } else if (!state.option.evil && left && left.value === \"document\" &&\n        (m === \"write\" || m === \"writeln\")) {\n      warning(\"W060\", left);\n    }\n\n    if (!state.option.evil && (m === \"eval\" || m === \"execScript\")) {\n      if (isGlobalEval(left, state)) {\n        warning(\"W061\");\n      }\n    }\n\n    return that;\n  }, 160, true);\n\n  infix(\"?.\", function(context, left, that) {\n    if (!state.inES11()) {\n      warning(\"W119\", state.tokens.curr, \"Optional chaining\", \"11\");\n    }\n\n\n    if (checkPunctuator(state.tokens.next, \"[\")) {\n      that.left = left;\n      advance();\n      that.right = state.tokens.curr.led(context, left);\n    } else if (checkPunctuator(state.tokens.next, \"(\")) {\n      that.left = left;\n      advance();\n      that.right = state.tokens.curr.led(context, left);\n      that.exps = true;\n    } else {\n      state.syntax[\".\"].led.call(that, context, left);\n    }\n\n    if (state.tokens.next.type === \"(template)\") {\n      error(\"E024\", state.tokens.next, \"`\");\n    }\n\n    return that;\n  }, 160, true);\n\n\n  /**\n   * Determine if a CallExpression's \"base\" is a type of expression commonly\n   * used in this position.\n   *\n   * @param {token} token - token describing the \"base\" of the CallExpression\n   * @returns {boolean}\n   */\n  function isTypicalCallExpression(token) {\n    return token.identifier || token.id === \".\" || token.id === \"[\" ||\n      token.id === \"=>\" || token.id === \"(\" || token.id === \"&&\" ||\n      token.id === \"||\" || token.id === \"?\" || token.id === \"async\" ||\n      token.id === \"?.\" || (state.inES6() && token[\"(name)\"]);\n  }\n\n  infix(\"(\", function(context, left, that) {\n    if (state.option.immed && left && !left.immed && left.id === \"function\") {\n      warning(\"W062\");\n    }\n\n    if (state.option.asi && checkPunctuators(state.tokens.prev, [\")\", \"]\"]) &&\n      !sameLine(state.tokens.prev, state.tokens.curr)) {\n      warning(\"W014\", state.tokens.curr, state.tokens.curr.id);\n    }\n\n    var n = 0;\n    var p = [];\n\n    if (left) {\n      if (left.type === \"(identifier)\") {\n        var newcapRe = /^[A-Z]([A-Z0-9_$]*[a-z][A-Za-z0-9_$]*)?$/;\n        var newcapIgnore = [\n          \"Array\", \"Boolean\", \"Date\", \"Error\", \"Function\", \"Number\",\n          \"Object\", \"RegExp\", \"String\", \"Symbol\"\n        ];\n        if (newcapRe.test(left.value) && newcapIgnore.indexOf(left.value) === -1) {\n          if (left.value === \"Math\") {\n            /* istanbul ignore next */\n            warning(\"W063\", left);\n          } else if (state.option.newcap) {\n            warning(\"W064\", left);\n          }\n        }\n      }\n    }\n\n    if (state.tokens.next.id !== \")\") {\n      for (;;) {\n        spreadrest(\"spread\");\n\n        p[p.length] = expression(context, 10);\n        n += 1;\n        if (state.tokens.next.id !== \",\") {\n          break;\n        }\n        advance(\",\");\n        checkComma({ allowTrailing: true });\n\n        if (state.tokens.next.id === \")\") {\n          if (!state.inES8()) {\n            warning(\"W119\", state.tokens.curr, \"Trailing comma in arguments lists\", \"8\");\n          }\n\n          break;\n        }\n      }\n    }\n\n    advance(\")\");\n\n    if (typeof left === \"object\") {\n      if (!state.inES5() && left.value === \"parseInt\" && n === 1) {\n        warning(\"W065\", state.tokens.curr);\n      }\n      if (!state.option.evil) {\n        if (left.value === \"eval\" || left.value === \"Function\" ||\n            left.value === \"execScript\") {\n          warning(\"W061\", left);\n\n          // This conditional expression was initially implemented with a typo\n          // which prevented the branch's execution in all cases. While\n          // enabling the code will produce behavior that is consistent with\n          // the other forms of code evaluation that follow, such a change is\n          // also technically incompatable with prior versions of JSHint (due\n          // to the fact that the behavior was never formally documented). This\n          // branch should be enabled as part of a major release.\n          //if (p[0] && p[0].id === \"(string)\") {\n          //  addEvalCode(left, p[0]);\n          //}\n        } else if (p[0] && p[0].id === \"(string)\" &&\n             (left.value === \"setTimeout\" ||\n            left.value === \"setInterval\")) {\n          warning(\"W066\", left);\n          addEvalCode(left, p[0]);\n\n        // window.setTimeout/setInterval\n        } else if (p[0] && p[0].id === \"(string)\" &&\n             left.value === \".\" &&\n             left.left.value === \"window\" &&\n             (left.right === \"setTimeout\" ||\n            left.right === \"setInterval\")) {\n          warning(\"W066\", left);\n          addEvalCode(left, p[0]);\n        }\n      }\n      if (!isTypicalCallExpression(left)) {\n        warning(\"W067\", that);\n      }\n    }\n\n    that.left = left;\n    return that;\n  }, 155, true).exps = true;\n\n  function peekThroughParens(parens) {\n    var pn = state.tokens.next;\n    var i = -1;\n    var pn1;\n\n    do {\n      if (pn.value === \"(\") {\n        parens += 1;\n      } else if (pn.value === \")\") {\n        parens -= 1;\n      }\n\n      i += 1;\n      pn1 = pn;\n      pn = peek(i);\n    } while (!(parens === 0 && pn1.value === \")\") && pn.type !== \"(end)\");\n\n    return pn;\n  }\n\n  prefix(\"(\", function(context, rbp) {\n    var ret, triggerFnExpr, first, last;\n    var opening = state.tokens.curr;\n    var preceeding = state.tokens.prev;\n    var isNecessary = !state.option.singleGroups;\n    var pn = peekThroughParens(1);\n\n    if (state.tokens.next.id === \"function\") {\n      triggerFnExpr = state.tokens.next.immed = true;\n    }\n\n    // If the balanced grouping operator is followed by a \"fat arrow\", the\n    // current token marks the beginning of a \"fat arrow\" function and parsing\n    // should proceed accordingly.\n    if (pn.value === \"=>\") {\n      pn.funct = doFunction(context, { type: \"arrow\", parsedOpening: true });\n      return pn;\n    }\n\n    // The ECMA262 grammar requires an expression between the \"opening\n    // parenthesis\" and \"close parenthesis\" tokens of the grouping operator.\n    // However, the \"ignore\" directive is commonly used to inject values that\n    // are not included in the token stream. For example:\n    //\n    //     return (\n    //       /*jshint ignore:start */\n    //       <div></div>\n    //       /*jshint ignore:end */\n    //     );\n    //\n    // The \"empty\" grouping operator is permitted in order to tolerate this\n    // pattern.\n    if (state.tokens.next.id === \")\") {\n      advance(\")\");\n      return;\n    }\n\n    ret = expression(context, 0);\n\n    advance(\")\", this);\n\n    if (!ret) {\n      return;\n    }\n\n    ret.paren = true;\n\n    if (state.option.immed && ret && ret.id === \"function\") {\n      if (state.tokens.next.id !== \"(\" &&\n        state.tokens.next.id !== \".\" && state.tokens.next.id !== \"[\") {\n        warning(\"W068\", this);\n      }\n    }\n\n    if (ret.id === \",\") {\n      first = ret.left;\n      while (first.id === \",\") {\n        first = first.left;\n      }\n\n      last = ret.right;\n    } else {\n      first = last = ret;\n\n      if (!isNecessary) {\n        // async functions are identified after parsing due to the complexity\n        // of disambiguating the `async` keyword.\n        if (!triggerFnExpr) {\n          triggerFnExpr = ret.id === \"async\";\n        }\n\n        isNecessary =\n          // Used to distinguish from an ExpressionStatement which may not\n          // begin with the `{` and `function` tokens\n          (opening.beginsStmt && (ret.id === \"{\" || triggerFnExpr)) ||\n          // Used to signal that a function expression is being supplied to\n          // some other operator.\n          (triggerFnExpr &&\n            // For parenthesis wrapping a function expression to be considered\n            // necessary, the grouping operator should be the left-hand-side of\n            // some other operator--either within the parenthesis or directly\n            // following them.\n            (!isEndOfExpr() || state.tokens.prev.id !== \"}\")) ||\n          // Used to demarcate an arrow function as the left-hand side of some\n          // operator.\n          (ret.id === \"=>\" && !isEndOfExpr()) ||\n          // Used as the return value of a single-statement arrow function\n          (ret.id === \"{\" && preceeding.id === \"=>\") ||\n          // Used to cover a unary expression as the left-hand side of the\n          // exponentiation operator\n          (beginsUnaryExpression(ret) && state.tokens.next.id === \"**\") ||\n          // Used to cover a logical operator as the right-hand side of the\n          // nullish coalescing operator\n          (preceeding.id === \"??\" && (ret.id === \"&&\" || ret.id === \"||\")) ||\n          // Used to delineate an integer number literal from a dereferencing\n          // punctuator (otherwise interpreted as a decimal point)\n          (ret.type === \"(number)\" &&\n            checkPunctuator(pn, \".\") && /^\\d+$/.test(ret.value)) ||\n          // Used to wrap object destructuring assignment\n          (opening.beginsStmt && ret.id === \"=\" && ret.left.id === \"{\") ||\n          // Used to allow optional chaining with other language features which\n          // are otherwise restricted.\n          (ret.id === \"?.\" &&\n              (preceeding.id === \"new\" || state.tokens.next.type === \"(template)\"));\n      }\n    }\n\n    // The operator may be necessary to override the default binding power of\n    // neighboring operators (whenever there is an operator in use within the\n    // first expression *or* the current group contains multiple expressions)\n    if (!isNecessary && (isOperator(first) || first !== last)) {\n      isNecessary =\n        (rbp > first.lbp) ||\n        (rbp > 0 && rbp === first.lbp) ||\n        (!isEndOfExpr() && last.rbp < state.tokens.next.lbp);\n    }\n\n    if (!isNecessary) {\n      warning(\"W126\", opening);\n    }\n\n    return ret;\n  });\n\n  application(\"=>\").rbp = 161;\n\n  infix(\"[\", function(context, left, that) {\n    var e, s, canUseDot;\n\n    if (state.option.asi && checkPunctuators(state.tokens.prev, [\")\", \"]\"]) &&\n      !sameLine(state.tokens.prev, state.tokens.curr)) {\n      warning(\"W014\", state.tokens.curr, state.tokens.curr.id);\n    }\n\n    e = expression(context & ~prodParams.noin, 0);\n\n    if (e && e.type === \"(string)\") {\n      if (!state.option.evil && (e.value === \"eval\" || e.value === \"execScript\")) {\n        if (isGlobalEval(left, state)) {\n          warning(\"W061\");\n        }\n      }\n\n      countMember(e.value);\n      if (!state.option.sub && reg.identifier.test(e.value)) {\n        s = state.syntax[e.value];\n\n        if (s) {\n          canUseDot = !isReserved(context, s);\n        } else {\n          // This branch exists to preserve legacy behavior with version 2.9.5\n          // and earlier. In those releases, `eval` and `arguments` were\n          // incorrectly interpreted as reserved keywords, so Member\n          // Expressions such as `object[\"eval\"]` did not trigger warning W069.\n          //\n          // TODO: Remove in JSHint 3\n          canUseDot = e.value !== \"eval\" && e.value !== \"arguments\";\n        }\n\n        if (canUseDot) {\n          warning(\"W069\", state.tokens.prev, e.value);\n        }\n      }\n    }\n    advance(\"]\", that);\n\n    if (e && e.value === \"hasOwnProperty\" && state.tokens.next.value === \"=\") {\n      warning(\"W001\");\n    }\n\n    that.left = left;\n    that.right = e;\n    return that;\n  }, 160, true);\n\n  function comprehensiveArrayExpression(context) {\n    var res = {};\n    res.exps = true;\n    state.funct[\"(comparray)\"].stack();\n\n    // Handle reversed for expressions, used in spidermonkey\n    var reversed = false;\n    if (state.tokens.next.value !== \"for\") {\n      reversed = true;\n      if (!state.inMoz()) {\n        warning(\"W116\", state.tokens.next, \"for\", state.tokens.next.value);\n      }\n      state.funct[\"(comparray)\"].setState(\"use\");\n      res.right = expression(context, 10);\n    }\n\n    advance(\"for\");\n    if (state.tokens.next.value === \"each\") {\n      advance(\"each\");\n      if (!state.inMoz()) {\n        warning(\"W118\", state.tokens.curr, \"for each\");\n      }\n    }\n    advance(\"(\");\n    state.funct[\"(comparray)\"].setState(\"define\");\n    res.left = expression(context, 130);\n    if (_.includes([\"in\", \"of\"], state.tokens.next.value)) {\n      advance();\n    } else {\n      /* istanbul ignore next */\n      error(\"E045\", state.tokens.curr);\n    }\n    state.funct[\"(comparray)\"].setState(\"generate\");\n    expression(context, 10);\n\n    advance(\")\");\n    if (state.tokens.next.value === \"if\") {\n      advance(\"if\");\n      advance(\"(\");\n      state.funct[\"(comparray)\"].setState(\"filter\");\n      expression(context, 10);\n      advance(\")\");\n    }\n\n    if (!reversed) {\n      state.funct[\"(comparray)\"].setState(\"use\");\n      res.right = expression(context, 10);\n    }\n\n    advance(\"]\");\n    state.funct[\"(comparray)\"].unstack();\n    return res;\n  }\n\n  prefix(\"[\", function(context) {\n    var blocktype = lookupBlockType();\n    if (blocktype.isCompArray) {\n      if (!state.option.esnext && !state.inMoz()) {\n        warning(\"W118\", state.tokens.curr, \"array comprehension\");\n      }\n      return comprehensiveArrayExpression(context);\n    } else if (blocktype.isDestAssign) {\n      this.destructAssign = destructuringPattern(context, {\n          openingParsed: true,\n          assignment: true\n        });\n      return this;\n    }\n    var b = !sameLine(state.tokens.curr, state.tokens.next);\n    this.first = [];\n    if (b) {\n      indent += state.option.indent;\n      if (state.tokens.next.from === indent + state.option.indent) {\n        /* istanbul ignore next */\n        indent += state.option.indent;\n      }\n    }\n    while (state.tokens.next.id !== \"(end)\") {\n      while (state.tokens.next.id === \",\") {\n        if (!state.option.elision) {\n          if (!state.inES5()) {\n            // Maintain compat with old options --- ES5 mode without\n            // elision=true will warn once per comma\n            warning(\"W070\");\n          } else {\n            warning(\"W128\");\n            do {\n              advance(\",\");\n            } while (state.tokens.next.id === \",\");\n            continue;\n          }\n        }\n        advance(\",\");\n      }\n\n      if (state.tokens.next.id === \"]\") {\n        break;\n      }\n\n      spreadrest(\"spread\");\n\n      this.first.push(expression(context, 10));\n      if (state.tokens.next.id === \",\") {\n        advance(\",\");\n        checkComma({ allowTrailing: true });\n        if (state.tokens.next.id === \"]\" && !state.inES5()) {\n          warning(\"W070\", state.tokens.curr);\n          break;\n        }\n      } else {\n        if (state.option.trailingcomma && state.inES5()) {\n          warningAt(\"W140\", state.tokens.curr.line, state.tokens.curr.character);\n        }\n        break;\n      }\n    }\n    if (b) {\n      indent -= state.option.indent;\n    }\n    advance(\"]\", this);\n    return this;\n  });\n\n\n  function isMethod() {\n    return !!state.funct[\"(method)\"];\n  }\n\n  /**\n   * Retrieve the value of the next token if it is a valid LiteralPropertyName\n   * and optionally advance the parser.\n   *\n   * @param {number} context - the parsing context; see `prod-params.js` for\n   *                           more information\n   *\n   * @returns {string|undefined} - the value of the identifier, if present\n   */\n  function propertyName(context) {\n    var id = optionalidentifier(context, true);\n\n    if (!id) {\n      if (state.tokens.next.id === \"(string)\") {\n        id = state.tokens.next.value;\n        advance();\n      } else if (state.tokens.next.id === \"(number)\") {\n        id = state.tokens.next.value.toString();\n        advance();\n      }\n    }\n\n    if (id === \"hasOwnProperty\") {\n      warning(\"W001\");\n    }\n\n    return id;\n  }\n\n  /**\n   * @param {Number} context The parsing context\n   * @param {Object} [options]\n   * @param {token} [options.loneArg] The argument to the function in cases\n   *                                  where it was defined using the\n   *                                  single-argument shorthand.\n   * @param {bool} [options.parsedOpening] Whether the opening parenthesis has\n   *                                       already been parsed.\n   *\n   * @returns {{ arity: number, params: Array.<string>, isSimple: boolean }}\n   */\n  function functionparams(context, options) {\n    var next;\n    var paramsIds = [];\n    var ident;\n    var tokens = [];\n    var t;\n    var pastDefault = false;\n    var pastRest = false;\n    var arity = 0;\n    var loneArg = options && options.loneArg;\n    var hasDestructuring = false;\n\n    if (loneArg && loneArg.identifier === true) {\n      state.funct[\"(scope)\"].addParam(loneArg.value, loneArg);\n      return { arity: 1, params: [ loneArg.value ], isSimple: true };\n    }\n\n    next = state.tokens.next;\n\n    if (!options || !options.parsedOpening) {\n      advance(\"(\");\n    }\n\n    if (state.tokens.next.id === \")\") {\n      advance(\")\");\n      return;\n    }\n\n    function addParam(addParamArgs) {\n      state.funct[\"(scope)\"].addParam.apply(state.funct[\"(scope)\"], addParamArgs);\n    }\n\n    for (;;) {\n      arity++;\n      // are added to the param scope\n      var currentParams = [];\n\n      pastRest = spreadrest(\"rest\");\n\n      if (_.includes([\"{\", \"[\"], state.tokens.next.id)) {\n        hasDestructuring = true;\n        tokens = destructuringPattern(context);\n        for (t in tokens) {\n          t = tokens[t];\n          if (t.id) {\n            paramsIds.push(t.id);\n            currentParams.push([t.id, t.token]);\n          }\n        }\n      } else {\n        ident = identifier(context);\n\n        if (ident) {\n          paramsIds.push(ident);\n          currentParams.push([ident, state.tokens.curr]);\n        } else {\n          // Skip invalid parameter.\n          while (!checkPunctuators(state.tokens.next, [\",\", \")\"])) advance();\n        }\n      }\n\n      // It is valid to have a regular argument after a default argument\n      // since undefined can be used for missing parameters. Still warn as it is\n      // a possible code smell.\n      if (pastDefault) {\n        if (state.tokens.next.id !== \"=\") {\n          error(\"W138\", state.tokens.curr);\n        }\n      }\n      if (state.tokens.next.id === \"=\") {\n        if (!state.inES6()) {\n          warning(\"W119\", state.tokens.next, \"default parameters\", \"6\");\n        }\n\n        if (pastRest) {\n          error(\"E062\", state.tokens.next);\n        }\n\n        advance(\"=\");\n        pastDefault = true;\n        expression(context, 10);\n      }\n\n      // now we have evaluated the default expression, add the variable to the param scope\n      currentParams.forEach(addParam);\n      if (state.tokens.next.id === \",\") {\n        if (pastRest) {\n          warning(\"W131\", state.tokens.next);\n        }\n        advance(\",\");\n        checkComma({ allowTrailing: true });\n      }\n\n      if (state.tokens.next.id === \")\") {\n        if (state.tokens.curr.id === \",\" && !state.inES8()) {\n          warning(\"W119\", state.tokens.curr, \"Trailing comma in function parameters\", \"8\");\n        }\n\n        advance(\")\", next);\n        return {\n          arity: arity,\n          params: paramsIds,\n          isSimple: !hasDestructuring && !pastRest && !pastDefault\n        };\n      }\n    }\n  }\n\n  /**\n   * Factory function for creating objects used to track statistics of function\n   * literals.\n   *\n   * @param {string} name - the identifier name to associate with the function\n   * @param {object} [token] - token responsible for creating the function\n   *                           object\n   * @param {object} [overwrites] - a collection of properties that should\n   *                                override the corresponding default value of\n   *                                the new \"functor\" object\n   */\n  function functor(name, token, overwrites) {\n    var funct = {\n      \"(name)\"      : name,\n      \"(breakage)\"  : 0,\n      \"(loopage)\"   : 0,\n      // The strictness of the function body is tracked via a dedicated\n      // property (as opposed to via the global `state` object) so that the\n      // value can be referenced after the body has been fully parsed (i.e.\n      // when validating the identifier used in function declarations and\n      // function expressions).\n      \"(isStrict)\"  : \"unknown\",\n\n      \"(global)\"    : false,\n\n      \"(line)\"      : null,\n      \"(character)\" : null,\n      \"(metrics)\"   : null,\n      \"(statement)\" : null,\n      \"(context)\"   : null,\n      \"(scope)\"     : null,\n      \"(comparray)\" : null,\n      \"(yielded)\"   : null,\n      \"(arrow)\"     : null,\n      \"(async)\"     : null,\n      \"(params)\"    : null\n    };\n\n    if (token) {\n      _.extend(funct, {\n        \"(line)\"     : token.line,\n        \"(character)\": token.character,\n        \"(metrics)\"  : createMetrics(token)\n      });\n    }\n\n    _.extend(funct, overwrites);\n\n    if (funct[\"(context)\"]) {\n      funct[\"(scope)\"] = funct[\"(context)\"][\"(scope)\"];\n      funct[\"(comparray)\"]  = funct[\"(context)\"][\"(comparray)\"];\n    }\n\n    return funct;\n  }\n\n  /**\n   * Determine if the parser has begun parsing executable code.\n   *\n   * @param {Token} funct - The current \"functor\" token\n   *\n   * @returns {boolean}\n   */\n  function hasParsedCode(funct) {\n    return funct[\"(global)\"] && !funct[\"(verb)\"];\n  }\n\n  /**\n   * This function is used as both a null-denotation method *and* a\n   * left-denotation method, meaning the first parameter is overloaded.\n   */\n  function doTemplateLiteral(context, leftOrRbp) {\n    // ASSERT: this.type === \"(template)\"\n    // jshint validthis: true\n    var ctx = this.context;\n    var noSubst = this.noSubst;\n    var depth = this.depth;\n    var left = typeof leftOrRbp === \"number\" ? null : leftOrRbp;\n\n    if (!noSubst) {\n      while (!end()) {\n        if (!state.tokens.next.template || state.tokens.next.depth > depth) {\n          expression(context, 0); // should probably have different rbp?\n        } else {\n          // skip template start / middle\n          advance();\n        }\n      }\n    }\n\n    return {\n      id: \"(template)\",\n      type: \"(template)\",\n      tag: left\n    };\n\n    function end() {\n      if (state.tokens.curr.template && state.tokens.curr.tail &&\n          state.tokens.curr.context === ctx) {\n        /* istanbul ignore next */\n        return true;\n      }\n      var complete = (state.tokens.next.template && state.tokens.next.tail &&\n                      state.tokens.next.context === ctx);\n      if (complete) advance();\n      return complete || state.tokens.next.isUnclosed;\n    }\n  }\n\n  /**\n   * Parse a function literal.\n   *\n   * @param {Number} context The parsing context\n   * @param {Object} [options]\n   * @param {string} [options.name] The identifier belonging to the function (if\n   *                                any)\n   * @param {token} [options.statement] The statement that triggered creation\n   *                                    of the current function.\n   * @param {string} [options.type] If specified, either \"generator\" or \"arrow\"\n   * @param {token} [options.loneArg] The argument to the function in cases\n   *                                  where it was defined using the\n   *                                  single-argument shorthand\n   * @param {bool} [options.parsedOpening] Whether the opening parenthesis has\n   *                                       already been parsed\n   * @param {string} [options.classExprBinding] Define a function with this\n   *                                            identifier in the new function's\n   *                                            scope, mimicking the bahavior of\n   *                                            class expression names within\n   *                                            the body of member functions.\n   */\n  function doFunction(context, options) {\n    var f, token, name, statement, classExprBinding, isGenerator, isArrow,\n      isMethod, ignoreLoopFunc;\n    var oldOption = state.option;\n    var oldIgnored = state.ignored;\n    var isAsync = context & prodParams.preAsync;\n\n    if (options) {\n      name = options.name;\n      statement = options.statement;\n      classExprBinding = options.classExprBinding;\n      isGenerator = options.type === \"generator\";\n      isArrow = options.type === \"arrow\";\n      isMethod = options.isMethod;\n      ignoreLoopFunc = options.ignoreLoopFunc;\n    }\n\n    context &= ~prodParams.noin;\n    context &= ~prodParams.tryClause;\n\n    if (isAsync) {\n      context |= prodParams.async;\n    } else {\n      context &= ~prodParams.async;\n    }\n\n    if (isGenerator) {\n      context |= prodParams.yield;\n    } else if (!isArrow) {\n      context &= ~prodParams.yield;\n    }\n    context &= ~prodParams.preAsync;\n\n    state.option = Object.create(state.option);\n    state.ignored = Object.create(state.ignored);\n\n    state.funct = functor(name || state.nameStack.infer(), state.tokens.next, {\n      \"(statement)\": statement,\n      \"(context)\":   state.funct,\n      \"(arrow)\":     isArrow,\n      \"(method)\":    isMethod,\n      \"(async)\":     isAsync\n    });\n\n    f = state.funct;\n    token = state.tokens.curr;\n\n    functions.push(state.funct);\n\n    // So that the function is available to itself and referencing itself is not\n    // seen as a closure, add the function name to a new scope, but do not\n    // test for unused (unused: false)\n    // it is a new block scope so that params can override it, it can be block scoped\n    // but declarations inside the function don't cause already declared error\n    state.funct[\"(scope)\"].stack(\"functionouter\");\n    var internallyAccessibleName = !isMethod && (name || classExprBinding);\n    if (internallyAccessibleName) {\n      state.funct[\"(scope)\"].block.add(internallyAccessibleName,\n        classExprBinding ? \"class\" : \"function\", state.tokens.curr, false);\n    }\n\n    if (!isArrow) {\n      state.funct[\"(scope)\"].funct.add(\"arguments\", \"var\", token, false);\n    }\n\n    // create the param scope (params added in functionparams)\n    state.funct[\"(scope)\"].stack(\"functionparams\");\n\n    var paramsInfo = functionparams(context, options);\n\n    if (paramsInfo) {\n      state.funct[\"(params)\"] = paramsInfo.params;\n      state.funct[\"(hasSimpleParams)\"] = paramsInfo.isSimple;\n      state.funct[\"(metrics)\"].arity = paramsInfo.arity;\n      state.funct[\"(metrics)\"].verifyMaxParametersPerFunction();\n    } else {\n      state.funct[\"(params)\"] = [];\n      state.funct[\"(metrics)\"].arity = 0;\n      state.funct[\"(hasSimpleParams)\"] = true;\n    }\n\n    if (isArrow) {\n      context &= ~prodParams.yield;\n\n      if (!state.inES6(true)) {\n        warning(\"W119\", state.tokens.curr, \"arrow function syntax (=>)\", \"6\");\n      }\n\n      if (!options.loneArg) {\n        advance(\"=>\");\n      }\n    }\n\n    block(context, false, true, true, isArrow);\n\n    if (!state.option.noyield && isGenerator && !state.funct[\"(yielded)\"]) {\n      warning(\"W124\", state.tokens.curr);\n    }\n\n    state.funct[\"(metrics)\"].verifyMaxStatementsPerFunction();\n    state.funct[\"(metrics)\"].verifyMaxComplexityPerFunction();\n    state.funct[\"(unusedOption)\"] = state.option.unused;\n    state.option = oldOption;\n    state.ignored = oldIgnored;\n    state.funct[\"(last)\"] = state.tokens.curr.line;\n    state.funct[\"(lastcharacter)\"] = state.tokens.curr.character;\n\n    // unstack the params scope\n    state.funct[\"(scope)\"].unstack(); // also does usage and label checks\n\n    // unstack the function outer stack\n    state.funct[\"(scope)\"].unstack();\n\n    state.funct = state.funct[\"(context)\"];\n\n    if (!ignoreLoopFunc && !state.option.loopfunc && state.funct[\"(loopage)\"]) {\n      // If the function we just parsed accesses any non-local variables\n      // trigger a warning. Otherwise, the function is safe even within\n      // a loop.\n      if (f[\"(outerMutables)\"]) {\n        warning(\"W083\", token, f[\"(outerMutables)\"].join(\", \"));\n      }\n    }\n\n    return f;\n  }\n\n  function createMetrics(functionStartToken) {\n    return {\n      statementCount: 0,\n      nestedBlockDepth: -1,\n      ComplexityCount: 1,\n      arity: 0,\n\n      verifyMaxStatementsPerFunction: function() {\n        if (state.option.maxstatements &&\n          this.statementCount > state.option.maxstatements) {\n          warning(\"W071\", functionStartToken, this.statementCount);\n        }\n      },\n\n      verifyMaxParametersPerFunction: function() {\n        if (_.isNumber(state.option.maxparams) &&\n          this.arity > state.option.maxparams) {\n          warning(\"W072\", functionStartToken, this.arity);\n        }\n      },\n\n      verifyMaxNestedBlockDepthPerFunction: function() {\n        if (state.option.maxdepth &&\n          this.nestedBlockDepth > 0 &&\n          this.nestedBlockDepth === state.option.maxdepth + 1) {\n          warning(\"W073\", null, this.nestedBlockDepth);\n        }\n      },\n\n      verifyMaxComplexityPerFunction: function() {\n        var max = state.option.maxcomplexity;\n        var cc = this.ComplexityCount;\n        if (max && cc > max) {\n          warning(\"W074\", functionStartToken, cc);\n        }\n      }\n    };\n  }\n\n  function increaseComplexityCount() {\n    state.funct[\"(metrics)\"].ComplexityCount += 1;\n  }\n\n  // Parse assignments that were found instead of conditionals.\n  // For example: if (a = 1) { ... }\n\n  function checkCondAssignment(token) {\n    if (!token || token.paren) {\n      return;\n    }\n\n    if (token.id === \",\") {\n      checkCondAssignment(token.right);\n      return;\n    }\n\n    switch (token.id) {\n    case \"=\":\n    case \"+=\":\n    case \"-=\":\n    case \"*=\":\n    case \"%=\":\n    case \"&=\":\n    case \"|=\":\n    case \"^=\":\n    case \"/=\":\n      if (!state.option.boss) {\n        warning(\"W084\", token);\n      }\n    }\n  }\n\n  /**\n   * Validate the properties defined within an object literal or class body.\n   * See the `saveAccessor` and `saveProperty` functions for more detail.\n   *\n   * @param {object} props - Collection of objects describing the properties\n   *                         encountered\n   */\n  function checkProperties(props) {\n    // Check for lonely setters if in the ES5 mode.\n    if (state.inES5()) {\n      for (var name in props) {\n        if (props[name] && props[name].setterToken && !props[name].getterToken &&\n          !props[name].static) {\n          warning(\"W078\", props[name].setterToken);\n        }\n      }\n    }\n  }\n\n  function metaProperty(context, name, c) {\n    if (checkPunctuator(state.tokens.next, \".\")) {\n      var left = state.tokens.curr.id;\n      advance(\".\");\n      var id = identifier(context);\n      state.tokens.curr.isMetaProperty = true;\n      if (name !== id) {\n        error(\"E057\", state.tokens.prev, left, id);\n      } else {\n        c();\n      }\n      return state.tokens.curr;\n    }\n  }\n\n//object literals\n  (function(x) {\n    x.nud = function(context) {\n      var b, f, i, params, t, isGeneratorMethod = false, nextVal;\n      var props = Object.create(null); // All properties, including accessors\n      var isAsyncMethod = false;\n\n      b = !sameLine(state.tokens.curr, state.tokens.next);\n      if (b) {\n        indent += state.option.indent;\n        if (state.tokens.next.from === indent + state.option.indent) {\n          /* istanbul ignore next */\n          indent += state.option.indent;\n        }\n      }\n\n      var blocktype = lookupBlockType();\n      if (blocktype.isDestAssign) {\n        this.destructAssign = destructuringPattern(context, {\n            openingParsed: true,\n            assignment: true\n          });\n        return this;\n      }\n      state.inObjectBody = true;\n      for (;;) {\n        if (state.tokens.next.id === \"}\") {\n          break;\n        }\n\n        nextVal = state.tokens.next.value;\n        if (state.tokens.next.identifier &&\n            (peekIgnoreEOL().id === \",\" || peekIgnoreEOL().id === \"}\")) {\n          if (!state.inES6()) {\n            warning(\"W104\", state.tokens.next, \"object short notation\", \"6\");\n          }\n          t = expression(context, 10);\n          i = t && t.value;\n          if (t) {\n            saveProperty(props, i, t);\n          }\n\n        } else if (peek().id !== \":\" && (nextVal === \"get\" || nextVal === \"set\")) {\n          advance(nextVal);\n\n          if (!state.inES5()) {\n            error(\"E034\");\n          }\n\n          if (state.tokens.next.id === \"[\") {\n            i = computedPropertyName(context);\n          } else {\n            i = propertyName(context);\n\n            // ES6 allows for get() {...} and set() {...} method\n            // definition shorthand syntax, so we don't produce an error\n            // if linting ECMAScript 6 code.\n            if (!i && !state.inES6()) {\n              error(\"E035\");\n            }\n          }\n\n          // We don't want to save this getter unless it's an actual getter\n          // and not an ES6 concise method\n          if (i) {\n            saveAccessor(nextVal, props, i, state.tokens.curr);\n          }\n\n          t = state.tokens.next;\n          f = doFunction(context, { isMethod: true });\n          params = f[\"(params)\"];\n\n          // Don't warn about getter/setter pairs if this is an ES6 concise method\n          if (nextVal === \"get\" && i && params.length) {\n            warning(\"W076\", t, params[0], i);\n          } else if (nextVal === \"set\" && i && f[\"(metrics)\"].arity !== 1) {\n            warning(\"W077\", t, i);\n          }\n\n        } else if (spreadrest(\"spread\")) {\n          if (!state.inES9()) {\n            warning(\"W119\", state.tokens.next, \"object spread property\", \"9\");\n          }\n\n          expression(context, 10);\n        } else {\n          if (state.tokens.next.id === \"async\" && !checkPunctuators(peek(), [\"(\", \":\"])) {\n            if (!state.inES8()) {\n              warning(\"W119\", state.tokens.next, \"async functions\", \"8\");\n            }\n\n            isAsyncMethod = true;\n            advance();\n\n            nolinebreak(state.tokens.curr);\n          } else {\n            isAsyncMethod = false;\n          }\n\n          if (state.tokens.next.value === \"*\" && state.tokens.next.type === \"(punctuator)\") {\n            if (isAsyncMethod && !state.inES9()) {\n              warning(\"W119\", state.tokens.next, \"async generators\", \"9\");\n            } else if (!state.inES6()) {\n              warning(\"W104\", state.tokens.next, \"generator functions\", \"6\");\n            }\n\n            advance(\"*\");\n            isGeneratorMethod = true;\n          } else {\n            isGeneratorMethod = false;\n          }\n\n          if (state.tokens.next.id === \"[\") {\n            i = computedPropertyName(context);\n            state.nameStack.set(i);\n          } else {\n            state.nameStack.set(state.tokens.next);\n            i = propertyName(context);\n            saveProperty(props, i, state.tokens.next);\n\n            if (typeof i !== \"string\") {\n              break;\n            }\n          }\n\n          if (state.tokens.next.value === \"(\") {\n            if (!state.inES6()) {\n              warning(\"W104\", state.tokens.curr, \"concise methods\", \"6\");\n            }\n\n            doFunction(isAsyncMethod ? context | prodParams.preAsync : context, {\n              isMethod: true,\n              type: isGeneratorMethod ? \"generator\" : null\n            });\n          } else {\n            advance(\":\");\n            expression(context, 10);\n          }\n        }\n\n        countMember(i);\n\n        if (state.tokens.next.id === \",\") {\n          advance(\",\");\n          checkComma({ allowTrailing: true, property: true });\n          if (state.tokens.next.id === \",\") {\n            /* istanbul ignore next */\n            warning(\"W070\", state.tokens.curr);\n          } else if (state.tokens.next.id === \"}\" && !state.inES5()) {\n            warning(\"W070\", state.tokens.curr);\n          }\n        } else {\n          if (state.option.trailingcomma && state.inES5()) {\n            warningAt(\"W140\", state.tokens.curr.line, state.tokens.curr.character);\n          }\n          break;\n        }\n      }\n      if (b) {\n        indent -= state.option.indent;\n      }\n      advance(\"}\", this);\n\n      checkProperties(props);\n      state.inObjectBody = false;\n\n      return this;\n    };\n    x.fud = function() {\n      /* istanbul ignore next */\n      error(\"E036\", state.tokens.curr);\n    };\n  }(delim(\"{\")));\n\n  function destructuringPattern(context, options) {\n    var isAssignment = options && options.assignment;\n\n    context &= ~prodParams.noin;\n\n    if (!state.inES6()) {\n      warning(\"W104\", state.tokens.curr,\n        isAssignment ? \"destructuring assignment\" : \"destructuring binding\", \"6\");\n    }\n\n    return destructuringPatternRecursive(context, options);\n  }\n\n  function destructuringPatternRecursive(context, options) {\n    var ids, idx;\n    var identifiers = [];\n    var openingParsed = options && options.openingParsed;\n    var isAssignment = options && options.assignment;\n    var recursiveOptions = isAssignment ? { assignment: isAssignment } : null;\n    var firstToken = openingParsed ? state.tokens.curr : state.tokens.next;\n\n    var nextInnerDE = function() {\n      var ident;\n      if (checkPunctuators(state.tokens.next, [\"[\", \"{\"])) {\n        ids = destructuringPatternRecursive(context, recursiveOptions);\n        for (idx = 0; idx < ids.length; idx++) {\n          identifiers.push({ id: ids[idx].id, token: ids[idx].token });\n        }\n      } else if (checkPunctuator(state.tokens.next, \",\")) {\n        identifiers.push({ id: null, token: state.tokens.curr });\n      } else if (checkPunctuator(state.tokens.next, \"(\")) {\n        advance(\"(\");\n        nextInnerDE();\n        advance(\")\");\n      } else {\n        if (isAssignment) {\n          var assignTarget = expression(context, 20);\n          if (assignTarget) {\n            checkLeftSideAssign(context, assignTarget);\n\n            // if the target was a simple identifier, add it to the list to return\n            if (assignTarget.identifier) {\n              ident = assignTarget.value;\n            }\n          }\n        } else {\n          ident = identifier(context);\n        }\n        if (ident) {\n          identifiers.push({ id: ident, token: state.tokens.curr });\n        }\n      }\n    };\n\n    var assignmentProperty = function(context) {\n      var id, expr;\n\n      if (checkPunctuator(state.tokens.next, \"[\")) {\n        advance(\"[\");\n        expression(context, 10);\n        advance(\"]\");\n        advance(\":\");\n        nextInnerDE();\n      } else if (state.tokens.next.id === \"(string)\" ||\n                 state.tokens.next.id === \"(number)\") {\n        advance();\n        advance(\":\");\n        nextInnerDE();\n      } else {\n        // this id will either be the property name or the property name and the assigning identifier\n        var isRest = spreadrest(\"rest\");\n\n        if (isRest) {\n          if (!state.inES9()) {\n            warning(\"W119\", state.tokens.next, \"object rest property\", \"9\");\n          }\n\n          // Due to visual symmetry with the array rest property (and the early\n          // design of the language feature), developers may mistakenly assume\n          // any expression is valid in this position. If the next token is not\n          // an identifier, attempt to parse an expression and issue an error.\n          // order to recover more gracefully from this condition.\n          if (state.tokens.next.type === \"(identifier)\") {\n            id = identifier(context);\n          } else {\n            expr = expression(context, 10);\n            error(\"E030\", expr, expr.value);\n          }\n        } else {\n          id = identifier(context);\n        }\n\n        if (!isRest && checkPunctuator(state.tokens.next, \":\")) {\n          advance(\":\");\n          nextInnerDE();\n        } else if (id) {\n          // in this case we are assigning (not declaring), so check assignment\n          if (isAssignment) {\n            checkLeftSideAssign(context, state.tokens.curr);\n          }\n          identifiers.push({ id: id, token: state.tokens.curr });\n        }\n\n        if (isRest && checkPunctuator(state.tokens.next, \",\")) {\n          warning(\"W130\", state.tokens.next);\n        }\n      }\n    };\n\n    var id, value;\n    if (checkPunctuator(firstToken, \"[\")) {\n      if (!openingParsed) {\n        advance(\"[\");\n      }\n      if (checkPunctuator(state.tokens.next, \"]\")) {\n        warning(\"W137\", state.tokens.curr);\n      }\n      var element_after_rest = false;\n      while (!checkPunctuator(state.tokens.next, \"]\")) {\n        var isRest = spreadrest(\"rest\");\n\n        nextInnerDE();\n\n        if (isRest && !element_after_rest &&\n            checkPunctuator(state.tokens.next, \",\")) {\n          warning(\"W130\", state.tokens.next);\n          element_after_rest = true;\n        }\n        if (!isRest && checkPunctuator(state.tokens.next, \"=\")) {\n          if (checkPunctuator(state.tokens.prev, \"...\")) {\n            /* istanbul ignore next */\n            advance(\"]\");\n          } else {\n            advance(\"=\");\n          }\n          id = state.tokens.prev;\n          value = expression(context, 10);\n          if (value && value.identifier && value.value === \"undefined\") {\n            warning(\"W080\", id, id.value);\n          }\n        }\n        if (!checkPunctuator(state.tokens.next, \"]\")) {\n          advance(\",\");\n        }\n      }\n      advance(\"]\");\n    } else if (checkPunctuator(firstToken, \"{\")) {\n\n      if (!openingParsed) {\n        advance(\"{\");\n      }\n      if (checkPunctuator(state.tokens.next, \"}\")) {\n        warning(\"W137\", state.tokens.curr);\n      }\n      while (!checkPunctuator(state.tokens.next, \"}\")) {\n        assignmentProperty(context);\n        if (checkPunctuator(state.tokens.next, \"=\")) {\n          advance(\"=\");\n          id = state.tokens.prev;\n          value = expression(context, 10);\n          if (value && value.identifier && value.value === \"undefined\") {\n            warning(\"W080\", id, id.value);\n          }\n        }\n        if (!checkPunctuator(state.tokens.next, \"}\")) {\n          advance(\",\");\n          if (checkPunctuator(state.tokens.next, \"}\")) {\n            // Trailing comma\n            // ObjectBindingPattern: { BindingPropertyList , }\n            break;\n          }\n        }\n      }\n      advance(\"}\");\n    }\n    return identifiers;\n  }\n\n  function destructuringPatternMatch(tokens, value) {\n    var first = value.first;\n\n    if (!first)\n      return;\n\n    _.zip(tokens, Array.isArray(first) ? first : [ first ]).forEach(function(val) {\n      var token = val[0];\n      var value = val[1];\n\n      if (token && value)\n        token.first = value;\n      else if (token && token.first && !value)\n        /* istanbul ignore next */\n        warning(\"W080\", token.first, token.first.value);\n    });\n  }\n\n  function blockVariableStatement(type, statement, context) {\n    // used for both let and const statements\n\n    var noin = context & prodParams.noin;\n    var isLet = type === \"let\";\n    var isConst = type === \"const\";\n    var tokens, lone, value, letblock;\n\n    if (!state.inES6()) {\n      warning(\"W104\", state.tokens.curr, type, \"6\");\n    }\n\n    if (isLet && isMozillaLet()) {\n      advance(\"(\");\n      state.funct[\"(scope)\"].stack();\n      letblock = true;\n      statement.declaration = false;\n    }\n\n    statement.first = [];\n    for (;;) {\n      var names = [];\n      if (_.includes([\"{\", \"[\"], state.tokens.next.value)) {\n        tokens = destructuringPattern(context);\n        lone = false;\n      } else {\n        tokens = [ { id: identifier(context), token: state.tokens.curr } ];\n        lone = true;\n      }\n\n      // A `const` declaration without an initializer is permissible within the\n      // head of for-in and for-of statements. If this binding list is being\n      // parsed as part of a `for` statement of any kind, allow the initializer\n      // to be omitted. Although this may erroneously allow such forms from\n      // \"C-style\" `for` statements (i.e. `for (const x;;) {}`, the `for`\n      // statement logic includes dedicated logic to issue the error for such\n      // cases.\n      if (!noin && isConst && state.tokens.next.id !== \"=\") {\n        warning(\"E012\", state.tokens.curr, state.tokens.curr.value);\n      }\n\n      for (var t in tokens) {\n        if (tokens.hasOwnProperty(t)) {\n          t = tokens[t];\n\n          // It is a Syntax Error if the BoundNames of BindingList contains\n          // \"let\".\n          if (t.id === \"let\") {\n            /* istanbul ignore next */\n            warning(\"W024\", t.token, t.id);\n          }\n\n          if (state.funct[\"(scope)\"].block.isGlobal()) {\n            if (predefined[t.id] === false) {\n              warning(\"W079\", t.token, t.id);\n            }\n          }\n          if (t.id) {\n            state.funct[\"(scope)\"].addbinding(t.id, {\n              type: type,\n              token: t.token });\n            names.push(t.token);\n          }\n        }\n      }\n\n      if (state.tokens.next.id === \"=\") {\n        statement.hasInitializer = true;\n\n        advance(\"=\");\n        if (!noin && peek(0).id === \"=\" && state.tokens.next.identifier) {\n          warning(\"W120\", state.tokens.next, state.tokens.next.value);\n        }\n        var id = state.tokens.prev;\n        value = expression(context, 10);\n        if (value) {\n          if (value.identifier && value.value === \"undefined\") {\n            warning(\"W080\", id, id.value);\n          }\n          if (!lone) {\n            destructuringPatternMatch(names, value);\n          }\n        }\n      }\n\n      // Bindings are not immediately initialized in for-in and for-of\n      // statements. As with `const` initializers (described above), the `for`\n      // statement parsing logic includes\n      if (state.tokens.next.value !== \"in\" && state.tokens.next.value !== \"of\") {\n        for (t in tokens) {\n          if (tokens.hasOwnProperty(t)) {\n            t = tokens[t];\n            state.funct[\"(scope)\"].initialize(t.id);\n          }\n        }\n      }\n\n      statement.first = statement.first.concat(names);\n\n      if (state.tokens.next.id !== \",\") {\n        break;\n      }\n\n      statement.hasComma = true;\n      advance(\",\");\n      checkComma();\n    }\n    if (letblock) {\n      advance(\")\");\n      block(context, true, true);\n      statement.block = true;\n      state.funct[\"(scope)\"].unstack();\n    }\n\n    return statement;\n  }\n\n  var conststatement = stmt(\"const\", function(context) {\n    return blockVariableStatement(\"const\", this, context);\n  });\n  conststatement.exps = true;\n  conststatement.declaration = true;\n\n\n  /**\n   * Determine if the current `let` token designates the beginning of a \"let\n   * block\" or \"let expression\" as implemented in the Mozilla SpiderMonkey\n   * engine.\n   *\n   * This function will only return `true` if Mozilla extensions have been\n   * enabled. It would be preferable to detect the language feature regardless\n   * of the parser's state because this would allow JSHint to instruct users to\n   * enable the `moz` option where necessary. This is not possible because the\n   * language extension is not compatible with standard JavaScript. For\n   * example, the following program code may describe a \"let block\" or a\n   * function invocation:\n   *\n   *     let(x)\n   *     {\n   *       typeof x;\n   *     }\n   *\n   * @returns {boolean}\n   */\n  function isMozillaLet() {\n    return state.tokens.next.id === \"(\" && state.inMoz();\n  }\n  var letstatement = stmt(\"let\", function(context) {\n    return blockVariableStatement(\"let\", this, context);\n  });\n  letstatement.nud = function(context, rbp) {\n    if (isMozillaLet()) {\n      // create a new block scope we use only for the current expression\n      state.funct[\"(scope)\"].stack();\n      advance(\"(\");\n      state.tokens.prev.fud(context);\n      advance(\")\");\n      expression(context, rbp);\n      state.funct[\"(scope)\"].unstack();\n    } else {\n      this.exps = false;\n      return state.syntax[\"(identifier)\"].nud.apply(this, arguments);\n    }\n  };\n  letstatement.meta = { es5: true, isFutureReservedWord: false, strictOnly: true };\n  letstatement.exps = true;\n  letstatement.declaration = true;\n  letstatement.useFud = function(context) {\n    var next = state.tokens.next;\n    var nextIsBindingName;\n\n    if (this.line !== next.line && !state.inES6()) {\n      return false;\n    }\n\n    // JSHint generally interprets `let` as a reserved word even though it is\n    // not considered as such by the ECMAScript specification because doing so\n    // simplifies parsing logic. It is special-cased here so that code such as\n    //\n    //     let\n    //     let\n    //\n    // is correctly interpreted as an invalid LexicalBinding. (Without this\n    // consideration, the code above would be parsed as two\n    // IdentifierReferences.)\n    nextIsBindingName = next.identifier && (!isReserved(context, next) ||\n      next.id === \"let\");\n\n    return nextIsBindingName || checkPunctuators(next, [\"{\", \"[\"]) ||\n      isMozillaLet();\n  };\n\n  var varstatement = stmt(\"var\", function(context) {\n    var noin = context & prodParams.noin;\n    var tokens, lone, value, id;\n\n    this.first = [];\n    for (;;) {\n      var names = [];\n      if (_.includes([\"{\", \"[\"], state.tokens.next.value)) {\n        tokens = destructuringPattern(context);\n        lone = false;\n      } else {\n        tokens = [];\n        id = identifier(context);\n\n        if (id) {\n          tokens.push({ id: id, token: state.tokens.curr });\n        }\n\n        lone = true;\n      }\n\n      if (state.option.varstmt) {\n        warning(\"W132\", this);\n      }\n\n\n      for (var t in tokens) {\n        if (tokens.hasOwnProperty(t)) {\n          t = tokens[t];\n          if (state.funct[\"(global)\"] && !state.impliedClosure()) {\n            if (predefined[t.id] === false) {\n              warning(\"W079\", t.token, t.id);\n            } else if (state.option.futurehostile === false) {\n              if ((!state.inES5() && vars.ecmaIdentifiers[5][t.id] === false) ||\n                (!state.inES6() && vars.ecmaIdentifiers[6][t.id] === false)) {\n                warning(\"W129\", t.token, t.id);\n              }\n            }\n          }\n          if (t.id) {\n            state.funct[\"(scope)\"].addbinding(t.id, {\n              type: \"var\",\n              token: t.token });\n\n            names.push(t.token);\n          }\n        }\n      }\n\n      if (state.tokens.next.id === \"=\") {\n        this.hasInitializer = true;\n\n        state.nameStack.set(state.tokens.curr);\n\n        advance(\"=\");\n        if (peek(0).id === \"=\" && state.tokens.next.identifier) {\n          if (!noin &&\n              !state.funct[\"(params)\"] ||\n              state.funct[\"(params)\"].indexOf(state.tokens.next.value) === -1) {\n            warning(\"W120\", state.tokens.next, state.tokens.next.value);\n          }\n        }\n        id = state.tokens.prev;\n        value = expression(context, 10);\n        if (value) {\n          if (!state.funct[\"(loopage)\"] && value.identifier &&\n            value.value === \"undefined\") {\n            warning(\"W080\", id, id.value);\n          }\n          if (!lone) {\n            destructuringPatternMatch(names, value);\n          }\n        }\n      }\n\n      this.first = this.first.concat(names);\n\n      if (state.tokens.next.id !== \",\") {\n        break;\n      }\n      this.hasComma = true;\n      advance(\",\");\n      checkComma();\n    }\n\n    return this;\n  });\n  varstatement.exps = true;\n\n  blockstmt(\"function\", function(context) {\n    var inexport = context & prodParams.export;\n    var generator = false;\n    var isAsync = context & prodParams.preAsync;\n    var labelType = \"\";\n\n    if (isAsync) {\n      labelType = \"async \";\n    }\n\n    if (state.tokens.next.value === \"*\") {\n      if (isAsync && !state.inES9()) {\n        warning(\"W119\", state.tokens.prev, \"async generators\", \"9\");\n      } else if (!isAsync && !state.inES6(true)) {\n        warning(\"W119\", state.tokens.next, \"function*\", \"6\");\n      }\n\n      advance(\"*\");\n      labelType += \"generator \";\n      generator = true;\n    }\n\n    labelType += \"function\";\n\n    if (inblock) {\n      warning(\"W082\", state.tokens.curr);\n    }\n    this.name = optionalidentifier(context) ? state.tokens.curr : null;\n\n    if (!this.name) {\n      if (!inexport) {\n        warning(\"W025\");\n      }\n    } else {\n      state.funct[\"(scope)\"].addbinding(this.name.value, {\n        type: labelType,\n        token: state.tokens.curr,\n        initialized: true });\n    }\n\n    var f = doFunction(context, {\n      name: this.name && this.name.value,\n      statement: this,\n      type: generator ? \"generator\" : null,\n      ignoreLoopFunc: inblock // a declaration may already have warned\n    });\n\n    // If the function declaration is strict because the surrounding code is\n    // strict, the invalid name will trigger E008 when the scope manager\n    // attempts to create a binding in the strict environment record. An error\n    // should only be signaled here when the function itself enables strict\n    // mode (the scope manager will not report an error because a declaration\n    // does not introduce a binding into the function's environment record).\n    var enablesStrictMode = f[\"(isStrict)\"] && !state.isStrict();\n    if (this.name && (f[\"(name)\"] === \"arguments\" || f[\"(name)\"] === \"eval\") &&\n      enablesStrictMode) {\n      error(\"E008\", this.name);\n    }\n\n    // Although the parser correctly recognizes the statement boundary in this\n    // condition, it's support for the invalid \"empty grouping\" expression\n    // makes it tolerant of productions such as `function f() {}();`.\n    if (state.tokens.next.id === \"(\" && peek().id === \")\" && peek(1).id !== \"=>\" &&\n      state.tokens.next.line === state.tokens.curr.line) {\n      error(\"E039\");\n    }\n    return this;\n  }).declaration = true;\n\n  prefix(\"function\", function(context) {\n    var generator = false;\n    var isAsync = context & prodParams.preAsync;\n\n    if (state.tokens.next.value === \"*\") {\n      if (isAsync && !state.inES9()) {\n        warning(\"W119\", state.tokens.prev, \"async generators\", \"9\");\n      } else if (!isAsync && !state.inES6(true)) {\n        warning(\"W119\", state.tokens.curr, \"function*\", \"6\");\n      }\n\n      advance(\"*\");\n      generator = true;\n    }\n\n    // This context modification restricts the use of `await` as the optional\n    // BindingIdentifier in async function expressions.\n    this.name = optionalidentifier(isAsync ? context | prodParams.async : context) ?\n      state.tokens.curr : null;\n\n    var f = doFunction(context, {\n      name: this.name && this.name.value,\n      type: generator ? \"generator\" : null\n    });\n\n    if (generator && this.name && this.name.value === \"yield\") {\n      error(\"E024\", this.name, \"yield\");\n    }\n\n    if (this.name && (f[\"(name)\"] === \"arguments\" || f[\"(name)\"] === \"eval\") &&\n      f[\"(isStrict)\"]) {\n      error(\"E008\", this.name);\n    }\n\n    return this;\n  });\n\n  blockstmt(\"if\", function(context) {\n    var t = state.tokens.next;\n    increaseComplexityCount();\n    advance(\"(\");\n    var expr = expression(context, 0);\n\n    if (!expr) {\n      quit(\"E041\", this);\n    }\n\n    checkCondAssignment(expr);\n\n    // When the if is within a for-in loop, check if the condition\n    // starts with a negation operator\n    var forinifcheck = null;\n    if (state.option.forin && state.forinifcheckneeded) {\n      state.forinifcheckneeded = false; // We only need to analyze the first if inside the loop\n      forinifcheck = state.forinifchecks[state.forinifchecks.length - 1];\n      if (expr.type === \"(punctuator)\" && expr.value === \"!\") {\n        forinifcheck.type = \"(negative)\";\n      } else {\n        forinifcheck.type = \"(positive)\";\n      }\n    }\n\n    advance(\")\", t);\n    var s = block(context, true, true);\n\n    // When the if is within a for-in loop and the condition has a negative form,\n    // check if the body contains nothing but a continue statement\n    if (forinifcheck && forinifcheck.type === \"(negative)\") {\n      if (s && s[0] && s[0].type === \"(identifier)\" && s[0].value === \"continue\") {\n        forinifcheck.type = \"(negative-with-continue)\";\n      }\n    }\n\n    if (state.tokens.next.id === \"else\") {\n      advance(\"else\");\n      if (state.tokens.next.id === \"if\" || state.tokens.next.id === \"switch\") {\n        statement(context);\n      } else {\n        block(context, true, true);\n      }\n    }\n    return this;\n  });\n\n  blockstmt(\"try\", function(context) {\n    var b;\n    var hasParameter = false;\n\n    function catchParameter() {\n      advance(\"(\");\n\n      if (checkPunctuators(state.tokens.next, [\"[\", \"{\"])) {\n        var tokens = destructuringPattern(context);\n        _.each(tokens, function(token) {\n          if (token.id) {\n            state.funct[\"(scope)\"].addParam(token.id, token.token, \"exception\");\n          }\n        });\n      } else if (state.tokens.next.type !== \"(identifier)\") {\n        warning(\"E030\", state.tokens.next, state.tokens.next.value);\n      } else {\n        // only advance if an identifier is present. This allows JSHint to\n        // recover from the case where no value is specified.\n        state.funct[\"(scope)\"].addParam(identifier(context), state.tokens.curr, \"exception\");\n      }\n\n      if (state.tokens.next.value === \"if\") {\n        if (!state.inMoz()) {\n          warning(\"W118\", state.tokens.curr, \"catch filter\");\n        }\n        advance(\"if\");\n        expression(context, 0);\n      }\n\n      advance(\")\");\n    }\n\n    block(context | prodParams.tryClause, true);\n\n    while (state.tokens.next.id === \"catch\") {\n      increaseComplexityCount();\n      if (b && (!state.inMoz())) {\n        warning(\"W118\", state.tokens.next, \"multiple catch blocks\");\n      }\n      advance(\"catch\");\n      if (state.tokens.next.id !== \"{\") {\n        state.funct[\"(scope)\"].stack(\"catchparams\");\n        hasParameter = true;\n        catchParameter();\n      } else if (!state.inES10()) {\n        warning(\"W119\", state.tokens.curr, \"optional catch binding\", \"10\");\n      }\n      block(context, false);\n\n      if (hasParameter) {\n        state.funct[\"(scope)\"].unstack();\n        hasParameter = false;\n      }\n      b = true;\n    }\n\n    if (state.tokens.next.id === \"finally\") {\n      advance(\"finally\");\n      block(context, true);\n      return;\n    }\n\n    if (!b) {\n      error(\"E021\", state.tokens.next, \"catch\", state.tokens.next.value);\n    }\n\n    return this;\n  });\n\n  blockstmt(\"while\", function(context) {\n    var t = state.tokens.next;\n    state.funct[\"(breakage)\"] += 1;\n    state.funct[\"(loopage)\"] += 1;\n    increaseComplexityCount();\n    advance(\"(\");\n    checkCondAssignment(expression(context, 0));\n    advance(\")\", t);\n    block(context, true, true);\n    state.funct[\"(breakage)\"] -= 1;\n    state.funct[\"(loopage)\"] -= 1;\n    return this;\n  }).labelled = true;\n\n  blockstmt(\"with\", function(context) {\n    var t = state.tokens.next;\n    if (state.isStrict()) {\n      error(\"E010\", state.tokens.curr);\n    } else if (!state.option.withstmt) {\n      warning(\"W085\", state.tokens.curr);\n    }\n\n    advance(\"(\");\n    expression(context, 0);\n    advance(\")\", t);\n    block(context, true, true);\n\n    return this;\n  });\n\n  blockstmt(\"switch\", function(context) {\n    var t = state.tokens.next;\n    var g = false;\n    var noindent = false;\n    var seenCase = false;\n\n    state.funct[\"(breakage)\"] += 1;\n    advance(\"(\");\n    checkCondAssignment(expression(context, 0));\n    advance(\")\", t);\n    t = state.tokens.next;\n    advance(\"{\");\n    state.funct[\"(scope)\"].stack();\n\n    if (state.tokens.next.from === indent)\n      noindent = true;\n\n    if (!noindent)\n      indent += state.option.indent;\n\n    for (;;) {\n      switch (state.tokens.next.id) {\n      case \"case\":\n        switch (state.funct[\"(verb)\"]) {\n        case \"yield\":\n        case \"break\":\n        case \"case\":\n        case \"continue\":\n        case \"return\":\n        case \"switch\":\n        case \"throw\":\n          break;\n        case \"default\":\n          if (state.option.leanswitch) {\n            warning(\"W145\", state.tokens.next);\n          }\n\n          break;\n        default:\n          // You can tell JSHint that you don't use break intentionally by\n          // adding a comment /* falls through */ on a line just before\n          // the next `case`.\n          if (!state.tokens.curr.caseFallsThrough) {\n            warning(\"W086\", state.tokens.curr, \"case\");\n          }\n        }\n\n        advance(\"case\");\n        expression(context, 0);\n        seenCase = true;\n        increaseComplexityCount();\n        g = true;\n        advance(\":\");\n        state.funct[\"(verb)\"] = \"case\";\n        break;\n      case \"default\":\n        switch (state.funct[\"(verb)\"]) {\n        case \"yield\":\n        case \"break\":\n        case \"continue\":\n        case \"return\":\n        case \"throw\":\n          break;\n        case \"case\":\n          if (state.option.leanswitch) {\n            warning(\"W145\", state.tokens.curr);\n          }\n\n          break;\n        default:\n          // Do not display a warning if 'default' is the first statement or if\n          // there is a special /* falls through */ comment.\n          if (seenCase && !state.tokens.curr.caseFallsThrough) {\n            warning(\"W086\", state.tokens.curr, \"default\");\n          }\n        }\n\n        advance(\"default\");\n        g = true;\n        advance(\":\");\n        state.funct[\"(verb)\"] = \"default\";\n        break;\n      case \"}\":\n        if (!noindent)\n          indent -= state.option.indent;\n\n        advance(\"}\", t);\n        state.funct[\"(scope)\"].unstack();\n        state.funct[\"(breakage)\"] -= 1;\n        state.funct[\"(verb)\"] = undefined;\n        return;\n      /* istanbul ignore next */\n      case \"(end)\":\n        error(\"E023\", state.tokens.next, \"}\");\n        return;\n      default:\n        indent += state.option.indent;\n        if (g) {\n          switch (state.tokens.curr.id) {\n          /* istanbul ignore next */\n          case \",\":\n            error(\"E040\");\n            return;\n          case \":\":\n            g = false;\n            statements(context);\n            break;\n          /* istanbul ignore next */\n          default:\n            error(\"E025\", state.tokens.curr);\n            return;\n          }\n        } else {\n          /* istanbul ignore else */\n          if (state.tokens.curr.id === \":\") {\n            advance(\":\");\n            error(\"E024\", state.tokens.curr, \":\");\n            statements(context);\n          } else {\n            error(\"E021\", state.tokens.next, \"case\", state.tokens.next.value);\n            return;\n          }\n        }\n        indent -= state.option.indent;\n      }\n    }\n  }).labelled = true;\n\n  stmt(\"debugger\", function() {\n    if (!state.option.debug) {\n      warning(\"W087\", this);\n    }\n    return this;\n  }).exps = true;\n\n  (function() {\n    var x = stmt(\"do\", function(context) {\n      state.funct[\"(breakage)\"] += 1;\n      state.funct[\"(loopage)\"] += 1;\n      increaseComplexityCount();\n\n      this.first = block(context, true, true);\n      advance(\"while\");\n      var t = state.tokens.next;\n      advance(\"(\");\n      checkCondAssignment(expression(context, 0));\n      advance(\")\", t);\n      state.funct[\"(breakage)\"] -= 1;\n      state.funct[\"(loopage)\"] -= 1;\n      return this;\n    });\n    x.labelled = true;\n    x.exps = true;\n  }());\n\n  blockstmt(\"for\", function(context) {\n    var s, t = state.tokens.next;\n    var letscope = false;\n    var isAsync = false;\n    var foreachtok = null;\n\n    if (t.value === \"each\") {\n      foreachtok = t;\n      advance(\"each\");\n      if (!state.inMoz()) {\n        warning(\"W118\", state.tokens.curr, \"for each\");\n      }\n    }\n\n    if (state.tokens.next.identifier && state.tokens.next.value === \"await\") {\n      advance(\"await\");\n      isAsync = true;\n\n      if (!(context & prodParams.async)) {\n        error(\"E024\", state.tokens.curr, \"await\");\n      } else if (!state.inES9()) {\n        warning(\"W119\", state.tokens.curr, \"asynchronous iteration\", \"9\");\n      }\n    }\n\n    increaseComplexityCount();\n    advance(\"(\");\n\n    // what kind of for(…) statement it is? for(…of…)? for(…in…)? for(…;…;…)?\n    var nextop; // contains the token of the \"in\" or \"of\" operator\n    var comma; // First comma punctuator at level 0\n    var initializer; // First initializer at level 0\n    var bindingPower;\n    var targets;\n    var target;\n    var decl;\n    var afterNext = peek();\n\n    var headContext = context | prodParams.noin;\n\n    if (state.tokens.next.id === \"var\") {\n      advance(\"var\");\n      decl = state.tokens.curr.fud(headContext);\n      comma = decl.hasComma ? decl : null;\n      initializer = decl.hasInitializer ? decl : null;\n    } else if (state.tokens.next.id === \"const\" ||\n      // The \"let\" keyword only signals a lexical binding if it is followed by\n      // an identifier, `{`, or `[`. Otherwise, it should be parsed as an\n      // IdentifierReference (i.e. in a subsquent branch).\n      (state.tokens.next.id === \"let\" &&\n        ((afterNext.identifier && afterNext.id !== \"in\") ||\n         checkPunctuators(afterNext, [\"{\", \"[\"])))) {\n      advance(state.tokens.next.id);\n      // create a new block scope\n      letscope = true;\n      state.funct[\"(scope)\"].stack();\n      decl = state.tokens.curr.fud(headContext);\n      comma = decl.hasComma ? decl : null;\n      initializer = decl.hasInitializer ? decl : null;\n    } else if (!checkPunctuator(state.tokens.next, \";\")) {\n      targets = [];\n\n      while (state.tokens.next.value !== \"in\" &&\n        state.tokens.next.value !== \"of\" &&\n        !checkPunctuator(state.tokens.next, \";\")) {\n\n        if (checkPunctuators(state.tokens.next, [\"{\", \"[\"])) {\n          destructuringPattern(headContext, { assignment: true })\n            .forEach(function(elem) {\n              this.push(elem.token);\n            }, targets);\n          if (checkPunctuator(state.tokens.next, \"=\")) {\n            advance(\"=\");\n            initializer = state.tokens.curr;\n            expression(headContext, 10);\n          }\n        } else {\n          target = expression(headContext, 10);\n\n          if (target) {\n            if (target.type === \"(identifier)\") {\n              targets.push(target);\n            } else if (checkPunctuator(target, \"=\")) {\n              initializer = target;\n              targets.push(target);\n            }\n          }\n        }\n\n        if (checkPunctuator(state.tokens.next, \",\")) {\n          advance(\",\");\n\n          if (!comma) {\n            comma = state.tokens.curr;\n          }\n        }\n      }\n\n      //checkLeftSideAssign(target, nextop);\n\n      // In the event of a syntax error, do not issue warnings regarding the\n      // implicit creation of bindings.\n      if (!initializer && !comma) {\n        targets.forEach(function(token) {\n          if (!state.funct[\"(scope)\"].has(token.value)) {\n            warning(\"W088\", token, token.value);\n          }\n        });\n      }\n    }\n\n    nextop = state.tokens.next;\n\n    if (isAsync && nextop.value !== \"of\") {\n      error(\"E066\", nextop);\n    }\n\n    // if we're in a for (… in|of …) statement\n    if (_.includes([\"in\", \"of\"], nextop.value)) {\n      if (nextop.value === \"of\") {\n        bindingPower = 20;\n\n        if (!state.inES6()) {\n          warning(\"W104\", nextop, \"for of\", \"6\");\n        }\n      } else {\n        bindingPower = 0;\n      }\n      if (comma) {\n        error(\"W133\", comma, nextop.value, \"more than one ForBinding\");\n      }\n      if (initializer) {\n        error(\"W133\", initializer, nextop.value, \"initializer is forbidden\");\n      }\n      if (target && !comma && !initializer) {\n        checkLeftSideAssign(context, target, nextop);\n      }\n\n      advance(nextop.value);\n\n      // The binding power is variable because for-in statements accept any\n      // Expression in this position, while for-of statements are limited to\n      // AssignmentExpressions. For example:\n      //\n      //     for ( LeftHandSideExpression in Expression ) Statement\n      //     for ( LeftHandSideExpression of AssignmentExpression ) Statement\n      expression(context, bindingPower);\n      advance(\")\", t);\n\n      if (nextop.value === \"in\" && state.option.forin) {\n        state.forinifcheckneeded = true;\n\n        if (state.forinifchecks === undefined) {\n          state.forinifchecks = [];\n        }\n\n        // Push a new for-in-if check onto the stack. The type will be modified\n        // when the loop's body is parsed and a suitable if statement exists.\n        state.forinifchecks.push({\n          type: \"(none)\"\n        });\n      }\n\n      state.funct[\"(breakage)\"] += 1;\n      state.funct[\"(loopage)\"] += 1;\n\n      s = block(context, true, true);\n\n      if (nextop.value === \"in\" && state.option.forin) {\n        if (state.forinifchecks && state.forinifchecks.length > 0) {\n          var check = state.forinifchecks.pop();\n\n          if (// No if statement or not the first statement in loop body\n              s && s.length > 0 && (typeof s[0] !== \"object\" || s[0].value !== \"if\") ||\n              // Positive if statement is not the only one in loop body\n              check.type === \"(positive)\" && s.length > 1 ||\n              // Negative if statement but no continue\n              check.type === \"(negative)\") {\n            warning(\"W089\", this);\n          }\n        }\n\n        // Reset the flag in case no if statement was contained in the loop body\n        state.forinifcheckneeded = false;\n      }\n\n      state.funct[\"(breakage)\"] -= 1;\n      state.funct[\"(loopage)\"] -= 1;\n\n    } else {\n      if (foreachtok) {\n        error(\"E045\", foreachtok);\n      }\n\n      advance(\";\");\n      if (decl && decl.first && decl.first[0]) {\n        if (decl.value === \"const\"  && !decl.hasInitializer) {\n          warning(\"E012\", decl, decl.first[0].value);\n        }\n\n        decl.first.forEach(function(token) {\n          state.funct[\"(scope)\"].initialize(token.value);\n        });\n      }\n\n      // start loopage after the first ; as the next two expressions are executed\n      // on every loop\n      state.funct[\"(loopage)\"] += 1;\n      if (state.tokens.next.id !== \";\") {\n        checkCondAssignment(expression(context, 0));\n      }\n\n      advance(\";\");\n      if (state.tokens.next.id === \";\") {\n        error(\"E021\", state.tokens.next, \")\", \";\");\n      }\n      if (state.tokens.next.id !== \")\") {\n        for (;;) {\n          expression(context, 0);\n          if (state.tokens.next.id !== \",\") {\n            break;\n          }\n          advance(\",\");\n          checkComma();\n        }\n      }\n      advance(\")\", t);\n      state.funct[\"(breakage)\"] += 1;\n      block(context, true, true);\n      state.funct[\"(breakage)\"] -= 1;\n      state.funct[\"(loopage)\"] -= 1;\n    }\n\n    // unstack loop blockscope\n    if (letscope) {\n      state.funct[\"(scope)\"].unstack();\n    }\n    return this;\n  }).labelled = true;\n\n\n  stmt(\"break\", function() {\n    var v = state.tokens.next.value;\n\n    if (state.tokens.next.identifier &&\n        sameLine(state.tokens.curr, state.tokens.next)) {\n      if (!state.funct[\"(scope)\"].funct.hasLabel(v)) {\n        warning(\"W090\", state.tokens.next, v);\n      }\n      this.first = state.tokens.next;\n      advance();\n    } else {\n      if (state.funct[\"(breakage)\"] === 0)\n        warning(\"W052\", state.tokens.next, this.value);\n    }\n\n    reachable(this);\n\n    return this;\n  }).exps = true;\n\n\n  stmt(\"continue\", function() {\n    var v = state.tokens.next.value;\n\n    if (state.funct[\"(breakage)\"] === 0 || !state.funct[\"(loopage)\"]) {\n      warning(\"W052\", state.tokens.next, this.value);\n    }\n\n    if (state.tokens.next.identifier) {\n      if (sameLine(state.tokens.curr, state.tokens.next)) {\n        if (!state.funct[\"(scope)\"].funct.hasLabel(v)) {\n          warning(\"W090\", state.tokens.next, v);\n        }\n        this.first = state.tokens.next;\n        advance();\n      }\n    }\n\n    reachable(this);\n\n    return this;\n  }).exps = true;\n\n\n  stmt(\"return\", function(context) {\n    if (sameLine(this, state.tokens.next)) {\n      if (state.tokens.next.id !== \";\" && !state.tokens.next.reach) {\n        this.first = expression(context, 0);\n\n        if (this.first &&\n            this.first.type === \"(punctuator)\" && this.first.value === \"=\" &&\n            !this.first.paren && !state.option.boss) {\n          warning(\"W093\", this.first);\n        }\n\n        if (state.option.noreturnawait && context & prodParams.async &&\n            !(context & prodParams.tryClause) &&\n            this.first.identifier && this.first.value === \"await\") {\n          warning(\"W146\", this.first);\n        }\n      }\n    } else {\n      if (state.tokens.next.type === \"(punctuator)\" &&\n        [\"[\", \"{\", \"+\", \"-\"].indexOf(state.tokens.next.value) > -1) {\n        nolinebreak(this); // always warn (Line breaking error)\n      }\n    }\n\n    reachable(this);\n\n    return this;\n  }).exps = true;\n\n  prefix(\"await\", function(context) {\n    if (context & prodParams.async) {\n      // If the parameters of the current function scope have not been defined,\n      // it is because the current expression is contained within the parameter\n      // list.\n      if (!state.funct[\"(params)\"]) {\n        error(\"E024\", this, \"await\");\n      }\n\n      expression(context, 10);\n      return this;\n    } else {\n      this.exps = false;\n      return state.syntax[\"(identifier)\"].nud.apply(this, arguments);\n    }\n  }).exps = true;\n\n  (function(asyncSymbol) {\n    asyncSymbol.meta = { es5: true, isFutureReservedWord: true, strictOnly: true };\n    asyncSymbol.isFunc = function() {\n      var next = state.tokens.next;\n      var afterParens;\n\n      if (this.line !== next.line) {\n        return false;\n      }\n\n      if (next.id === \"function\") {\n        return true;\n      }\n\n      if (next.id === \"(\") {\n        afterParens = peekThroughParens(0);\n\n        return afterParens.id === \"=>\";\n      }\n\n      if (next.identifier) {\n        return peek().id === \"=>\";\n      }\n\n      return false;\n    };\n    asyncSymbol.useFud = asyncSymbol.isFunc;\n    // async function declaration\n    asyncSymbol.fud = function(context) {\n      if (!state.inES8()) {\n        warning(\"W119\", this, \"async functions\", \"8\");\n      }\n      context |= prodParams.preAsync;\n      context |= prodParams.initial;\n      this.func = expression(context, 0);\n      this.block = this.func.block;\n      this.exps = this.func.exps;\n      return this;\n    };\n    asyncSymbol.exps = true;\n    delete asyncSymbol.reserved;\n  }(prefix(\"async\", function(context, rbp) {\n    if (this.isFunc(context)) {\n      if (!state.inES8()) {\n        warning(\"W119\", this, \"async functions\", \"8\");\n      }\n\n      context |= prodParams.preAsync;\n      this.func = expression(context, rbp);\n      this.identifier = false;\n      return this;\n    }\n\n    this.exps = false;\n    return state.syntax[\"(identifier)\"].nud.apply(this, arguments);\n  })));\n\n  (function(yieldSymbol) {\n    yieldSymbol.rbp = yieldSymbol.lbp = 25;\n    yieldSymbol.exps = true;\n  })(prefix(\"yield\", function(context) {\n    if (state.inMoz()) {\n      return mozYield.call(this, context);\n    }\n\n    if (!(context & prodParams.yield)) {\n      this.exps = false;\n      return state.syntax[\"(identifier)\"].nud.apply(this, arguments);\n    }\n\n    var prev = state.tokens.prev;\n\n    // If the parameters of the current function scope have not been defined,\n    // it is because the current expression is contained within the parameter\n    // list.\n    if (!state.funct[\"(params)\"]) {\n      error(\"E024\", this, \"yield\");\n    }\n\n    if (!this.beginsStmt && prev.lbp > 30 && !checkPunctuators(prev, [\"(\"])) {\n      error(\"E061\", this);\n    }\n\n    if (!state.inES6()) {\n      warning(\"W104\", state.tokens.curr, \"yield\", \"6\");\n    }\n    state.funct[\"(yielded)\"] = true;\n\n    if (state.tokens.next.value === \"*\") {\n      advance(\"*\");\n    }\n\n    // Parse operand\n    if (state.tokens.curr.value === \"*\" || sameLine(state.tokens.curr, state.tokens.next)) {\n      if (state.tokens.next.nud) {\n\n        nobreaknonadjacent(state.tokens.curr, state.tokens.next);\n        this.first = expression(context, 10);\n\n        if (this.first.type === \"(punctuator)\" && this.first.value === \"=\" &&\n            !this.first.paren && !state.option.boss) {\n          warning(\"W093\", this.first);\n        }\n      } else if (state.tokens.next.led) {\n        if (state.tokens.next.id !== \",\") {\n          error(\"W017\", state.tokens.next);\n        }\n      }\n    }\n\n    return this;\n  }));\n\n  /**\n   * Parsing logic for non-standard Mozilla implementation of `yield`\n   * expressions.\n   */\n  var mozYield = function(context) {\n    var prev = state.tokens.prev;\n    if (state.inES6(true) && !(context & prodParams.yield)) {\n      error(\"E046\", state.tokens.curr, \"yield\");\n    }\n    state.funct[\"(yielded)\"] = true;\n    var delegatingYield = false;\n\n    if (state.tokens.next.value === \"*\") {\n      delegatingYield = true;\n      advance(\"*\");\n    }\n\n    if (sameLine(this, state.tokens.next)) {\n      if (delegatingYield ||\n          (state.tokens.next.id !== \";\" && !state.option.asi &&\n           !state.tokens.next.reach && state.tokens.next.nud)) {\n\n        nobreaknonadjacent(state.tokens.curr, state.tokens.next);\n        this.first = expression(context, 10);\n\n        if (this.first.type === \"(punctuator)\" && this.first.value === \"=\" &&\n            !this.first.paren && !state.option.boss) {\n          warning(\"W093\", this.first);\n        }\n      }\n      if (state.tokens.next.id !== \")\" &&\n          (prev.lbp > 30 || (!prev.assign && !isEndOfExpr()))) {\n        error(\"E050\", this);\n      }\n    } else if (!state.option.asi) {\n      nolinebreak(this); // always warn (Line breaking error)\n    }\n    return this;\n  };\n\n  stmt(\"throw\", function(context) {\n    nolinebreak(this);\n    this.first = expression(context, 20);\n\n    reachable(this);\n\n    return this;\n  }).exps = true;\n\n  prefix(\"import\", function(context) {\n    var mp = metaProperty(context, \"meta\", function() {\n      if (!state.inES11(true)) {\n        warning(\"W119\", state.tokens.prev, \"import.meta\", \"11\");\n      }\n      if (!state.option.module) {\n        error(\"E070\", state.tokens.prev);\n      }\n    });\n\n    if (mp) {\n      return mp;\n    }\n\n    if (!checkPunctuator(state.tokens.next, \"(\")) {\n      return state.syntax[\"(identifier)\"].nud.call(this, context);\n    }\n\n    if (!state.inES11()) {\n      warning(\"W119\", state.tokens.curr, \"dynamic import\", \"11\");\n    }\n\n    advance(\"(\");\n    expression(context, 10);\n    advance(\")\");\n    return this;\n  });\n\n  var importSymbol = stmt(\"import\", function(context) {\n    if (!state.funct[\"(scope)\"].block.isGlobal()) {\n      error(\"E053\", state.tokens.curr, \"Import\");\n    }\n\n    if (!state.inES6()) {\n      warning(\"W119\", state.tokens.curr, \"import\", \"6\");\n    }\n\n    if (state.tokens.next.type === \"(string)\") {\n      // ModuleSpecifier :: StringLiteral\n      advance(\"(string)\");\n      return this;\n    }\n\n    if (state.tokens.next.identifier) {\n      // ImportClause :: ImportedDefaultBinding\n      this.name = identifier(context);\n      // Import bindings are immutable (see ES6 8.1.1.5.5)\n      state.funct[\"(scope)\"].addbinding(this.name, {\n        type: \"import\",\n        initialized: true,\n        token: state.tokens.curr });\n\n      if (state.tokens.next.value === \",\") {\n        // ImportClause :: ImportedDefaultBinding , NameSpaceImport\n        // ImportClause :: ImportedDefaultBinding , NamedImports\n        advance(\",\");\n        // At this point, we intentionally fall through to continue matching\n        // either NameSpaceImport or NamedImports.\n        // Discussion:\n        // https://github.com/jshint/jshint/pull/2144#discussion_r23978406\n      } else {\n        advance(\"from\");\n        advance(\"(string)\");\n        return this;\n      }\n    }\n\n    if (state.tokens.next.id === \"*\") {\n      // ImportClause :: NameSpaceImport\n      advance(\"*\");\n      advance(\"as\");\n      if (state.tokens.next.identifier) {\n        this.name = identifier(context);\n        // Import bindings are immutable (see ES6 8.1.1.5.5)\n        state.funct[\"(scope)\"].addbinding(this.name, {\n          type: \"import\",\n          initialized: true,\n          token: state.tokens.curr });\n      }\n    } else {\n      // ImportClause :: NamedImports\n      advance(\"{\");\n      for (;;) {\n        if (state.tokens.next.value === \"}\") {\n          advance(\"}\");\n          break;\n        }\n        var importName;\n        if (peek().value === \"as\") {\n          identifier(context, true);\n          advance(\"as\");\n          importName = identifier(context);\n        } else {\n          importName = identifier(context);\n        }\n\n        // Import bindings are immutable (see ES6 8.1.1.5.5)\n        state.funct[\"(scope)\"].addbinding(importName, {\n          type: \"import\",\n          initialized: true,\n          token: state.tokens.curr });\n\n        if (state.tokens.next.value === \",\") {\n          advance(\",\");\n        } else if (state.tokens.next.value === \"}\") {\n          advance(\"}\");\n          break;\n        } else {\n          error(\"E024\", state.tokens.next, state.tokens.next.value);\n          break;\n        }\n      }\n    }\n\n    // FromClause\n    advance(\"from\");\n    advance(\"(string)\");\n\n    // Support for ES2015 modules was released without warning for `import`\n    // declarations that lack bindings. Issuing a warning would therefor\n    // constitute a breaking change.\n    // TODO: enable this warning in JSHint 3\n    // if (hasBindings) {\n    //   warning(\"W142\", this, \"import\", moduleSpecifier);\n    // }\n\n    return this;\n  });\n  importSymbol.exps = true;\n  importSymbol.reserved = true;\n  importSymbol.meta = { isFutureReservedWord: true, es5: true };\n  importSymbol.useFud = function() {\n    return !(checkPunctuators(state.tokens.next, [\".\", \"(\"]));\n  };\n  importSymbol.rbp = 161;\n\n  stmt(\"export\", function(context) {\n    var ok = true;\n    var token;\n    var moduleSpecifier;\n    context = context | prodParams.export;\n\n    if (!state.inES6()) {\n      warning(\"W119\", state.tokens.curr, \"export\", \"6\");\n      ok = false;\n    }\n\n    if (!state.funct[\"(scope)\"].block.isGlobal()) {\n      error(\"E053\", state.tokens.curr, \"Export\");\n      ok = false;\n    }\n\n    if (state.tokens.next.value === \"*\") {\n      // ExportDeclaration :: export * FromClause\n      // ExportDeclaration :: export * as IdentifierName FromClause\n      advance(\"*\");\n\n      if (state.tokens.next.value === \"as\") {\n        if (!state.inES11()) {\n          warning(\"W119\", state.tokens.curr, \"export * as ns from\", \"11\");\n        }\n        advance(\"as\");\n        identifier(context, true);\n        state.funct[\"(scope)\"].setExported(null, state.tokens.curr);\n      }\n\n      advance(\"from\");\n      advance(\"(string)\");\n      return this;\n    }\n\n    if (state.tokens.next.type === \"default\") {\n      // ExportDeclaration ::\n      //      export default [lookahead ∉ { function, class }] AssignmentExpression[In] ;\n      //      export default HoistableDeclaration\n      //      export default ClassDeclaration\n\n      // because the 'name' of a default-exported function is, confusingly, 'default'\n      // see https://bocoup.com/blog/whats-in-a-function-name\n      state.nameStack.set(state.tokens.next);\n\n      advance(\"default\");\n      var def = state.tokens.curr;\n      var exportType = state.tokens.next.id;\n      if (exportType === \"function\") {\n        this.block = true;\n        advance(\"function\");\n        token = state.syntax[\"function\"].fud(context);\n        state.funct[\"(scope)\"].setExported(token.name, def);\n      } else if (exportType === \"async\" && peek().id === \"function\") {\n        this.block = true;\n        advance(\"async\");\n        advance(\"function\");\n        token = state.syntax[\"function\"].fud(context | prodParams.preAsync);\n        state.funct[\"(scope)\"].setExported(token.name, def);\n      } else if (exportType === \"class\") {\n        this.block = true;\n        advance(\"class\");\n        token = state.syntax[\"class\"].fud(context);\n        state.funct[\"(scope)\"].setExported(token.name, def);\n      } else {\n        expression(context, 10);\n        state.funct[\"(scope)\"].setExported(null, def);\n      }\n      return this;\n    }\n    if (state.tokens.next.value === \"{\") {\n      // ExportDeclaration :: export ExportClause\n      advance(\"{\");\n      var exportedTokens = [];\n      while (!checkPunctuator(state.tokens.next, \"}\")) {\n        if (!state.tokens.next.identifier) {\n          /* istanbul ignore next */\n          error(\"E030\", state.tokens.next, state.tokens.next.value);\n        }\n        advance();\n\n        if (state.tokens.next.value === \"as\") {\n          advance(\"as\");\n          if (!state.tokens.next.identifier) {\n            /* istanbul ignore next */\n            error(\"E030\", state.tokens.next, state.tokens.next.value);\n          }\n          exportedTokens.push({\n            local: state.tokens.prev,\n            export: state.tokens.next\n          });\n          advance();\n        } else {\n          exportedTokens.push({\n            local: state.tokens.curr,\n            export: state.tokens.curr\n          });\n        }\n\n        if (!checkPunctuator(state.tokens.next, \"}\")) {\n          advance(\",\");\n        }\n      }\n      advance(\"}\");\n      if (state.tokens.next.value === \"from\") {\n        // ExportDeclaration :: export ExportClause FromClause\n        advance(\"from\");\n        moduleSpecifier = state.tokens.next;\n        advance(\"(string)\");\n      } else if (ok) {\n        exportedTokens.forEach(function(x) {\n          state.funct[\"(scope)\"].setExported(x.local, x.export);\n        });\n      }\n\n      if (exportedTokens.length === 0) {\n        if (moduleSpecifier) {\n          warning(\"W142\", this, \"export\", moduleSpecifier.value);\n        } else {\n          warning(\"W141\", this, \"export\");\n        }\n      }\n\n      return this;\n    } else if (state.tokens.next.id === \"var\") {\n      // ExportDeclaration :: export VariableStatement\n      advance(\"var\");\n      token = state.tokens.curr.fud(context);\n      token.first.forEach(function(binding) {\n        state.funct[\"(scope)\"].setExported(binding, binding);\n      });\n    } else if (state.tokens.next.id === \"let\") {\n      // ExportDeclaration :: export VariableStatement\n      advance(\"let\");\n      token = state.tokens.curr.fud(context);\n      token.first.forEach(function(binding) {\n        state.funct[\"(scope)\"].setExported(binding, binding);\n      });\n    } else if (state.tokens.next.id === \"const\") {\n      // ExportDeclaration :: export VariableStatement\n      advance(\"const\");\n      token = state.tokens.curr.fud(context);\n      token.first.forEach(function(binding) {\n        state.funct[\"(scope)\"].setExported(binding, binding);\n      });\n    } else if (state.tokens.next.id === \"function\") {\n      // ExportDeclaration :: export Declaration\n      this.block = true;\n      advance(\"function\");\n      token = state.syntax[\"function\"].fud(context);\n      state.funct[\"(scope)\"].setExported(token.name, token.name);\n    } else if (state.tokens.next.id === \"async\" && peek().id === \"function\") {\n      // ExportDeclaration :: export Declaration\n      this.block = true;\n      advance(\"async\");\n      advance(\"function\");\n      token = state.syntax[\"function\"].fud(context | prodParams.preAsync);\n      state.funct[\"(scope)\"].setExported(token.name, token.name);\n    } else if (state.tokens.next.id === \"class\") {\n      // ExportDeclaration :: export Declaration\n      this.block = true;\n      advance(\"class\");\n      token = state.syntax[\"class\"].fud(context);\n      state.funct[\"(scope)\"].setExported(token.name, token.name);\n    } else {\n      /* istanbul ignore next */\n      error(\"E024\", state.tokens.next, state.tokens.next.value);\n    }\n\n    return this;\n  }).exps = true;\n\n  /**\n   * Determine if SuperCall or SuperProperty may be used in the current context\n   * (as described by the provided \"functor\" object).\n   *\n   * @param {string} type - one of \"property\" or \"call\"\n   * @param {object} funct - a \"functor\" object describing the current function\n   *                         context\n   *\n   * @returns {boolean}\n   */\n  function supportsSuper(type, funct) {\n    if (type === \"call\" && funct[\"(async)\"]) {\n      return false;\n    }\n\n    if (type === \"property\" && funct[\"(method)\"]) {\n      return true;\n    }\n\n    if (type === \"call\" && funct[\"(statement)\"] &&\n      funct[\"(statement)\"].id === \"class\") {\n      return true;\n    }\n\n    if (funct[\"(arrow)\"]) {\n      return supportsSuper(type, funct[\"(context)\"]);\n    }\n\n    return false;\n  }\n\n  var superNud = function() {\n    var next = state.tokens.next;\n\n    if (checkPunctuators(next, [\"[\", \".\"])) {\n      if (!supportsSuper(\"property\", state.funct)) {\n        error(\"E063\", this);\n      }\n    } else if (checkPunctuator(next, \"(\")) {\n      if (!supportsSuper(\"call\", state.funct)) {\n        error(\"E064\", this);\n      }\n    } else {\n      error(\"E024\", next, next.value || next.id);\n    }\n\n    return this;\n  };\n\n  // Future Reserved Words\n\n  FutureReservedWord(\"abstract\");\n  FutureReservedWord(\"boolean\");\n  FutureReservedWord(\"byte\");\n  FutureReservedWord(\"char\");\n  FutureReservedWord(\"double\");\n  FutureReservedWord(\"enum\", { es5: true });\n  FutureReservedWord(\"export\", { es5: true });\n  FutureReservedWord(\"extends\", { es5: true });\n  FutureReservedWord(\"final\");\n  FutureReservedWord(\"float\");\n  FutureReservedWord(\"goto\");\n  FutureReservedWord(\"implements\", { es5: true, strictOnly: true });\n  FutureReservedWord(\"int\");\n  FutureReservedWord(\"interface\", { es5: true, strictOnly: true });\n  FutureReservedWord(\"long\");\n  FutureReservedWord(\"native\");\n  FutureReservedWord(\"package\", { es5: true, strictOnly: true });\n  FutureReservedWord(\"private\", { es5: true, strictOnly: true });\n  FutureReservedWord(\"protected\", { es5: true, strictOnly: true });\n  FutureReservedWord(\"public\", { es5: true, strictOnly: true });\n  FutureReservedWord(\"short\");\n  FutureReservedWord(\"static\", { es5: true, strictOnly: true });\n  FutureReservedWord(\"synchronized\");\n  FutureReservedWord(\"transient\");\n  FutureReservedWord(\"volatile\");\n\n  // this function is used to determine whether a squarebracket or a curlybracket\n  // expression is a comprehension array, destructuring assignment or a json value.\n\n  var lookupBlockType = function() {\n    var pn, pn1, prev;\n    var i = -1;\n    var bracketStack = 0;\n    var ret = {};\n    if (checkPunctuators(state.tokens.curr, [\"[\", \"{\"])) {\n      bracketStack += 1;\n    }\n    do {\n      prev = i === -1 ? state.tokens.curr : pn;\n      pn = i === -1 ? state.tokens.next : peek(i);\n      pn1 = peek(i + 1);\n      i = i + 1;\n      if (checkPunctuators(pn, [\"[\", \"{\"])) {\n        bracketStack += 1;\n      } else if (checkPunctuators(pn, [\"]\", \"}\"])) {\n        bracketStack -= 1;\n      }\n      if (bracketStack === 1 && pn.identifier && pn.value === \"for\" &&\n          !checkPunctuator(prev, \".\")) {\n        ret.isCompArray = true;\n        ret.notJson = true;\n        break;\n      }\n      if (bracketStack === 0 && checkPunctuators(pn, [\"}\", \"]\"])) {\n        if (pn1.value === \"=\") {\n          ret.isDestAssign = true;\n          ret.notJson = true;\n          break;\n        } else if (pn1.value === \".\") {\n          ret.notJson = true;\n          break;\n        }\n      }\n      if (checkPunctuator(pn, \";\")) {\n        ret.notJson = true;\n      }\n    } while (bracketStack > 0 && pn.id !== \"(end)\");\n    return ret;\n  };\n\n  /**\n   * Update an object used to track property names within object initializers\n   * and class bodies. Produce warnings in response to duplicated names.\n   *\n   * @param {object} props - a collection of all properties of the object or\n   *                         class to which the current property is being\n   *                         assigned\n   * @param {string} name - the property name\n   * @param {object} tkn - the token defining the property\n   * @param {boolean} [isClass] - whether the accessor is part of an ES6 Class\n   *                              definition\n   * @param {boolean} [isStatic] - whether the accessor is a static method\n   * @param {boolean} [isComputed] - whether the property is a computed expression like [Symbol.iterator]\n   */\n  function saveProperty(props, name, tkn, isClass, isStatic, isComputed) {\n    if (tkn.identifier) {\n      name = tkn.value;\n    }\n    var key = name;\n    if (isClass && isStatic) {\n      key = \"static \" + name;\n    }\n\n    if (props[key] && name !== \"__proto__\" && !isComputed) {\n      var msg = [\"key\", \"class method\", \"static class method\"];\n      msg = msg[(isClass || false) + (isStatic || false)];\n      warning(\"W075\", state.tokens.next, msg, name);\n    } else {\n      props[key] = Object.create(null);\n    }\n\n    props[key].basic = true;\n    props[key].basictkn = tkn;\n  }\n\n  /**\n   * Update an object used to track property names within object initializers\n   * and class bodies. Produce warnings in response to duplicated names.\n   *\n   * @param {string} accessorType - Either \"get\" or \"set\"\n   * @param {object} props - a collection of all properties of the object or\n   *                         class to which the current accessor is being\n   *                         assigned\n   * @param {object} tkn - the identifier token representing the accessor name\n   * @param {boolean} [isClass] - whether the accessor is part of an ES6 Class\n   *                              definition\n   * @param {boolean} [isStatic] - whether the accessor is a static method\n   */\n  function saveAccessor(accessorType, props, name, tkn, isClass, isStatic) {\n    var flagName = accessorType === \"get\" ? \"getterToken\" : \"setterToken\";\n    var key = name;\n    state.tokens.curr.accessorType = accessorType;\n    state.nameStack.set(tkn);\n    if (isClass && isStatic) {\n      key = \"static \" + name;\n    }\n\n    if (props[key]) {\n      if ((props[key].basic || props[key][flagName]) && name !== \"__proto__\") {\n        var msg = \"\";\n        if (isClass) {\n          if (isStatic) {\n            msg += \"static \";\n          }\n          msg += accessorType + \"ter method\";\n        } else {\n          msg = \"key\";\n        }\n        warning(\"W075\", state.tokens.next, msg, name);\n      }\n    } else {\n      props[key] = Object.create(null);\n    }\n\n    props[key][flagName] = tkn;\n    if (isStatic) {\n      props[key].static = true;\n    }\n  }\n\n  /**\n   * Parse a computed property name within object initializers and class bodies\n   * as introduced by ES2015. For example:\n   *\n   *     void {\n   *       [object.method()]: null\n   *     };\n   *\n   * @param {number} context - the parsing context\n   *\n   * @returns {object} - the token value that describes the expression which\n   *                     defines the property name\n   */\n  function computedPropertyName(context) {\n    advance(\"[\");\n\n    // Explicitly reclassify token as a delimeter to prevent its later\n    // interpretation as an \"infix\" operator.\n    state.tokens.curr.delim = true;\n    state.tokens.curr.lbp = 0;\n\n    if (!state.inES6()) {\n      warning(\"W119\", state.tokens.curr, \"computed property names\", \"6\");\n    }\n    var value = expression(context & ~prodParams.noin, 10);\n    advance(\"]\");\n    return value;\n  }\n\n  /**\n   * Test whether a given token is a punctuator whose `value` property matches\n   * one of the specified values. This function explicitly verifies the token's\n   * `type` property so that like-valued string literals (e.g. `\";\"`) do not\n   * produce false positives.\n   *\n   * @param {Token} token\n   * @param {Array.<string>} values\n   *\n   * @returns {boolean}\n   */\n  function checkPunctuators(token, values) {\n    if (token.type === \"(punctuator)\") {\n      return _.includes(values, token.value);\n    }\n    return false;\n  }\n\n  /**\n   * Test whether a given token is a punctuator whose `value` property matches\n   * the specified value. This function explicitly verifies the token's `type`\n   * property so that like-valued string literals (e.g. `\";\"`) do not produce\n   * false positives.\n   *\n   * @param {Token} token\n   * @param {string} value\n   *\n   * @returns {boolean}\n   */\n  function checkPunctuator(token, value) {\n    return token.type === \"(punctuator)\" && token.value === value;\n  }\n\n  // Check whether this function has been reached for a destructuring assign with undeclared values\n  function destructuringAssignOrJsonValue(context) {\n    // lookup for the assignment (ECMAScript 6 only)\n    // if it has semicolons, it is a block, so go parse it as a block\n    // or it's not a block, but there are assignments, check for undeclared variables\n\n    var block = lookupBlockType();\n    if (block.notJson) {\n      if (!state.inES6() && block.isDestAssign) {\n        /* istanbul ignore next */\n        warning(\"W104\", state.tokens.curr, \"destructuring assignment\", \"6\");\n      }\n      statements(context);\n    // otherwise parse json value\n    } else {\n      state.option.laxbreak = true;\n      state.jsonMode = true;\n      jsonValue();\n    }\n  }\n\n  /**\n   * Parse and define the three states of a list comprehension in order to\n   * avoid defining global variables, but keeping them to the list\n   * comprehension scope only. The order of the states are as follows:\n   *\n   * - \"use\" - which will be the returned iterative part of the list\n   *   comprehension\n   * - \"define\" - which will define the variables local to the list\n   *   comprehension\n   * - \"filter\" - which will help filter out values\n   */\n  var arrayComprehension = function() {\n    var CompArray = function() {\n      this.mode = \"use\";\n      this.variables = [];\n    };\n    var _carrays = [];\n    var _current;\n    function declare(v) {\n      var l = _current.variables.filter(function(elt) {\n        // if it has, change its undef state\n        if (elt.value === v) {\n          elt.undef = false;\n          return v;\n        }\n      }).length;\n      return l !== 0;\n    }\n    function use(v) {\n      var l = _current.variables.filter(function(elt) {\n        // and if it has been defined\n        if (elt.value === v && !elt.undef) {\n          if (elt.unused === true) {\n            elt.unused = false;\n          }\n          return v;\n        }\n      }).length;\n      // otherwise we warn about it\n      return (l === 0);\n    }\n    return { stack: function() {\n          _current = new CompArray();\n          _carrays.push(_current);\n        },\n        unstack: function() {\n          _current.variables.filter(function(v) {\n            if (v.unused)\n              warning(\"W098\", v.token, v.token.raw_text || v.value);\n            if (v.undef)\n              state.funct[\"(scope)\"].block.use(v.value, v.token);\n          });\n          _carrays.splice(-1, 1);\n          _current = _carrays[_carrays.length - 1];\n        },\n        setState: function(s) {\n          if (_.includes([\"use\", \"define\", \"generate\", \"filter\"], s))\n            _current.mode = s;\n        },\n        check: function(v) {\n          if (!_current) {\n            return;\n          }\n          // When we are in \"use\" state of the list comp, we enqueue that var\n          if (_current && _current.mode === \"use\") {\n            if (use(v)) {\n              _current.variables.push({\n                token: state.tokens.curr,\n                value: v,\n                undef: true,\n                unused: false\n              });\n            }\n            return true;\n          // When we are in \"define\" state of the list comp,\n          } else if (_current && _current.mode === \"define\") {\n            // check if the variable has been used previously\n            if (!declare(v)) {\n              _current.variables.push({\n                token: state.tokens.curr,\n                value: v,\n                undef: false,\n                unused: true\n              });\n            }\n            return true;\n          // When we are in the \"generate\" state of the list comp,\n          } else if (_current && _current.mode === \"generate\") {\n            state.funct[\"(scope)\"].block.use(v, state.tokens.curr);\n            return true;\n          // When we are in \"filter\" state,\n          } else if (_current && _current.mode === \"filter\") {\n            // we check whether current variable has been declared\n            if (use(v)) {\n              // if not we warn about it\n              /* istanbul ignore next */\n              state.funct[\"(scope)\"].block.use(v, state.tokens.curr);\n            }\n            return true;\n          }\n          /* istanbul ignore next */\n          return false;\n        }\n        };\n  };\n\n\n  /**\n   * Parse input according to the JSON format.\n   *\n   * http://json.org/\n   */\n  function jsonValue() {\n    function jsonObject() {\n      var o = {}, t = state.tokens.next;\n      advance(\"{\");\n      if (state.tokens.next.id !== \"}\") {\n        for (;;) {\n          if (state.tokens.next.id === \"(end)\") {\n            error(\"E026\", state.tokens.next, t.line);\n          } else if (state.tokens.next.id === \"}\") {\n            warning(\"W094\", state.tokens.curr);\n            break;\n          } else if (state.tokens.next.id === \",\") {\n            error(\"E028\", state.tokens.next);\n          } else if (state.tokens.next.id !== \"(string)\") {\n            warning(\"W095\", state.tokens.next, state.tokens.next.value);\n          }\n          if (o[state.tokens.next.value] === true) {\n            warning(\"W075\", state.tokens.next, \"key\", state.tokens.next.value);\n          } else if ((state.tokens.next.value === \"__proto__\" &&\n            !state.option.proto) || (state.tokens.next.value === \"__iterator__\" &&\n            !state.option.iterator)) {\n            warning(\"W096\", state.tokens.next, state.tokens.next.value);\n          } else {\n            o[state.tokens.next.value] = true;\n          }\n          advance();\n          advance(\":\");\n          jsonValue();\n          if (state.tokens.next.id !== \",\") {\n            break;\n          }\n          advance(\",\");\n        }\n      }\n      advance(\"}\");\n    }\n\n    function jsonArray() {\n      var t = state.tokens.next;\n      advance(\"[\");\n      if (state.tokens.next.id !== \"]\") {\n        for (;;) {\n          if (state.tokens.next.id === \"(end)\") {\n            error(\"E027\", state.tokens.next, t.line);\n          } else if (state.tokens.next.id === \"]\") {\n            warning(\"W094\", state.tokens.curr);\n            break;\n          } else if (state.tokens.next.id === \",\") {\n            error(\"E028\", state.tokens.next);\n          }\n          jsonValue();\n          if (state.tokens.next.id !== \",\") {\n            break;\n          }\n          advance(\",\");\n        }\n      }\n      advance(\"]\");\n    }\n\n    switch (state.tokens.next.id) {\n    case \"{\":\n      jsonObject();\n      break;\n    case \"[\":\n      jsonArray();\n      break;\n    case \"true\":\n    case \"false\":\n    case \"null\":\n    case \"(number)\":\n    case \"(string)\":\n      advance();\n      break;\n    case \"-\":\n      advance(\"-\");\n      advance(\"(number)\");\n      break;\n    default:\n      error(\"E003\", state.tokens.next);\n    }\n  }\n\n  /**\n   * Lint dynamically-evaluated code, appending any resulting errors/warnings\n   * into the global `errors` array.\n   *\n   * @param {array} internals - collection of \"internals\" objects describing\n   *                            string tokens that contain evaluated code\n   * @param {object} options - linting options to apply\n   * @param {object} globals - globally-defined bindings for the evaluated code\n   */\n  function lintEvalCode(internals, options, globals) {\n    var priorErrorCount, idx, jdx, internal;\n\n    for (idx = 0; idx < internals.length; idx += 1) {\n      internal = internals[idx];\n      options.scope = internal.elem;\n      priorErrorCount = JSHINT.errors.length;\n\n      itself(internal.code, options, globals);\n\n      for (jdx = priorErrorCount; jdx < JSHINT.errors.length; jdx += 1) {\n        JSHINT.errors[jdx].line += internal.token.line - 1;\n      }\n    }\n  }\n\n  var escapeRegex = function(str) {\n    return str.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\");\n  };\n\n  // The actual JSHINT function itself.\n  var itself = function(s, o, g) {\n    var x, reIgnoreStr, reIgnore;\n    var optionKeys, newOptionObj, newIgnoredObj;\n\n    o = _.clone(o);\n    state.reset();\n    newOptionObj = state.option;\n    newIgnoredObj = state.ignored;\n\n    if (o && o.scope) {\n      JSHINT.scope = o.scope;\n    } else {\n      JSHINT.errors = [];\n      JSHINT.internals = [];\n      JSHINT.blacklist = {};\n      JSHINT.scope = \"(main)\";\n    }\n\n    predefined = Object.create(null);\n    combine(predefined, vars.ecmaIdentifiers[3]);\n    combine(predefined, vars.reservedVars);\n\n    declared = Object.create(null);\n    var exported = Object.create(null); // Variables that live outside the current file\n\n    function each(obj, cb) {\n      if (!obj)\n        return;\n\n      if (!Array.isArray(obj) && typeof obj === \"object\")\n        obj = Object.keys(obj);\n\n      obj.forEach(cb);\n    }\n\n    if (o) {\n\n      each([o.predef, o.globals], function(dict) {\n        each(dict, function(item) {\n          var slice, prop;\n\n          if (item[0] === \"-\") {\n            slice = item.slice(1);\n            JSHINT.blacklist[slice] = slice;\n            // remove from predefined if there\n            delete predefined[slice];\n          } else {\n            prop = Object.getOwnPropertyDescriptor(dict, item);\n            predefined[item] = prop ? prop.value : false;\n          }\n        });\n      });\n\n      each(o.exported || null, function(item) {\n        exported[item] = true;\n      });\n\n      delete o.predef;\n      delete o.exported;\n\n      optionKeys = Object.keys(o);\n      for (x = 0; x < optionKeys.length; x++) {\n        if (/^-W\\d{3}$/g.test(optionKeys[x])) {\n          newIgnoredObj[optionKeys[x].slice(1)] = true;\n        } else {\n          var optionKey = optionKeys[x];\n          newOptionObj[optionKey] = o[optionKey];\n        }\n      }\n    }\n\n    state.option = newOptionObj;\n    state.ignored = newIgnoredObj;\n\n    state.option.indent = state.option.indent || 4;\n    state.option.maxerr = state.option.maxerr || 50;\n\n    indent = 1;\n\n    var scopeManagerInst = scopeManager(state, predefined, exported, declared);\n    scopeManagerInst.on(\"warning\", function(ev) {\n      warning.apply(null, [ ev.code, ev.token].concat(ev.data));\n    });\n\n    scopeManagerInst.on(\"error\", function(ev) {\n      /* istanbul ignore next */\n      error.apply(null, [ ev.code, ev.token ].concat(ev.data));\n    });\n\n    state.funct = functor(\"(global)\", null, {\n      \"(global)\"    : true,\n      \"(scope)\"     : scopeManagerInst,\n      \"(comparray)\" : arrayComprehension(),\n      \"(metrics)\"   : createMetrics(state.tokens.next)\n    });\n\n    functions = [state.funct];\n    member = {};\n    membersOnly = null;\n    inblock = false;\n    lookahead = [];\n\n    if (!isString(s) && !Array.isArray(s)) {\n      errorAt(\"E004\", 0);\n      return false;\n    }\n\n    api = {\n      get isJSON() {\n        /* istanbul ignore next */\n        return state.jsonMode;\n      },\n\n      getOption: function(name) {\n        return state.option[name] || null;\n      },\n\n      getCache: function(name) {\n        return state.cache[name];\n      },\n\n      setCache: function(name, value) {\n        state.cache[name] = value;\n      },\n\n      warn: function(code, data) {\n        warningAt.apply(null, [ code, data.line, data.char ].concat(data.data));\n      },\n\n      on: function(names, listener) {\n        names.split(\" \").forEach(function(name) {\n          emitter.on(name, listener);\n        }.bind(this));\n      }\n    };\n\n    emitter.removeAllListeners();\n    (extraModules || []).forEach(function(func) {\n      func(api);\n    });\n\n    state.tokens.prev = state.tokens.curr = state.tokens.next = state.syntax[\"(begin)\"];\n    if (o && o.ignoreDelimiters) {\n\n      if (!Array.isArray(o.ignoreDelimiters)) {\n        /* istanbul ignore next */\n        o.ignoreDelimiters = [o.ignoreDelimiters];\n      }\n\n      o.ignoreDelimiters.forEach(function(delimiterPair) {\n        if (!delimiterPair.start || !delimiterPair.end)\n            return;\n\n        reIgnoreStr = escapeRegex(delimiterPair.start) +\n                      \"[\\\\s\\\\S]*?\" +\n                      escapeRegex(delimiterPair.end);\n\n        reIgnore = new RegExp(reIgnoreStr, \"ig\");\n\n        s = s.replace(reIgnore, function(match) {\n          return match.replace(/./g, \" \");\n        });\n      });\n    }\n\n    lex = new Lexer(s);\n\n    lex.on(\"warning\", function(ev) {\n      warningAt.apply(null, [ ev.code, ev.line, ev.character].concat(ev.data));\n    });\n\n    lex.on(\"error\", function(ev) {\n      errorAt.apply(null, [ ev.code, ev.line, ev.character ].concat(ev.data));\n    });\n\n    lex.on(\"fatal\", function(ev) {\n      quit(\"E041\", ev);\n    });\n\n    lex.on(\"Identifier\", function(ev) {\n      emitter.emit(\"Identifier\", ev);\n    });\n\n    lex.on(\"String\", function(ev) {\n      emitter.emit(\"String\", ev);\n    });\n\n    lex.on(\"Number\", function(ev) {\n      emitter.emit(\"Number\", ev);\n    });\n\n    // Check options\n    var name;\n    for (name in o) {\n      if (_.has(o, name)) {\n        checkOption(name, true, state.tokens.curr);\n      }\n    }\n    if (o) {\n      for (name in o.unstable) {\n        if (_.has(o.unstable, name)) {\n          checkOption(name, false, state.tokens.curr);\n        }\n      }\n    }\n\n    try {\n      applyOptions();\n\n      // combine the passed globals after we've assumed all our options\n      combine(predefined, g || {});\n\n      //reset values\n      checkComma.first = true;\n\n      advance();\n      switch (state.tokens.next.id) {\n      case \"{\":\n      case \"[\":\n        destructuringAssignOrJsonValue(0);\n        break;\n      default:\n        directives();\n\n        if (state.directive[\"use strict\"]) {\n          if (!state.allowsGlobalUsd()) {\n            warning(\"W097\", state.directive[\"use strict\"]);\n          }\n        }\n\n        statements(0);\n      }\n\n      if (state.tokens.next.id !== \"(end)\") {\n        quit(\"E041\", state.tokens.curr);\n      }\n\n      state.funct[\"(scope)\"].unstack();\n\n    } catch (err) {\n      if (err && err.name === \"JSHintError\") {\n        var nt = state.tokens.next || {};\n        JSHINT.errors.push({\n          scope     : \"(main)\",\n          raw       : err.raw,\n          code      : err.code,\n          reason    : err.reason,\n          line      : err.line || nt.line,\n          character : err.character || nt.from\n        });\n      } else {\n        /* istanbul ignore next */\n        throw err;\n      }\n    }\n\n    // Loop over the listed \"internals\", and check them as well.\n    if (JSHINT.scope === \"(main)\") {\n      lintEvalCode(JSHINT.internals, o || {}, g);\n    }\n\n    return JSHINT.errors.length === 0;\n  };\n\n  // Modules.\n  itself.addModule = function(func) {\n    extraModules.push(func);\n  };\n\n  itself.addModule(style.register);\n\n  // Data summary.\n  itself.data = function() {\n    var data = {\n      functions: [],\n      options: state.option\n    };\n\n    var fu, f, i, n, globals;\n\n    if (itself.errors.length) {\n      data.errors = itself.errors;\n    }\n\n    if (state.jsonMode) {\n      /* istanbul ignore next */\n      data.json = true;\n    }\n\n    var impliedGlobals = state.funct[\"(scope)\"].getImpliedGlobals();\n    if (impliedGlobals.length > 0) {\n      data.implieds = impliedGlobals;\n    }\n\n    globals = state.funct[\"(scope)\"].getUsedOrDefinedGlobals();\n    if (globals.length > 0) {\n      data.globals = globals;\n    }\n\n    for (i = 1; i < functions.length; i += 1) {\n      f = functions[i];\n      fu = {};\n\n      fu.name = f[\"(name)\"];\n      fu.param = f[\"(params)\"];\n      fu.line = f[\"(line)\"];\n      fu.character = f[\"(character)\"];\n      fu.last = f[\"(last)\"];\n      fu.lastcharacter = f[\"(lastcharacter)\"];\n\n      fu.metrics = {\n        complexity: f[\"(metrics)\"].ComplexityCount,\n        parameters: f[\"(metrics)\"].arity,\n        statements: f[\"(metrics)\"].statementCount\n      };\n\n      data.functions.push(fu);\n    }\n\n    var unuseds = state.funct[\"(scope)\"].getUnuseds();\n    if (unuseds.length > 0) {\n      data.unused = unuseds;\n    }\n\n    for (n in member) {\n      if (typeof member[n] === \"number\") {\n        data.member = member;\n        break;\n      }\n    }\n\n    return data;\n  };\n\n  itself.jshint = itself;\n\n  return itself;\n}());\n\n// Make JSHINT a Node module, if possible.\nif (typeof exports === \"object\" && exports) {\n  exports.JSHINT = JSHINT;\n}\n\n},{\"./lex.js\":17,\"./messages.js\":18,\"./options.js\":20,\"./prod-params.js\":21,\"./reg.js\":22,\"./scope-manager.js\":23,\"./state.js\":24,\"./style.js\":25,\"./vars.js\":27,\"console-browserify\":9,\"events\":11,\"lodash\":12}]},{},[]);\n\nJSHINT = require('jshint').JSHINT;\nif (typeof exports === 'object' && exports) exports.JSHINT = JSHINT;\n}());"
  },
  {
    "path": "web/assets/codemirror/jsonlint.js",
    "content": "var jsonlint=function(){var a=!0,b=!1,c={},d=function(){var a={trace:function(){},yy:{},symbols_:{error:2,JSONString:3,STRING:4,JSONNumber:5,NUMBER:6,JSONNullLiteral:7,NULL:8,JSONBooleanLiteral:9,TRUE:10,FALSE:11,JSONText:12,JSONValue:13,EOF:14,JSONObject:15,JSONArray:16,\"{\":17,\"}\":18,JSONMemberList:19,JSONMember:20,\":\":21,\",\":22,\"[\":23,\"]\":24,JSONElementList:25,$accept:0,$end:1},terminals_:{2:\"error\",4:\"STRING\",6:\"NUMBER\",8:\"NULL\",10:\"TRUE\",11:\"FALSE\",14:\"EOF\",17:\"{\",18:\"}\",21:\":\",22:\",\",23:\"[\",24:\"]\"},productions_:[0,[3,1],[5,1],[7,1],[9,1],[9,1],[12,2],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[15,2],[15,3],[20,3],[19,1],[19,3],[16,2],[16,3],[25,1],[25,3]],performAction:function(b,c,d,e,f,g,h){var i=g.length-1;switch(f){case 1:this.$=b.replace(/\\\\(\\\\|\")/g,\"$1\").replace(/\\\\n/g,\"\\n\").replace(/\\\\r/g,\"\\r\").replace(/\\\\t/g,\"\t\").replace(/\\\\v/g,\"\u000b\").replace(/\\\\f/g,\"\\f\").replace(/\\\\b/g,\"\\b\");break;case 2:this.$=Number(b);break;case 3:this.$=null;break;case 4:this.$=!0;break;case 5:this.$=!1;break;case 6:return this.$=g[i-1];case 13:this.$={};break;case 14:this.$=g[i-1];break;case 15:this.$=[g[i-2],g[i]];break;case 16:this.$={},this.$[g[i][0]]=g[i][1];break;case 17:this.$=g[i-2],g[i-2][g[i][0]]=g[i][1];break;case 18:this.$=[];break;case 19:this.$=g[i-1];break;case 20:this.$=[g[i]];break;case 21:this.$=g[i-2],g[i-2].push(g[i])}},table:[{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],12:1,13:2,15:7,16:8,17:[1,14],23:[1,15]},{1:[3]},{14:[1,16]},{14:[2,7],18:[2,7],22:[2,7],24:[2,7]},{14:[2,8],18:[2,8],22:[2,8],24:[2,8]},{14:[2,9],18:[2,9],22:[2,9],24:[2,9]},{14:[2,10],18:[2,10],22:[2,10],24:[2,10]},{14:[2,11],18:[2,11],22:[2,11],24:[2,11]},{14:[2,12],18:[2,12],22:[2,12],24:[2,12]},{14:[2,3],18:[2,3],22:[2,3],24:[2,3]},{14:[2,4],18:[2,4],22:[2,4],24:[2,4]},{14:[2,5],18:[2,5],22:[2,5],24:[2,5]},{14:[2,1],18:[2,1],21:[2,1],22:[2,1],24:[2,1]},{14:[2,2],18:[2,2],22:[2,2],24:[2,2]},{3:20,4:[1,12],18:[1,17],19:18,20:19},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:23,15:7,16:8,17:[1,14],23:[1,15],24:[1,21],25:22},{1:[2,6]},{14:[2,13],18:[2,13],22:[2,13],24:[2,13]},{18:[1,24],22:[1,25]},{18:[2,16],22:[2,16]},{21:[1,26]},{14:[2,18],18:[2,18],22:[2,18],24:[2,18]},{22:[1,28],24:[1,27]},{22:[2,20],24:[2,20]},{14:[2,14],18:[2,14],22:[2,14],24:[2,14]},{3:20,4:[1,12],20:29},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:30,15:7,16:8,17:[1,14],23:[1,15]},{14:[2,19],18:[2,19],22:[2,19],24:[2,19]},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:31,15:7,16:8,17:[1,14],23:[1,15]},{18:[2,17],22:[2,17]},{18:[2,15],22:[2,15]},{22:[2,21],24:[2,21]}],defaultActions:{16:[2,6]},parseError:function(b,c){throw new Error(b)},parse:function(b){function o(a){d.length=d.length-2*a,e.length=e.length-a,f.length=f.length-a}function p(){var a;return a=c.lexer.lex()||1,typeof a!=\"number\"&&(a=c.symbols_[a]||a),a}var c=this,d=[0],e=[null],f=[],g=this.table,h=\"\",i=0,j=0,k=0,l=2,m=1;this.lexer.setInput(b),this.lexer.yy=this.yy,this.yy.lexer=this.lexer,typeof this.lexer.yylloc==\"undefined\"&&(this.lexer.yylloc={});var n=this.lexer.yylloc;f.push(n),typeof this.yy.parseError==\"function\"&&(this.parseError=this.yy.parseError);var q,r,s,t,u,v,w={},x,y,z,A;for(;;){s=d[d.length-1],this.defaultActions[s]?t=this.defaultActions[s]:(q==null&&(q=p()),t=g[s]&&g[s][q]);if(typeof t==\"undefined\"||!t.length||!t[0]){if(!k){A=[];for(x in g[s])this.terminals_[x]&&x>2&&A.push(\"'\"+this.terminals_[x]+\"'\");var B=\"\";this.lexer.showPosition?B=\"Parse error on line \"+(i+1)+\":\\n\"+this.lexer.showPosition()+\"\\nExpecting \"+A.join(\", \")+\", got '\"+this.terminals_[q]+\"'\":B=\"Parse error on line \"+(i+1)+\": Unexpected \"+(q==1?\"end of input\":\"'\"+(this.terminals_[q]||q)+\"'\"),this.parseError(B,{text:this.lexer.match,token:this.terminals_[q]||q,line:this.lexer.yylineno,loc:n,expected:A})}if(k==3){if(q==m)throw new Error(B||\"Parsing halted.\");j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,n=this.lexer.yylloc,q=p()}for(;;){if(l.toString()in g[s])break;if(s==0)throw new Error(B||\"Parsing halted.\");o(1),s=d[d.length-1]}r=q,q=l,s=d[d.length-1],t=g[s]&&g[s][l],k=3}if(t[0]instanceof Array&&t.length>1)throw new Error(\"Parse Error: multiple actions possible at state: \"+s+\", token: \"+q);switch(t[0]){case 1:d.push(q),e.push(this.lexer.yytext),f.push(this.lexer.yylloc),d.push(t[1]),q=null,r?(q=r,r=null):(j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,n=this.lexer.yylloc,k>0&&k--);break;case 2:y=this.productions_[t[1]][1],w.$=e[e.length-y],w._$={first_line:f[f.length-(y||1)].first_line,last_line:f[f.length-1].last_line,first_column:f[f.length-(y||1)].first_column,last_column:f[f.length-1].last_column},v=this.performAction.call(w,h,j,i,this.yy,t[1],e,f);if(typeof v!=\"undefined\")return v;y&&(d=d.slice(0,-1*y*2),e=e.slice(0,-1*y),f=f.slice(0,-1*y)),d.push(this.productions_[t[1]][0]),e.push(w.$),f.push(w._$),z=g[d[d.length-2]][d[d.length-1]],d.push(z);break;case 3:return!0}}return!0}},b=function(){var a={EOF:1,parseError:function(b,c){if(!this.yy.parseError)throw new Error(b);this.yy.parseError(b,c)},setInput:function(a){return this._input=a,this._more=this._less=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match=\"\",this.conditionStack=[\"INITIAL\"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this},input:function(){var a=this._input[0];this.yytext+=a,this.yyleng++,this.match+=a,this.matched+=a;var b=a.match(/\\n/);return b&&this.yylineno++,this._input=this._input.slice(1),a},unput:function(a){return this._input=a+this._input,this},more:function(){return this._more=!0,this},less:function(a){this._input=this.match.slice(a)+this._input},pastInput:function(){var a=this.matched.substr(0,this.matched.length-this.match.length);return(a.length>20?\"...\":\"\")+a.substr(-20).replace(/\\n/g,\"\")},upcomingInput:function(){var a=this.match;return a.length<20&&(a+=this._input.substr(0,20-a.length)),(a.substr(0,20)+(a.length>20?\"...\":\"\")).replace(/\\n/g,\"\")},showPosition:function(){var a=this.pastInput(),b=(new Array(a.length+1)).join(\"-\");return a+this.upcomingInput()+\"\\n\"+b+\"^\"},next:function(){if(this.done)return this.EOF;this._input||(this.done=!0);var a,b,c,d,e,f;this._more||(this.yytext=\"\",this.match=\"\");var g=this._currentRules();for(var h=0;h<g.length;h++){c=this._input.match(this.rules[g[h]]);if(c&&(!b||c[0].length>b[0].length)){b=c,d=h;if(!this.options.flex)break}}if(b){f=b[0].match(/\\n.*/g),f&&(this.yylineno+=f.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:f?f[f.length-1].length-1:this.yylloc.last_column+b[0].length},this.yytext+=b[0],this.match+=b[0],this.yyleng=this.yytext.length,this._more=!1,this._input=this._input.slice(b[0].length),this.matched+=b[0],a=this.performAction.call(this,this.yy,this,g[d],this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1);if(a)return a;return}if(this._input===\"\")return this.EOF;this.parseError(\"Lexical error on line \"+(this.yylineno+1)+\". Unrecognized text.\\n\"+this.showPosition(),{text:\"\",token:null,line:this.yylineno})},lex:function(){var b=this.next();return typeof b!=\"undefined\"?b:this.lex()},begin:function(b){this.conditionStack.push(b)},popState:function(){return this.conditionStack.pop()},_currentRules:function(){return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules},topState:function(){return this.conditionStack[this.conditionStack.length-2]},pushState:function(b){this.begin(b)}};return a.options={},a.performAction=function(b,c,d,e){var f=e;switch(d){case 0:break;case 1:return 6;case 2:return c.yytext=c.yytext.substr(1,c.yyleng-2),4;case 3:return 17;case 4:return 18;case 5:return 23;case 6:return 24;case 7:return 22;case 8:return 21;case 9:return 10;case 10:return 11;case 11:return 8;case 12:return 14;case 13:return\"INVALID\"}},a.rules=[/^(?:\\s+)/,/^(?:(-?([0-9]|[1-9][0-9]+))(\\.[0-9]+)?([eE][-+]?[0-9]+)?\\b)/,/^(?:\"(?:\\\\[\\\\\"bfnrt/]|\\\\u[a-fA-F0-9]{4}|[^\\\\\\0-\\x09\\x0a-\\x1f\"])*\")/,/^(?:\\{)/,/^(?:\\})/,/^(?:\\[)/,/^(?:\\])/,/^(?:,)/,/^(?::)/,/^(?:true\\b)/,/^(?:false\\b)/,/^(?:null\\b)/,/^(?:$)/,/^(?:.)/],a.conditions={INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,10,11,12,13],inclusive:!0}},a}();return a.lexer=b,a}();return typeof a!=\"undefined\"&&typeof c!=\"undefined\"&&(c.parser=d,c.parse=function(){return d.parse.apply(d,arguments)},c.main=function(d){if(!d[1])throw new Error(\"Usage: \"+d[0]+\" FILE\");if(typeof process!=\"undefined\")var e=a(\"fs\").readFileSync(a(\"path\").join(process.cwd(),d[1]),\"utf8\");else var f=a(\"file\").path(a(\"file\").cwd()),e=f.join(d[1]).read({charset:\"utf-8\"});return c.parser.parse(e)},typeof b!=\"undefined\"&&a.main===b&&c.main(typeof process!=\"undefined\"?process.argv.slice(1):a(\"system\").args)),c}();"
  },
  {
    "path": "web/assets/codemirror/lint/javascript-lint.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: https://codemirror.net/5/LICENSE\n\n// Depends on jshint.js from https://github.com/jshint/jshint\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n  // declare global: JSHINT\n\n  function validator(text, options) {\n    if (!window.JSHINT) {\n      if (window.console) {\n        window.console.error(\"Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run.\");\n      }\n      return [];\n    }\n    if (!options.indent) // JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation\n      options.indent = 1; // JSHint default value is 4\n    JSHINT(text, options, options.globals);\n    var errors = JSHINT.data().errors, result = [];\n    if (errors) parseErrors(errors, result);\n    return result;\n  }\n\n  CodeMirror.registerHelper(\"lint\", \"javascript\", validator);\n\n  function parseErrors(errors, output) {\n    for ( var i = 0; i < errors.length; i++) {\n      var error = errors[i];\n      if (error) {\n        if (error.line <= 0) {\n          if (window.console) {\n            window.console.warn(\"Cannot display JSHint error (invalid line \" + error.line + \")\", error);\n          }\n          continue;\n        }\n\n        var start = error.character - 1, end = start + 1;\n        if (error.evidence) {\n          var index = error.evidence.substring(start).search(/.\\b/);\n          if (index > -1) {\n            end += index;\n          }\n        }\n\n        // Convert to format expected by validation service\n        var hint = {\n          message: error.reason,\n          severity: error.code ? (error.code.startsWith('W') ? \"warning\" : \"error\") : \"error\",\n          from: CodeMirror.Pos(error.line - 1, start),\n          to: CodeMirror.Pos(error.line - 1, end)\n        };\n\n        output.push(hint);\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "web/assets/codemirror/lint/lint.css",
    "content": "/* The lint marker gutter */\n.CodeMirror-lint-markers {\n  width: 16px;\n}\n\n.CodeMirror-lint-tooltip {\n  background-color: #ffd;\n  border: 1px solid black;\n  border-radius: 4px;\n  color: black;\n  font-family: monospace;\n  font-size: 10pt;\n  overflow: hidden;\n  padding: 2px 5px;\n  position: fixed;\n  white-space: pre;\n  white-space: pre-wrap;\n  z-index: 100;\n  max-width: 600px;\n  opacity: 0;\n  transition: opacity .4s;\n  -moz-transition: opacity .4s;\n  -webkit-transition: opacity .4s;\n  -o-transition: opacity .4s;\n  -ms-transition: opacity .4s;\n}\n\n.CodeMirror-lint-mark {\n  background-position: left bottom;\n  background-repeat: repeat-x;\n}\n\n.CodeMirror-lint-mark-warning {\n  background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=\");\n}\n\n.CodeMirror-lint-mark-error {\n  background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==\");\n}\n\n.CodeMirror-lint-marker {\n  background-position: center center;\n  background-repeat: no-repeat;\n  cursor: pointer;\n  display: inline-block;\n  height: 16px;\n  width: 16px;\n  vertical-align: middle;\n  position: relative;\n}\n\n.CodeMirror-lint-message {\n  padding-left: 18px;\n  background-position: top left;\n  background-repeat: no-repeat;\n}\n\n.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {\n  background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=\");\n}\n\n.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {\n  background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=\");\n}\n\n.CodeMirror-lint-marker-multiple {\n  background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC\");\n  background-repeat: no-repeat;\n  background-position: right bottom;\n  width: 100%; height: 100%;\n}\n\n.CodeMirror-lint-line-error {\n  background-color: rgba(183, 76, 81, 0.08);\n}\n\n.CodeMirror-lint-line-warning {\n  background-color: rgba(255, 211, 0, 0.1);\n}\n"
  },
  {
    "path": "web/assets/codemirror/lint/lint.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: https://codemirror.net/5/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n  var GUTTER_ID = \"CodeMirror-lint-markers\";\n  var LINT_LINE_ID = \"CodeMirror-lint-line-\";\n\n  function showTooltip(cm, e, content) {\n    var tt = document.createElement(\"div\");\n    tt.className = \"CodeMirror-lint-tooltip cm-s-\" + cm.options.theme;\n    tt.appendChild(content.cloneNode(true));\n    if (cm.state.lint.options.selfContain)\n      cm.getWrapperElement().appendChild(tt);\n    else\n      document.body.appendChild(tt);\n\n    function position(e) {\n      if (!tt.parentNode) return CodeMirror.off(document, \"mousemove\", position);\n      var top = Math.max(0, e.clientY - tt.offsetHeight - 5);\n      var left = Math.max(0, Math.min(e.clientX + 5, tt.ownerDocument.defaultView.innerWidth - tt.offsetWidth));\n      tt.style.top = top + \"px\"\n      tt.style.left = left + \"px\";\n    }\n    CodeMirror.on(document, \"mousemove\", position);\n    position(e);\n    if (tt.style.opacity != null) tt.style.opacity = 1;\n    return tt;\n  }\n  function rm(elt) {\n    if (elt.parentNode) elt.parentNode.removeChild(elt);\n  }\n  function hideTooltip(tt) {\n    if (!tt.parentNode) return;\n    if (tt.style.opacity == null) rm(tt);\n    tt.style.opacity = 0;\n    setTimeout(function() { rm(tt); }, 600);\n  }\n\n  function showTooltipFor(cm, e, content, node) {\n    var tooltip = showTooltip(cm, e, content);\n    function hide() {\n      CodeMirror.off(node, \"mouseout\", hide);\n      if (tooltip) { hideTooltip(tooltip); tooltip = null; }\n    }\n    var poll = setInterval(function() {\n      if (tooltip) for (var n = node;; n = n.parentNode) {\n        if (n && n.nodeType == 11) n = n.host;\n        if (n == document.body) return;\n        if (!n) { hide(); break; }\n      }\n      if (!tooltip) return clearInterval(poll);\n    }, 400);\n    CodeMirror.on(node, \"mouseout\", hide);\n  }\n\n  function LintState(cm, conf, hasGutter) {\n    this.marked = [];\n    if (conf instanceof Function) conf = {getAnnotations: conf};\n    if (!conf || conf === true) conf = {};\n    this.options = {};\n    this.linterOptions = conf.options || {};\n    for (var prop in defaults) this.options[prop] = defaults[prop];\n    for (var prop in conf) {\n      if (defaults.hasOwnProperty(prop)) {\n        if (conf[prop] != null) this.options[prop] = conf[prop];\n      } else if (!conf.options) {\n        this.linterOptions[prop] = conf[prop];\n      }\n    }\n    this.timeout = null;\n    this.hasGutter = hasGutter;\n    this.onMouseOver = function(e) { onMouseOver(cm, e); };\n    this.waitingFor = 0\n  }\n\n  var defaults = {\n    highlightLines: false,\n    tooltips: true,\n    delay: 500,\n    lintOnChange: true,\n    getAnnotations: null,\n    async: false,\n    selfContain: null,\n    formatAnnotation: null,\n    onUpdateLinting: null\n  }\n\n  function clearMarks(cm) {\n    var state = cm.state.lint;\n    if (state.hasGutter) cm.clearGutter(GUTTER_ID);\n    if (state.options.highlightLines) clearErrorLines(cm);\n    for (var i = 0; i < state.marked.length; ++i)\n      state.marked[i].clear();\n    state.marked.length = 0;\n  }\n\n  function clearErrorLines(cm) {\n    cm.eachLine(function(line) {\n      var has = line.wrapClass && /\\bCodeMirror-lint-line-\\w+\\b/.exec(line.wrapClass);\n      if (has) cm.removeLineClass(line, \"wrap\", has[0]);\n    })\n  }\n\n  function makeMarker(cm, labels, severity, multiple, tooltips) {\n    var marker = document.createElement(\"div\"), inner = marker;\n    marker.className = \"CodeMirror-lint-marker CodeMirror-lint-marker-\" + severity;\n    if (multiple) {\n      inner = marker.appendChild(document.createElement(\"div\"));\n      inner.className = \"CodeMirror-lint-marker CodeMirror-lint-marker-multiple\";\n    }\n\n    if (tooltips != false) CodeMirror.on(inner, \"mouseover\", function(e) {\n      showTooltipFor(cm, e, labels, inner);\n    });\n\n    return marker;\n  }\n\n  function getMaxSeverity(a, b) {\n    if (a == \"error\") return a;\n    else return b;\n  }\n\n  function groupByLine(annotations) {\n    var lines = [];\n    for (var i = 0; i < annotations.length; ++i) {\n      var ann = annotations[i], line = ann.from.line;\n      (lines[line] || (lines[line] = [])).push(ann);\n    }\n    return lines;\n  }\n\n  function annotationTooltip(ann) {\n    var severity = ann.severity;\n    if (!severity) severity = \"error\";\n    var tip = document.createElement(\"div\");\n    tip.className = \"CodeMirror-lint-message CodeMirror-lint-message-\" + severity;\n    if (typeof ann.messageHTML != 'undefined') {\n      tip.innerHTML = ann.messageHTML;\n    } else {\n      tip.appendChild(document.createTextNode(ann.message));\n    }\n    return tip;\n  }\n\n  function lintAsync(cm, getAnnotations) {\n    var state = cm.state.lint\n    var id = ++state.waitingFor\n    function abort() {\n      id = -1\n      cm.off(\"change\", abort)\n    }\n    cm.on(\"change\", abort)\n    getAnnotations(cm.getValue(), function(annotations, arg2) {\n      cm.off(\"change\", abort)\n      if (state.waitingFor != id) return\n      if (arg2 && annotations instanceof CodeMirror) annotations = arg2\n      cm.operation(function() {updateLinting(cm, annotations)})\n    }, state.linterOptions, cm);\n  }\n\n  function startLinting(cm) {\n    var state = cm.state.lint;\n    if (!state) return;\n    var options = state.options;\n    /*\n     * Passing rules in `options` property prevents JSHint (and other linters) from complaining\n     * about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.\n     */\n    var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), \"lint\");\n    if (!getAnnotations) return;\n    if (options.async || getAnnotations.async) {\n      lintAsync(cm, getAnnotations)\n    } else {\n      var annotations = getAnnotations(cm.getValue(), state.linterOptions, cm);\n      if (!annotations) return;\n      if (annotations.then) annotations.then(function(issues) {\n        cm.operation(function() {updateLinting(cm, issues)})\n      });\n      else cm.operation(function() {updateLinting(cm, annotations)})\n    }\n  }\n\n  function updateLinting(cm, annotationsNotSorted) {\n    var state = cm.state.lint;\n    if (!state) return;\n    var options = state.options;\n    clearMarks(cm);\n\n    var annotations = groupByLine(annotationsNotSorted);\n\n    for (var line = 0; line < annotations.length; ++line) {\n      var anns = annotations[line];\n      if (!anns) continue;\n\n      var maxSeverity = null;\n      var tipLabel = state.hasGutter && document.createDocumentFragment();\n\n      for (var i = 0; i < anns.length; ++i) {\n        var ann = anns[i];\n        var severity = ann.severity;\n        if (!severity) severity = \"error\";\n        maxSeverity = getMaxSeverity(maxSeverity, severity);\n\n        if (options.formatAnnotation) ann = options.formatAnnotation(ann);\n        if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));\n\n        if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {\n          className: \"CodeMirror-lint-mark CodeMirror-lint-mark-\" + severity,\n          __annotation: ann\n        }));\n      }\n      if (state.hasGutter)\n        cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, anns.length > 1,\n                                                       options.tooltips));\n\n      if (options.highlightLines)\n        cm.addLineClass(line, \"wrap\", LINT_LINE_ID + maxSeverity);\n    }\n    if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);\n  }\n\n  function onChange(cm) {\n    var state = cm.state.lint;\n    if (!state) return;\n    clearTimeout(state.timeout);\n    state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay);\n  }\n\n  function popupTooltips(cm, annotations, e) {\n    var target = e.target || e.srcElement;\n    var tooltip = document.createDocumentFragment();\n    for (var i = 0; i < annotations.length; i++) {\n      var ann = annotations[i];\n      tooltip.appendChild(annotationTooltip(ann));\n    }\n    showTooltipFor(cm, e, tooltip, target);\n  }\n\n  function onMouseOver(cm, e) {\n    var target = e.target || e.srcElement;\n    if (!/\\bCodeMirror-lint-mark-/.test(target.className)) return;\n    var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;\n    var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, \"client\"));\n\n    var annotations = [];\n    for (var i = 0; i < spans.length; ++i) {\n      var ann = spans[i].__annotation;\n      if (ann) annotations.push(ann);\n    }\n    if (annotations.length) popupTooltips(cm, annotations, e);\n  }\n\n  CodeMirror.defineOption(\"lint\", false, function(cm, val, old) {\n    if (old && old != CodeMirror.Init) {\n      clearMarks(cm);\n      if (cm.state.lint.options.lintOnChange !== false)\n        cm.off(\"change\", onChange);\n      CodeMirror.off(cm.getWrapperElement(), \"mouseover\", cm.state.lint.onMouseOver);\n      clearTimeout(cm.state.lint.timeout);\n      delete cm.state.lint;\n    }\n\n    if (val) {\n      var gutters = cm.getOption(\"gutters\"), hasLintGutter = false;\n      for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;\n      var state = cm.state.lint = new LintState(cm, val, hasLintGutter);\n      if (state.options.lintOnChange)\n        cm.on(\"change\", onChange);\n      if (state.options.tooltips != false && state.options.tooltips != \"gutter\")\n        CodeMirror.on(cm.getWrapperElement(), \"mouseover\", state.onMouseOver);\n\n      startLinting(cm);\n    }\n  });\n\n  CodeMirror.defineExtension(\"performLint\", function() {\n    startLinting(this);\n  });\n});\n"
  },
  {
    "path": "web/assets/js/axios-init.js",
    "content": "axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';\naxios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';\n\naxios.interceptors.request.use(\n    (config) => {\n        if (config.data instanceof FormData) {\n            config.headers['Content-Type'] = 'multipart/form-data';\n        } else {\n            config.data = Qs.stringify(config.data, {\n                arrayFormat: 'repeat',\n            });\n        }\n        return config;\n    },\n    (error) => Promise.reject(error),\n);\n\naxios.interceptors.response.use(\n    (response) => response,\n    (error) => {\n        if (error.response) {\n            const statusCode = error.response.status;\n            // Check the status code\n            if (statusCode === 401) { // Unauthorized\n                return window.location.reload();\n            }\n        }\n        return Promise.reject(error);\n    }\n);\n"
  },
  {
    "path": "web/assets/js/model/dbinbound.js",
    "content": "class DBInbound {\n\n    constructor(data) {\n        this.id = 0;\n        this.userId = 0;\n        this.up = 0;\n        this.down = 0;\n        this.total = 0;\n        this.allTime = 0;\n        this.remark = \"\";\n        this.enable = true;\n        this.expiryTime = 0;\n        this.trafficReset = \"never\";\n        this.lastTrafficResetTime = 0;\n\n        this.listen = \"\";\n        this.port = 0;\n        this.protocol = \"\";\n        this.settings = \"\";\n        this.streamSettings = \"\";\n        this.tag = \"\";\n        this.sniffing = \"\";\n        this.clientStats = \"\"\n        if (data == null) {\n            return;\n        }\n        ObjectUtil.cloneProps(this, data);\n    }\n\n    get totalGB() {\n        return NumberFormatter.toFixed(this.total / SizeFormatter.ONE_GB, 2);\n    }\n\n    set totalGB(gb) {\n        this.total = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);\n    }\n\n    get isVMess() {\n        return this.protocol === Protocols.VMESS;\n    }\n\n    get isVLess() {\n        return this.protocol === Protocols.VLESS;\n    }\n\n    get isTrojan() {\n        return this.protocol === Protocols.TROJAN;\n    }\n\n    get isSS() {\n        return this.protocol === Protocols.SHADOWSOCKS;\n    }\n\n    get isMixed() {\n        return this.protocol === Protocols.MIXED;\n    }\n\n    get isHTTP() {\n        return this.protocol === Protocols.HTTP;\n    }\n\n    get isWireguard() {\n        return this.protocol === Protocols.WIREGUARD;\n    }\n\n    get address() {\n        let address = location.hostname;\n        if (!ObjectUtil.isEmpty(this.listen) && this.listen !== \"0.0.0.0\") {\n            address = this.listen;\n        }\n        return address;\n    }\n\n    get _expiryTime() {\n        if (this.expiryTime === 0) {\n            return null;\n        }\n        return moment(this.expiryTime);\n    }\n\n    set _expiryTime(t) {\n        if (t == null) {\n            this.expiryTime = 0;\n        } else {\n            this.expiryTime = t.valueOf();\n        }\n    }\n\n    get isExpiry() {\n        return this.expiryTime < new Date().getTime();\n    }\n\n    toInbound() {\n        let settings = {};\n        if (!ObjectUtil.isEmpty(this.settings)) {\n            settings = JSON.parse(this.settings);\n        }\n\n        let streamSettings = {};\n        if (!ObjectUtil.isEmpty(this.streamSettings)) {\n            streamSettings = JSON.parse(this.streamSettings);\n        }\n\n        let sniffing = {};\n        if (!ObjectUtil.isEmpty(this.sniffing)) {\n            sniffing = JSON.parse(this.sniffing);\n        }\n\n        const config = {\n            port: this.port,\n            listen: this.listen,\n            protocol: this.protocol,\n            settings: settings,\n            streamSettings: streamSettings,\n            tag: this.tag,\n            sniffing: sniffing,\n            clientStats: this.clientStats,\n        };\n        return Inbound.fromJson(config);\n    }\n\n    isMultiUser() {\n        switch (this.protocol) {\n            case Protocols.VMESS:\n            case Protocols.VLESS:\n            case Protocols.TROJAN:\n                return true;\n            case Protocols.SHADOWSOCKS:\n                return this.toInbound().isSSMultiUser;\n            default:\n                return false;\n        }\n    }\n\n    hasLink() {\n        switch (this.protocol) {\n            case Protocols.VMESS:\n            case Protocols.VLESS:\n            case Protocols.TROJAN:\n            case Protocols.SHADOWSOCKS:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    genInboundLinks(remarkModel) {\n        const inbound = this.toInbound();\n        return inbound.genInboundLinks(this.remark, remarkModel);\n    }\n}"
  },
  {
    "path": "web/assets/js/model/inbound.js",
    "content": "const Protocols = {\n    VMESS: 'vmess',\n    VLESS: 'vless',\n    TROJAN: 'trojan',\n    SHADOWSOCKS: 'shadowsocks',\n    TUNNEL: 'tunnel',\n    MIXED: 'mixed',\n    HTTP: 'http',\n    WIREGUARD: 'wireguard',\n    TUN: 'tun',\n};\n\nconst SSMethods = {\n    AES_256_GCM: 'aes-256-gcm',\n    CHACHA20_POLY1305: 'chacha20-poly1305',\n    CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',\n    XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',\n    BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',\n    BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',\n    BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',\n};\n\nconst TLS_FLOW_CONTROL = {\n    VISION: \"xtls-rprx-vision\",\n    VISION_UDP443: \"xtls-rprx-vision-udp443\",\n};\n\nconst TLS_VERSION_OPTION = {\n    TLS10: \"1.0\",\n    TLS11: \"1.1\",\n    TLS12: \"1.2\",\n    TLS13: \"1.3\",\n};\n\nconst TLS_CIPHER_OPTION = {\n    AES_128_GCM: \"TLS_AES_128_GCM_SHA256\",\n    AES_256_GCM: \"TLS_AES_256_GCM_SHA384\",\n    CHACHA20_POLY1305: \"TLS_CHACHA20_POLY1305_SHA256\",\n    ECDHE_ECDSA_AES_128_CBC: \"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\",\n    ECDHE_ECDSA_AES_256_CBC: \"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\",\n    ECDHE_RSA_AES_128_CBC: \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\",\n    ECDHE_RSA_AES_256_CBC: \"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\",\n    ECDHE_ECDSA_AES_128_GCM: \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\",\n    ECDHE_ECDSA_AES_256_GCM: \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\",\n    ECDHE_RSA_AES_128_GCM: \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n    ECDHE_RSA_AES_256_GCM: \"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\",\n    ECDHE_ECDSA_CHACHA20_POLY1305: \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",\n    ECDHE_RSA_CHACHA20_POLY1305: \"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256\",\n};\n\nconst UTLS_FINGERPRINT = {\n    UTLS_CHROME: \"chrome\",\n    UTLS_FIREFOX: \"firefox\",\n    UTLS_SAFARI: \"safari\",\n    UTLS_IOS: \"ios\",\n    UTLS_android: \"android\",\n    UTLS_EDGE: \"edge\",\n    UTLS_360: \"360\",\n    UTLS_QQ: \"qq\",\n    UTLS_RANDOM: \"random\",\n    UTLS_RANDOMIZED: \"randomized\",\n    UTLS_RONDOMIZEDNOALPN: \"randomizednoalpn\",\n    UTLS_UNSAFE: \"unsafe\",\n};\n\nconst ALPN_OPTION = {\n    H3: \"h3\",\n    H2: \"h2\",\n    HTTP1: \"http/1.1\",\n};\n\nconst SNIFFING_OPTION = {\n    HTTP: \"http\",\n    TLS: \"tls\",\n    QUIC: \"quic\",\n    FAKEDNS: \"fakedns\"\n};\n\nconst USAGE_OPTION = {\n    ENCIPHERMENT: \"encipherment\",\n    VERIFY: \"verify\",\n    ISSUE: \"issue\",\n};\n\nconst DOMAIN_STRATEGY_OPTION = {\n    AS_IS: \"AsIs\",\n    USE_IP: \"UseIP\",\n    USE_IPV6V4: \"UseIPv6v4\",\n    USE_IPV6: \"UseIPv6\",\n    USE_IPV4V6: \"UseIPv4v6\",\n    USE_IPV4: \"UseIPv4\",\n    FORCE_IP: \"ForceIP\",\n    FORCE_IPV6V4: \"ForceIPv6v4\",\n    FORCE_IPV6: \"ForceIPv6\",\n    FORCE_IPV4V6: \"ForceIPv4v6\",\n    FORCE_IPV4: \"ForceIPv4\",\n};\n\nconst TCP_CONGESTION_OPTION = {\n    BBR: \"bbr\",\n    CUBIC: \"cubic\",\n    RENO: \"reno\",\n};\n\nconst USERS_SECURITY = {\n    AES_128_GCM: \"aes-128-gcm\",\n    CHACHA20_POLY1305: \"chacha20-poly1305\",\n    AUTO: \"auto\",\n    NONE: \"none\",\n    ZERO: \"zero\",\n};\n\nconst MODE_OPTION = {\n    AUTO: \"auto\",\n    PACKET_UP: \"packet-up\",\n    STREAM_UP: \"stream-up\",\n    STREAM_ONE: \"stream-one\",\n};\n\nObject.freeze(Protocols);\nObject.freeze(SSMethods);\nObject.freeze(TLS_FLOW_CONTROL);\nObject.freeze(TLS_VERSION_OPTION);\nObject.freeze(TLS_CIPHER_OPTION);\nObject.freeze(UTLS_FINGERPRINT);\nObject.freeze(ALPN_OPTION);\nObject.freeze(SNIFFING_OPTION);\nObject.freeze(USAGE_OPTION);\nObject.freeze(DOMAIN_STRATEGY_OPTION);\nObject.freeze(TCP_CONGESTION_OPTION);\nObject.freeze(USERS_SECURITY);\nObject.freeze(MODE_OPTION);\n\nclass XrayCommonClass {\n\n    static toJsonArray(arr) {\n        return arr.map(obj => obj.toJson());\n    }\n\n    static fromJson() {\n        return new XrayCommonClass();\n    }\n\n    toJson() {\n        return this;\n    }\n\n    toString(format = true) {\n        return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());\n    }\n\n    static toHeaders(v2Headers) {\n        let newHeaders = [];\n        if (v2Headers) {\n            Object.keys(v2Headers).forEach(key => {\n                let values = v2Headers[key];\n                if (typeof (values) === 'string') {\n                    newHeaders.push({ name: key, value: values });\n                } else {\n                    for (let i = 0; i < values.length; ++i) {\n                        newHeaders.push({ name: key, value: values[i] });\n                    }\n                }\n            });\n        }\n        return newHeaders;\n    }\n\n    static toV2Headers(headers, arr = true) {\n        let v2Headers = {};\n        for (let i = 0; i < headers.length; ++i) {\n            let name = headers[i].name;\n            let value = headers[i].value;\n            if (ObjectUtil.isEmpty(name) || ObjectUtil.isEmpty(value)) {\n                continue;\n            }\n            if (!(name in v2Headers)) {\n                v2Headers[name] = arr ? [value] : value;\n            } else {\n                if (arr) {\n                    v2Headers[name].push(value);\n                } else {\n                    v2Headers[name] = value;\n                }\n            }\n        }\n        return v2Headers;\n    }\n}\n\nclass TcpStreamSettings extends XrayCommonClass {\n    constructor(\n        acceptProxyProtocol = false,\n        type = 'none',\n        request = new TcpStreamSettings.TcpRequest(),\n        response = new TcpStreamSettings.TcpResponse(),\n    ) {\n        super();\n        this.acceptProxyProtocol = acceptProxyProtocol;\n        this.type = type;\n        this.request = request;\n        this.response = response;\n    }\n\n    static fromJson(json = {}) {\n        let header = json.header;\n        if (!header) {\n            header = {};\n        }\n        return new TcpStreamSettings(json.acceptProxyProtocol,\n            header.type,\n            TcpStreamSettings.TcpRequest.fromJson(header.request),\n            TcpStreamSettings.TcpResponse.fromJson(header.response),\n        );\n    }\n\n    toJson() {\n        return {\n            acceptProxyProtocol: this.acceptProxyProtocol,\n            header: {\n                type: this.type,\n                request: this.type === 'http' ? this.request.toJson() : undefined,\n                response: this.type === 'http' ? this.response.toJson() : undefined,\n            },\n        };\n    }\n}\n\nTcpStreamSettings.TcpRequest = class extends XrayCommonClass {\n    constructor(\n        version = '1.1',\n        method = 'GET',\n        path = ['/'],\n        headers = [],\n    ) {\n        super();\n        this.version = version;\n        this.method = method;\n        this.path = path.length === 0 ? ['/'] : path;\n        this.headers = headers;\n    }\n\n    addPath(path) {\n        this.path.push(path);\n    }\n\n    removePath(index) {\n        this.path.splice(index, 1);\n    }\n\n    addHeader(name, value) {\n        this.headers.push({ name: name, value: value });\n    }\n\n    removeHeader(index) {\n        this.headers.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new TcpStreamSettings.TcpRequest(\n            json.version,\n            json.method,\n            json.path,\n            XrayCommonClass.toHeaders(json.headers),\n        );\n    }\n\n    toJson() {\n        return {\n            version: this.version,\n            method: this.method,\n            path: ObjectUtil.clone(this.path),\n            headers: XrayCommonClass.toV2Headers(this.headers),\n        };\n    }\n};\n\nTcpStreamSettings.TcpResponse = class extends XrayCommonClass {\n    constructor(\n        version = '1.1',\n        status = '200',\n        reason = 'OK',\n        headers = [],\n    ) {\n        super();\n        this.version = version;\n        this.status = status;\n        this.reason = reason;\n        this.headers = headers;\n    }\n\n    addHeader(name, value) {\n        this.headers.push({ name: name, value: value });\n    }\n\n    removeHeader(index) {\n        this.headers.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new TcpStreamSettings.TcpResponse(\n            json.version,\n            json.status,\n            json.reason,\n            XrayCommonClass.toHeaders(json.headers),\n        );\n    }\n\n    toJson() {\n        return {\n            version: this.version,\n            status: this.status,\n            reason: this.reason,\n            headers: XrayCommonClass.toV2Headers(this.headers),\n        };\n    }\n};\n\nclass KcpStreamSettings extends XrayCommonClass {\n    constructor(\n        mtu = 1350,\n        tti = 20,\n        uplinkCapacity = 5,\n        downlinkCapacity = 20,\n        congestion = false,\n        readBufferSize = 1,\n        writeBufferSize = 1,\n    ) {\n        super();\n        this.mtu = mtu;\n        this.tti = tti;\n        this.upCap = uplinkCapacity;\n        this.downCap = downlinkCapacity;\n        this.congestion = congestion;\n        this.readBuffer = readBufferSize;\n        this.writeBuffer = writeBufferSize;\n    }\n\n    static fromJson(json = {}) {\n        return new KcpStreamSettings(\n            json.mtu,\n            json.tti,\n            json.uplinkCapacity,\n            json.downlinkCapacity,\n            json.congestion,\n            json.readBufferSize,\n            json.writeBufferSize,\n        );\n    }\n\n    toJson() {\n        return {\n            mtu: this.mtu,\n            tti: this.tti,\n            uplinkCapacity: this.upCap,\n            downlinkCapacity: this.downCap,\n            congestion: this.congestion,\n            readBufferSize: this.readBuffer,\n            writeBufferSize: this.writeBuffer,\n        };\n    }\n}\n\nclass WsStreamSettings extends XrayCommonClass {\n    constructor(\n        acceptProxyProtocol = false,\n        path = '/',\n        host = '',\n        headers = [],\n        heartbeatPeriod = 0,\n    ) {\n        super();\n        this.acceptProxyProtocol = acceptProxyProtocol;\n        this.path = path;\n        this.host = host;\n        this.headers = headers;\n        this.heartbeatPeriod = heartbeatPeriod;\n    }\n\n    addHeader(name, value) {\n        this.headers.push({ name: name, value: value });\n    }\n\n    removeHeader(index) {\n        this.headers.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new WsStreamSettings(\n            json.acceptProxyProtocol,\n            json.path,\n            json.host,\n            XrayCommonClass.toHeaders(json.headers),\n            json.heartbeatPeriod,\n        );\n    }\n\n    toJson() {\n        return {\n            acceptProxyProtocol: this.acceptProxyProtocol,\n            path: this.path,\n            host: this.host,\n            headers: XrayCommonClass.toV2Headers(this.headers, false),\n            heartbeatPeriod: this.heartbeatPeriod,\n        };\n    }\n}\n\nclass GrpcStreamSettings extends XrayCommonClass {\n    constructor(\n        serviceName = \"\",\n        authority = \"\",\n        multiMode = false,\n    ) {\n        super();\n        this.serviceName = serviceName;\n        this.authority = authority;\n        this.multiMode = multiMode;\n    }\n\n    static fromJson(json = {}) {\n        return new GrpcStreamSettings(\n            json.serviceName,\n            json.authority,\n            json.multiMode\n        );\n    }\n\n    toJson() {\n        return {\n            serviceName: this.serviceName,\n            authority: this.authority,\n            multiMode: this.multiMode,\n        }\n    }\n}\n\nclass HTTPUpgradeStreamSettings extends XrayCommonClass {\n    constructor(\n        acceptProxyProtocol = false,\n        path = '/',\n        host = '',\n        headers = []\n    ) {\n        super();\n        this.acceptProxyProtocol = acceptProxyProtocol;\n        this.path = path;\n        this.host = host;\n        this.headers = headers;\n    }\n\n    addHeader(name, value) {\n        this.headers.push({ name: name, value: value });\n    }\n\n    removeHeader(index) {\n        this.headers.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new HTTPUpgradeStreamSettings(\n            json.acceptProxyProtocol,\n            json.path,\n            json.host,\n            XrayCommonClass.toHeaders(json.headers),\n        );\n    }\n\n    toJson() {\n        return {\n            acceptProxyProtocol: this.acceptProxyProtocol,\n            path: this.path,\n            host: this.host,\n            headers: XrayCommonClass.toV2Headers(this.headers, false),\n        };\n    }\n}\n\nclass xHTTPStreamSettings extends XrayCommonClass {\n    constructor(\n        path = '/',\n        host = '',\n        headers = [],\n        scMaxBufferedPosts = 30,\n        scMaxEachPostBytes = \"1000000\",\n        scStreamUpServerSecs = \"20-80\",\n        noSSEHeader = false,\n        xPaddingBytes = \"100-1000\",\n        mode = MODE_OPTION.AUTO,\n        xPaddingObfsMode = false,\n        xPaddingKey = '',\n        xPaddingHeader = '',\n        xPaddingPlacement = '',\n        xPaddingMethod = '',\n        uplinkHTTPMethod = '',\n        sessionPlacement = '',\n        sessionKey = '',\n        seqPlacement = '',\n        seqKey = '',\n        uplinkDataPlacement = '',\n        uplinkDataKey = '',\n        uplinkChunkSize = 0,\n    ) {\n        super();\n        this.path = path;\n        this.host = host;\n        this.headers = headers;\n        this.scMaxBufferedPosts = scMaxBufferedPosts;\n        this.scMaxEachPostBytes = scMaxEachPostBytes;\n        this.scStreamUpServerSecs = scStreamUpServerSecs;\n        this.noSSEHeader = noSSEHeader;\n        this.xPaddingBytes = xPaddingBytes;\n        this.mode = mode;\n        this.xPaddingObfsMode = xPaddingObfsMode;\n        this.xPaddingKey = xPaddingKey;\n        this.xPaddingHeader = xPaddingHeader;\n        this.xPaddingPlacement = xPaddingPlacement;\n        this.xPaddingMethod = xPaddingMethod;\n        this.uplinkHTTPMethod = uplinkHTTPMethod;\n        this.sessionPlacement = sessionPlacement;\n        this.sessionKey = sessionKey;\n        this.seqPlacement = seqPlacement;\n        this.seqKey = seqKey;\n        this.uplinkDataPlacement = uplinkDataPlacement;\n        this.uplinkDataKey = uplinkDataKey;\n        this.uplinkChunkSize = uplinkChunkSize;\n    }\n\n    addHeader(name, value) {\n        this.headers.push({ name: name, value: value });\n    }\n\n    removeHeader(index) {\n        this.headers.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new xHTTPStreamSettings(\n            json.path,\n            json.host,\n            XrayCommonClass.toHeaders(json.headers),\n            json.scMaxBufferedPosts,\n            json.scMaxEachPostBytes,\n            json.scStreamUpServerSecs,\n            json.noSSEHeader,\n            json.xPaddingBytes,\n            json.mode,\n            json.xPaddingObfsMode,\n            json.xPaddingKey,\n            json.xPaddingHeader,\n            json.xPaddingPlacement,\n            json.xPaddingMethod,\n            json.uplinkHTTPMethod,\n            json.sessionPlacement,\n            json.sessionKey,\n            json.seqPlacement,\n            json.seqKey,\n            json.uplinkDataPlacement,\n            json.uplinkDataKey,\n            json.uplinkChunkSize,\n        );\n    }\n\n    toJson() {\n        return {\n            path: this.path,\n            host: this.host,\n            headers: XrayCommonClass.toV2Headers(this.headers, false),\n            scMaxBufferedPosts: this.scMaxBufferedPosts,\n            scMaxEachPostBytes: this.scMaxEachPostBytes,\n            scStreamUpServerSecs: this.scStreamUpServerSecs,\n            noSSEHeader: this.noSSEHeader,\n            xPaddingBytes: this.xPaddingBytes,\n            mode: this.mode,\n            xPaddingObfsMode: this.xPaddingObfsMode,\n            xPaddingKey: this.xPaddingKey,\n            xPaddingHeader: this.xPaddingHeader,\n            xPaddingPlacement: this.xPaddingPlacement,\n            xPaddingMethod: this.xPaddingMethod,\n            uplinkHTTPMethod: this.uplinkHTTPMethod,\n            sessionPlacement: this.sessionPlacement,\n            sessionKey: this.sessionKey,\n            seqPlacement: this.seqPlacement,\n            seqKey: this.seqKey,\n            uplinkDataPlacement: this.uplinkDataPlacement,\n            uplinkDataKey: this.uplinkDataKey,\n            uplinkChunkSize: this.uplinkChunkSize,\n        };\n    }\n}\n\nclass TlsStreamSettings extends XrayCommonClass {\n    constructor(\n        serverName = '',\n        minVersion = TLS_VERSION_OPTION.TLS12,\n        maxVersion = TLS_VERSION_OPTION.TLS13,\n        cipherSuites = '',\n        rejectUnknownSni = false,\n        disableSystemRoot = false,\n        enableSessionResumption = false,\n        certificates = [new TlsStreamSettings.Cert()],\n        alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],\n        echServerKeys = '',\n        echForceQuery = 'none',\n        settings = new TlsStreamSettings.Settings()\n    ) {\n        super();\n        this.sni = serverName;\n        this.minVersion = minVersion;\n        this.maxVersion = maxVersion;\n        this.cipherSuites = cipherSuites;\n        this.rejectUnknownSni = rejectUnknownSni;\n        this.disableSystemRoot = disableSystemRoot;\n        this.enableSessionResumption = enableSessionResumption;\n        this.certs = certificates;\n        this.alpn = alpn;\n        this.echServerKeys = echServerKeys;\n        this.echForceQuery = echForceQuery;\n        this.settings = settings;\n    }\n\n    addCert() {\n        this.certs.push(new TlsStreamSettings.Cert());\n    }\n\n    removeCert(index) {\n        this.certs.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        let certs;\n        let settings;\n        if (!ObjectUtil.isEmpty(json.certificates)) {\n            certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));\n        }\n\n        if (!ObjectUtil.isEmpty(json.settings)) {\n            settings = new TlsStreamSettings.Settings(json.settings.fingerprint, json.settings.echConfigList);\n        }\n        return new TlsStreamSettings(\n            json.serverName,\n            json.minVersion,\n            json.maxVersion,\n            json.cipherSuites,\n            json.rejectUnknownSni,\n            json.disableSystemRoot,\n            json.enableSessionResumption,\n            certs,\n            json.alpn,\n            json.echServerKeys,\n            json.echForceQuery,\n            settings,\n        );\n    }\n\n    toJson() {\n        return {\n            serverName: this.sni,\n            minVersion: this.minVersion,\n            maxVersion: this.maxVersion,\n            cipherSuites: this.cipherSuites,\n            rejectUnknownSni: this.rejectUnknownSni,\n            disableSystemRoot: this.disableSystemRoot,\n            enableSessionResumption: this.enableSessionResumption,\n            certificates: TlsStreamSettings.toJsonArray(this.certs),\n            alpn: this.alpn,\n            echServerKeys: this.echServerKeys,\n            echForceQuery: this.echForceQuery,\n            settings: this.settings,\n        };\n    }\n}\n\nTlsStreamSettings.Cert = class extends XrayCommonClass {\n    constructor(\n        useFile = true,\n        certificateFile = '',\n        keyFile = '',\n        certificate = '',\n        key = '',\n        oneTimeLoading = false,\n        usage = USAGE_OPTION.ENCIPHERMENT,\n        buildChain = false,\n    ) {\n        super();\n        this.useFile = useFile;\n        this.certFile = certificateFile;\n        this.keyFile = keyFile;\n        this.cert = Array.isArray(certificate) ? certificate.join('\\n') : certificate;\n        this.key = Array.isArray(key) ? key.join('\\n') : key;\n        this.oneTimeLoading = oneTimeLoading;\n        this.usage = usage;\n        this.buildChain = buildChain\n    }\n\n    static fromJson(json = {}) {\n        if ('certificateFile' in json && 'keyFile' in json) {\n            return new TlsStreamSettings.Cert(\n                true,\n                json.certificateFile,\n                json.keyFile, '', '',\n                json.oneTimeLoading,\n                json.usage,\n                json.buildChain,\n            );\n        } else {\n            return new TlsStreamSettings.Cert(\n                false, '', '',\n                json.certificate.join('\\n'),\n                json.key.join('\\n'),\n                json.oneTimeLoading,\n                json.usage,\n                json.buildChain,\n            );\n        }\n    }\n\n    toJson() {\n        if (this.useFile) {\n            return {\n                certificateFile: this.certFile,\n                keyFile: this.keyFile,\n                oneTimeLoading: this.oneTimeLoading,\n                usage: this.usage,\n                buildChain: this.buildChain,\n            };\n        } else {\n            return {\n                certificate: this.cert.split('\\n'),\n                key: this.key.split('\\n'),\n                oneTimeLoading: this.oneTimeLoading,\n                usage: this.usage,\n                buildChain: this.buildChain,\n            };\n        }\n    }\n};\n\nTlsStreamSettings.Settings = class extends XrayCommonClass {\n    constructor(\n        fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,\n        echConfigList = '',\n    ) {\n        super();\n        this.fingerprint = fingerprint;\n        this.echConfigList = echConfigList;\n    }\n    static fromJson(json = {}) {\n        return new TlsStreamSettings.Settings(\n            json.fingerprint,\n            json.echConfigList,\n        );\n    }\n    toJson() {\n        return {\n            fingerprint: this.fingerprint,\n            echConfigList: this.echConfigList\n        };\n    }\n};\n\n\nclass RealityStreamSettings extends XrayCommonClass {\n    constructor(\n        show = false,\n        xver = 0,\n        target = '',\n        serverNames = '',\n        privateKey = '',\n        minClientVer = '',\n        maxClientVer = '',\n        maxTimediff = 0,\n        shortIds = RandomUtil.randomShortIds(),\n        mldsa65Seed = '',\n        settings = new RealityStreamSettings.Settings()\n    ) {\n        super();\n        // If target/serverNames are not provided, use random values\n        if (!target && !serverNames) {\n            const randomTarget = typeof getRandomRealityTarget !== 'undefined'\n                ? getRandomRealityTarget()\n                : { target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' };\n            target = randomTarget.target;\n            serverNames = randomTarget.sni;\n        }\n        this.show = show;\n        this.xver = xver;\n        this.target = target;\n        this.serverNames = Array.isArray(serverNames) ? serverNames.join(\",\") : serverNames;\n        this.privateKey = privateKey;\n        this.minClientVer = minClientVer;\n        this.maxClientVer = maxClientVer;\n        this.maxTimediff = maxTimediff;\n        this.shortIds = Array.isArray(shortIds) ? shortIds.join(\",\") : shortIds;\n        this.mldsa65Seed = mldsa65Seed;\n        this.settings = settings;\n    }\n\n    static fromJson(json = {}) {\n        let settings;\n        if (!ObjectUtil.isEmpty(json.settings)) {\n            settings = new RealityStreamSettings.Settings(\n                json.settings.publicKey,\n                json.settings.fingerprint,\n                json.settings.serverName,\n                json.settings.spiderX,\n                json.settings.mldsa65Verify,\n            );\n        }\n        return new RealityStreamSettings(\n            json.show,\n            json.xver,\n            json.target,\n            json.serverNames,\n            json.privateKey,\n            json.minClientVer,\n            json.maxClientVer,\n            json.maxTimediff,\n            json.shortIds,\n            json.mldsa65Seed,\n            settings,\n        );\n    }\n\n    toJson() {\n        return {\n            show: this.show,\n            xver: this.xver,\n            target: this.target,\n            serverNames: this.serverNames.split(\",\"),\n            privateKey: this.privateKey,\n            minClientVer: this.minClientVer,\n            maxClientVer: this.maxClientVer,\n            maxTimediff: this.maxTimediff,\n            shortIds: this.shortIds.split(\",\"),\n            mldsa65Seed: this.mldsa65Seed,\n            settings: this.settings,\n        };\n    }\n}\n\nRealityStreamSettings.Settings = class extends XrayCommonClass {\n    constructor(\n        publicKey = '',\n        fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,\n        serverName = '',\n        spiderX = '/',\n        mldsa65Verify = ''\n    ) {\n        super();\n        this.publicKey = publicKey;\n        this.fingerprint = fingerprint;\n        this.serverName = serverName;\n        this.spiderX = spiderX;\n        this.mldsa65Verify = mldsa65Verify;\n    }\n    static fromJson(json = {}) {\n        return new RealityStreamSettings.Settings(\n            json.publicKey,\n            json.fingerprint,\n            json.serverName,\n            json.spiderX,\n            json.mldsa65Verify\n        );\n    }\n    toJson() {\n        return {\n            publicKey: this.publicKey,\n            fingerprint: this.fingerprint,\n            serverName: this.serverName,\n            spiderX: this.spiderX,\n            mldsa65Verify: this.mldsa65Verify\n        };\n    }\n};\n\nclass SockoptStreamSettings extends XrayCommonClass {\n    constructor(\n        acceptProxyProtocol = false,\n        tcpFastOpen = false,\n        mark = 0,\n        tproxy = \"off\",\n        tcpMptcp = false,\n        penetrate = false,\n        domainStrategy = DOMAIN_STRATEGY_OPTION.USE_IP,\n        tcpMaxSeg = 1440,\n        dialerProxy = \"\",\n        tcpKeepAliveInterval = 0,\n        tcpKeepAliveIdle = 300,\n        tcpUserTimeout = 10000,\n        tcpcongestion = TCP_CONGESTION_OPTION.BBR,\n        V6Only = false,\n        tcpWindowClamp = 600,\n        interfaceName = \"\",\n        trustedXForwardedFor = [],\n    ) {\n        super();\n        this.acceptProxyProtocol = acceptProxyProtocol;\n        this.tcpFastOpen = tcpFastOpen;\n        this.mark = mark;\n        this.tproxy = tproxy;\n        this.tcpMptcp = tcpMptcp;\n        this.penetrate = penetrate;\n        this.domainStrategy = domainStrategy;\n        this.tcpMaxSeg = tcpMaxSeg;\n        this.dialerProxy = dialerProxy;\n        this.tcpKeepAliveInterval = tcpKeepAliveInterval;\n        this.tcpKeepAliveIdle = tcpKeepAliveIdle;\n        this.tcpUserTimeout = tcpUserTimeout;\n        this.tcpcongestion = tcpcongestion;\n        this.V6Only = V6Only;\n        this.tcpWindowClamp = tcpWindowClamp;\n        this.interfaceName = interfaceName;\n        this.trustedXForwardedFor = trustedXForwardedFor;\n    }\n\n    static fromJson(json = {}) {\n        if (Object.keys(json).length === 0) return undefined;\n        return new SockoptStreamSettings(\n            json.acceptProxyProtocol,\n            json.tcpFastOpen,\n            json.mark,\n            json.tproxy,\n            json.tcpMptcp,\n            json.penetrate,\n            json.domainStrategy,\n            json.tcpMaxSeg,\n            json.dialerProxy,\n            json.tcpKeepAliveInterval,\n            json.tcpKeepAliveIdle,\n            json.tcpUserTimeout,\n            json.tcpcongestion,\n            json.V6Only,\n            json.tcpWindowClamp,\n            json.interface,\n            json.trustedXForwardedFor || [],\n        );\n    }\n\n    toJson() {\n        const result = {\n            acceptProxyProtocol: this.acceptProxyProtocol,\n            tcpFastOpen: this.tcpFastOpen,\n            mark: this.mark,\n            tproxy: this.tproxy,\n            tcpMptcp: this.tcpMptcp,\n            penetrate: this.penetrate,\n            domainStrategy: this.domainStrategy,\n            tcpMaxSeg: this.tcpMaxSeg,\n            dialerProxy: this.dialerProxy,\n            tcpKeepAliveInterval: this.tcpKeepAliveInterval,\n            tcpKeepAliveIdle: this.tcpKeepAliveIdle,\n            tcpUserTimeout: this.tcpUserTimeout,\n            tcpcongestion: this.tcpcongestion,\n            V6Only: this.V6Only,\n            tcpWindowClamp: this.tcpWindowClamp,\n            interface: this.interfaceName,\n        };\n        if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {\n            result.trustedXForwardedFor = this.trustedXForwardedFor;\n        }\n        return result;\n    }\n}\n\nclass UdpMask extends XrayCommonClass {\n    constructor(type = 'salamander', settings = {}) {\n        super();\n        this.type = type;\n        this.settings = this._getDefaultSettings(type, settings);\n    }\n\n    _getDefaultSettings(type, settings = {}) {\n        switch (type) {\n            case 'salamander':\n            case 'mkcp-aes128gcm':\n                return { password: settings.password || '' };\n            case 'header-dns':\n            case 'xdns':\n                return { domain: settings.domain || '' };\n            case 'xicmp':\n                return { ip: settings.ip || '', id: settings.id ?? 0 };\n            case 'mkcp-original':\n            case 'header-dtls':\n            case 'header-srtp':\n            case 'header-utp':\n            case 'header-wechat':\n            case 'header-wireguard':\n                return {};\n            default:\n                return settings;\n        }\n    }\n\n    static fromJson(json = {}) {\n        return new UdpMask(\n            json.type || 'salamander',\n            json.settings || {}\n        );\n    }\n\n    toJson() {\n        return {\n            type: this.type,\n            settings: (this.settings && Object.keys(this.settings).length > 0) ? this.settings : undefined\n        };\n    }\n}\n\nclass FinalMaskStreamSettings extends XrayCommonClass {\n    constructor(udp = []) {\n        super();\n        this.udp = Array.isArray(udp) ? udp.map(u => new UdpMask(u.type, u.settings)) : [new UdpMask(udp.type, udp.settings)];\n    }\n\n    static fromJson(json = {}) {\n        return new FinalMaskStreamSettings(json.udp || []);\n    }\n\n    toJson() {\n        return {\n            udp: this.udp.map(udp => udp.toJson())\n        };\n\n    }\n}\n\nclass StreamSettings extends XrayCommonClass {\n    constructor(network = 'tcp',\n        security = 'none',\n        externalProxy = [],\n        tlsSettings = new TlsStreamSettings(),\n        realitySettings = new RealityStreamSettings(),\n        tcpSettings = new TcpStreamSettings(),\n        kcpSettings = new KcpStreamSettings(),\n        wsSettings = new WsStreamSettings(),\n        grpcSettings = new GrpcStreamSettings(),\n        httpupgradeSettings = new HTTPUpgradeStreamSettings(),\n        xhttpSettings = new xHTTPStreamSettings(),\n        finalmask = new FinalMaskStreamSettings(),\n        sockopt = undefined,\n    ) {\n        super();\n        this.network = network;\n        this.security = security;\n        this.externalProxy = externalProxy;\n        this.tls = tlsSettings;\n        this.reality = realitySettings;\n        this.tcp = tcpSettings;\n        this.kcp = kcpSettings;\n        this.ws = wsSettings;\n        this.grpc = grpcSettings;\n        this.httpupgrade = httpupgradeSettings;\n        this.xhttp = xhttpSettings;\n        this.finalmask = finalmask;\n        this.sockopt = sockopt;\n    }\n\n    addUdpMask(type = 'salamander') {\n        this.finalmask.udp.push(new UdpMask(type));\n    }\n\n    delUdpMask(index) {\n        if (this.finalmask.udp) {\n            this.finalmask.udp.splice(index, 1);\n        }\n    }\n\n    get hasFinalMask() {\n        return this.finalmask.udp && this.finalmask.udp.length > 0;\n    }\n\n    get isTls() {\n        return this.security === \"tls\";\n    }\n\n    set isTls(isTls) {\n        if (isTls) {\n            this.security = 'tls';\n        } else {\n            this.security = 'none';\n        }\n    }\n\n    //for Reality\n    get isReality() {\n        return this.security === \"reality\";\n    }\n\n    set isReality(isReality) {\n        if (isReality) {\n            this.security = 'reality';\n        } else {\n            this.security = 'none';\n        }\n    }\n\n    get sockoptSwitch() {\n        return this.sockopt != undefined;\n    }\n\n    set sockoptSwitch(value) {\n        this.sockopt = value ? new SockoptStreamSettings() : undefined;\n    }\n\n    static fromJson(json = {}) {\n        return new StreamSettings(\n            json.network,\n            json.security,\n            json.externalProxy,\n            TlsStreamSettings.fromJson(json.tlsSettings),\n            RealityStreamSettings.fromJson(json.realitySettings),\n            TcpStreamSettings.fromJson(json.tcpSettings),\n            KcpStreamSettings.fromJson(json.kcpSettings),\n            WsStreamSettings.fromJson(json.wsSettings),\n            GrpcStreamSettings.fromJson(json.grpcSettings),\n            HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),\n            xHTTPStreamSettings.fromJson(json.xhttpSettings),\n            FinalMaskStreamSettings.fromJson(json.finalmask),\n            SockoptStreamSettings.fromJson(json.sockopt),\n        );\n    }\n\n    toJson() {\n        const network = this.network;\n        return {\n            network: network,\n            security: this.security,\n            externalProxy: this.externalProxy,\n            tlsSettings: this.isTls ? this.tls.toJson() : undefined,\n            realitySettings: this.isReality ? this.reality.toJson() : undefined,\n            tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,\n            kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,\n            wsSettings: network === 'ws' ? this.ws.toJson() : undefined,\n            grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,\n            httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,\n            xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,\n            finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,\n            sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,\n        };\n    }\n}\n\nclass Sniffing extends XrayCommonClass {\n    constructor(\n        enabled = false,\n        destOverride = ['http', 'tls', 'quic', 'fakedns'],\n        metadataOnly = false,\n        routeOnly = false) {\n        super();\n        this.enabled = enabled;\n        this.destOverride = destOverride;\n        this.metadataOnly = metadataOnly;\n        this.routeOnly = routeOnly;\n    }\n\n    static fromJson(json = {}) {\n        let destOverride = ObjectUtil.clone(json.destOverride);\n        if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {\n            if (ObjectUtil.isEmpty(destOverride[0])) {\n                destOverride = ['http', 'tls', 'quic', 'fakedns'];\n            }\n        }\n        return new Sniffing(\n            !!json.enabled,\n            destOverride,\n            json.metadataOnly,\n            json.routeOnly,\n        );\n    }\n}\n\nclass Inbound extends XrayCommonClass {\n    constructor(\n        port = RandomUtil.randomInteger(10000, 60000),\n        listen = '',\n        protocol = Protocols.VLESS,\n        settings = null,\n        streamSettings = new StreamSettings(),\n        tag = '',\n        sniffing = new Sniffing(),\n        clientStats = '',\n    ) {\n        super();\n        this.port = port;\n        this.listen = listen;\n        this._protocol = protocol;\n        this.settings = ObjectUtil.isEmpty(settings) ? Inbound.Settings.getSettings(protocol) : settings;\n        this.stream = streamSettings;\n        this.tag = tag;\n        this.sniffing = sniffing;\n        this.clientStats = clientStats;\n    }\n    getClientStats() {\n        return this.clientStats;\n    }\n\n    get clients() {\n        switch (this.protocol) {\n            case Protocols.VMESS: return this.settings.vmesses;\n            case Protocols.VLESS: return this.settings.vlesses;\n            case Protocols.TROJAN: return this.settings.trojans;\n            case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;\n            default: return null;\n        }\n    }\n\n    get protocol() {\n        return this._protocol;\n    }\n\n    set protocol(protocol) {\n        this._protocol = protocol;\n        this.settings = Inbound.Settings.getSettings(protocol);\n        if (protocol === Protocols.TROJAN) {\n            this.tls = false;\n        }\n    }\n\n    get network() {\n        return this.stream.network;\n    }\n\n    set network(network) {\n        this.stream.network = network;\n    }\n\n    get isTcp() {\n        return this.network === \"tcp\";\n    }\n\n    get isWs() {\n        return this.network === \"ws\";\n    }\n\n    get isKcp() {\n        return this.network === \"kcp\";\n    }\n\n    get isGrpc() {\n        return this.network === \"grpc\";\n    }\n\n    get isHttpupgrade() {\n        return this.network === \"httpupgrade\";\n    }\n\n    get isXHTTP() {\n        return this.network === \"xhttp\";\n    }\n\n    // Shadowsocks\n    get method() {\n        switch (this.protocol) {\n            case Protocols.SHADOWSOCKS:\n                return this.settings.method;\n            default:\n                return \"\";\n        }\n    }\n    get isSSMultiUser() {\n        return this.method != SSMethods.BLAKE3_CHACHA20_POLY1305;\n    }\n    get isSS2022() {\n        return this.method.substring(0, 4) === \"2022\";\n    }\n\n    get serverName() {\n        if (this.stream.isTls) return this.stream.tls.sni;\n        if (this.stream.isReality) return this.stream.reality.serverNames;\n        return \"\";\n    }\n\n    getHeader(obj, name) {\n        for (const header of obj.headers) {\n            if (header.name.toLowerCase() === name.toLowerCase()) {\n                return header.value;\n            }\n        }\n        return \"\";\n    }\n\n    get host() {\n        if (this.isTcp) {\n            return this.getHeader(this.stream.tcp.request, 'host');\n        } else if (this.isWs) {\n            return this.stream.ws.host?.length > 0 ? this.stream.ws.host : this.getHeader(this.stream.ws, 'host');\n        } else if (this.isHttpupgrade) {\n            return this.stream.httpupgrade.host?.length > 0 ? this.stream.httpupgrade.host : this.getHeader(this.stream.httpupgrade, 'host');\n        } else if (this.isXHTTP) {\n            return this.stream.xhttp.host?.length > 0 ? this.stream.xhttp.host : this.getHeader(this.stream.xhttp, 'host');\n        }\n        return null;\n    }\n\n    get path() {\n        if (this.isTcp) {\n            return this.stream.tcp.request.path[0];\n        } else if (this.isWs) {\n            return this.stream.ws.path;\n        } else if (this.isHttpupgrade) {\n            return this.stream.httpupgrade.path;\n        } else if (this.isXHTTP) {\n            return this.stream.xhttp.path;\n        }\n        return null;\n    }\n\n    get serviceName() {\n        return this.stream.grpc.serviceName;\n    }\n\n    isExpiry(index) {\n        let exp = this.clients[index].expiryTime;\n        return exp > 0 ? exp < new Date().getTime() : false;\n    }\n\n    canEnableTls() {\n        if (![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;\n        return [\"tcp\", \"ws\", \"http\", \"grpc\", \"httpupgrade\", \"xhttp\"].includes(this.network);\n    }\n\n    //this is used for xtls-rprx-vision\n    canEnableTlsFlow() {\n        if (((this.stream.security === 'tls') || (this.stream.security === 'reality')) && (this.network === \"tcp\")) {\n            return this.protocol === Protocols.VLESS;\n        }\n        return false;\n    }\n\n    // Vision seed applies only when vision flow is selected\n    canEnableVisionSeed() {\n        if (!this.canEnableTlsFlow()) return false;\n        const clients = this.settings?.vlesses;\n        if (!Array.isArray(clients)) return false;\n        return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION || c?.flow === TLS_FLOW_CONTROL.VISION_UDP443);\n    }\n\n    canEnableReality() {\n        if (![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;\n        return [\"tcp\", \"http\", \"grpc\", \"xhttp\"].includes(this.network);\n    }\n\n    canEnableStream() {\n        return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);\n    }\n\n    reset() {\n        this.port = RandomUtil.randomInteger(10000, 60000);\n        this.listen = '';\n        this.protocol = Protocols.VMESS;\n        this.settings = Inbound.Settings.getSettings(Protocols.VMESS);\n        this.stream = new StreamSettings();\n        this.tag = '';\n        this.sniffing = new Sniffing();\n    }\n\n    genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security) {\n        if (this.protocol !== Protocols.VMESS) {\n            return '';\n        }\n        const tls = forceTls == 'same' ? this.stream.security : forceTls;\n        let obj = {\n            v: '2',\n            ps: remark,\n            add: address,\n            port: port,\n            id: clientId,\n            scy: security,\n            net: this.stream.network,\n            tls: tls,\n        };\n        const network = this.stream.network;\n        if (network === 'tcp') {\n            const tcp = this.stream.tcp;\n            obj.type = tcp.type;\n            if (tcp.type === 'http') {\n                const request = tcp.request;\n                obj.path = request.path.join(',');\n                const host = this.getHeader(request, 'host');\n                if (host) obj.host = host;\n            }\n        } else if (network === 'kcp') {\n            const kcp = this.stream.kcp;\n        } else if (network === 'ws') {\n            const ws = this.stream.ws;\n            obj.path = ws.path;\n            obj.host = ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host');\n        } else if (network === 'grpc') {\n            obj.path = this.stream.grpc.serviceName;\n            obj.authority = this.stream.grpc.authority;\n            if (this.stream.grpc.multiMode) {\n                obj.type = 'multi'\n            }\n        } else if (network === 'httpupgrade') {\n            const httpupgrade = this.stream.httpupgrade;\n            obj.path = httpupgrade.path;\n            obj.host = httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host');\n        } else if (network === 'xhttp') {\n            const xhttp = this.stream.xhttp;\n            obj.path = xhttp.path;\n            obj.host = xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host');\n            obj.type = xhttp.mode;\n        }\n\n        if (tls === 'tls') {\n            if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {\n                obj.sni = this.stream.tls.sni;\n            }\n            if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)) {\n                obj.fp = this.stream.tls.settings.fingerprint;\n            }\n            if (this.stream.tls.alpn.length > 0) {\n                obj.alpn = this.stream.tls.alpn.join(',');\n            }\n        }\n\n        return 'vmess://' + Base64.encode(JSON.stringify(obj, null, 2));\n    }\n\n    genVLESSLink(address = '', port = this.port, forceTls, remark = '', clientId, flow) {\n        const uuid = clientId;\n        const type = this.stream.network;\n        const security = forceTls == 'same' ? this.stream.security : forceTls;\n        const params = new Map();\n        params.set(\"type\", this.stream.network);\n        params.set(\"encryption\", this.settings.encryption);\n        switch (type) {\n            case \"tcp\":\n                const tcp = this.stream.tcp;\n                if (tcp.type === 'http') {\n                    const request = tcp.request;\n                    params.set(\"path\", request.path.join(','));\n                    const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');\n                    if (index >= 0) {\n                        const host = request.headers[index].value;\n                        params.set(\"host\", host);\n                    }\n                    params.set(\"headerType\", 'http');\n                }\n                break;\n            case \"kcp\":\n                const kcp = this.stream.kcp;\n                break;\n            case \"ws\":\n                const ws = this.stream.ws;\n                params.set(\"path\", ws.path);\n                params.set(\"host\", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));\n                break;\n            case \"grpc\":\n                const grpc = this.stream.grpc;\n                params.set(\"serviceName\", grpc.serviceName);\n                params.set(\"authority\", grpc.authority);\n                if (grpc.multiMode) {\n                    params.set(\"mode\", \"multi\");\n                }\n                break;\n            case \"httpupgrade\":\n                const httpupgrade = this.stream.httpupgrade;\n                params.set(\"path\", httpupgrade.path);\n                params.set(\"host\", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));\n                break;\n            case \"xhttp\":\n                const xhttp = this.stream.xhttp;\n                params.set(\"path\", xhttp.path);\n                params.set(\"host\", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host'));\n                params.set(\"mode\", xhttp.mode);\n                break;\n        }\n\n        if (security === 'tls') {\n            params.set(\"security\", \"tls\");\n            if (this.stream.isTls) {\n                params.set(\"fp\", this.stream.tls.settings.fingerprint);\n                params.set(\"alpn\", this.stream.tls.alpn);\n                if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {\n                    params.set(\"sni\", this.stream.tls.sni);\n                }\n                if (this.stream.tls.settings.echConfigList?.length > 0) {\n                    params.set(\"ech\", this.stream.tls.settings.echConfigList);\n                }\n                if (type == \"tcp\" && !ObjectUtil.isEmpty(flow)) {\n                    params.set(\"flow\", flow);\n                }\n            }\n        }\n\n        else if (security === 'reality') {\n            params.set(\"security\", \"reality\");\n            params.set(\"pbk\", this.stream.reality.settings.publicKey);\n            params.set(\"fp\", this.stream.reality.settings.fingerprint);\n            if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {\n                params.set(\"sni\", this.stream.reality.serverNames.split(\",\")[0]);\n            }\n            if (this.stream.reality.shortIds.length > 0) {\n                params.set(\"sid\", this.stream.reality.shortIds.split(\",\")[0]);\n            }\n            if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {\n                params.set(\"spx\", this.stream.reality.settings.spiderX);\n            }\n            if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {\n                params.set(\"pqv\", this.stream.reality.settings.mldsa65Verify);\n            }\n            if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) {\n                params.set(\"flow\", flow);\n            }\n        }\n\n        else {\n            params.set(\"security\", \"none\");\n        }\n\n        const link = `vless://${uuid}@${address}:${port}`;\n        const url = new URL(link);\n        for (const [key, value] of params) {\n            url.searchParams.set(key, value)\n        }\n        url.hash = encodeURIComponent(remark);\n        return url.toString();\n    }\n\n    genSSLink(address = '', port = this.port, forceTls, remark = '', clientPassword) {\n        let settings = this.settings;\n        const type = this.stream.network;\n        const security = forceTls == 'same' ? this.stream.security : forceTls;\n        const params = new Map();\n        params.set(\"type\", this.stream.network);\n        switch (type) {\n            case \"tcp\":\n                const tcp = this.stream.tcp;\n                if (tcp.type === 'http') {\n                    const request = tcp.request;\n                    params.set(\"path\", request.path.join(','));\n                    const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');\n                    if (index >= 0) {\n                        const host = request.headers[index].value;\n                        params.set(\"host\", host);\n                    }\n                    params.set(\"headerType\", 'http');\n                }\n                break;\n            case \"kcp\":\n                const kcp = this.stream.kcp;\n                break;\n            case \"ws\":\n                const ws = this.stream.ws;\n                params.set(\"path\", ws.path);\n                params.set(\"host\", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));\n                break;\n            case \"grpc\":\n                const grpc = this.stream.grpc;\n                params.set(\"serviceName\", grpc.serviceName);\n                params.set(\"authority\", grpc.authority);\n                if (grpc.multiMode) {\n                    params.set(\"mode\", \"multi\");\n                }\n                break;\n            case \"httpupgrade\":\n                const httpupgrade = this.stream.httpupgrade;\n                params.set(\"path\", httpupgrade.path);\n                params.set(\"host\", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));\n                break;\n            case \"xhttp\":\n                const xhttp = this.stream.xhttp;\n                params.set(\"path\", xhttp.path);\n                params.set(\"host\", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host'));\n                params.set(\"mode\", xhttp.mode);\n                break;\n        }\n\n        if (security === 'tls') {\n            params.set(\"security\", \"tls\");\n            if (this.stream.isTls) {\n                params.set(\"fp\", this.stream.tls.settings.fingerprint);\n                params.set(\"alpn\", this.stream.tls.alpn);\n                if (this.stream.tls.settings.echConfigList?.length > 0) {\n                    params.set(\"ech\", this.stream.tls.settings.echConfigList);\n                }\n                if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {\n                    params.set(\"sni\", this.stream.tls.sni);\n                }\n            }\n        }\n\n\n        let password = new Array();\n        if (this.isSS2022) password.push(settings.password);\n        if (this.isSSMultiUser) password.push(clientPassword);\n\n        let link = `ss://${Base64.encode(`${settings.method}:${password.join(':')}`, true)}@${address}:${port}`;\n        const url = new URL(link);\n        for (const [key, value] of params) {\n            url.searchParams.set(key, value)\n        }\n        url.hash = encodeURIComponent(remark);\n        return url.toString();\n    }\n\n    genTrojanLink(address = '', port = this.port, forceTls, remark = '', clientPassword) {\n        const security = forceTls == 'same' ? this.stream.security : forceTls;\n        const type = this.stream.network;\n        const params = new Map();\n        params.set(\"type\", this.stream.network);\n        switch (type) {\n            case \"tcp\":\n                const tcp = this.stream.tcp;\n                if (tcp.type === 'http') {\n                    const request = tcp.request;\n                    params.set(\"path\", request.path.join(','));\n                    const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');\n                    if (index >= 0) {\n                        const host = request.headers[index].value;\n                        params.set(\"host\", host);\n                    }\n                    params.set(\"headerType\", 'http');\n                }\n                break;\n            case \"kcp\":\n                const kcp = this.stream.kcp;\n                break;\n            case \"ws\":\n                const ws = this.stream.ws;\n                params.set(\"path\", ws.path);\n                params.set(\"host\", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));\n                break;\n            case \"grpc\":\n                const grpc = this.stream.grpc;\n                params.set(\"serviceName\", grpc.serviceName);\n                params.set(\"authority\", grpc.authority);\n                if (grpc.multiMode) {\n                    params.set(\"mode\", \"multi\");\n                }\n                break;\n            case \"httpupgrade\":\n                const httpupgrade = this.stream.httpupgrade;\n                params.set(\"path\", httpupgrade.path);\n                params.set(\"host\", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));\n                break;\n            case \"xhttp\":\n                const xhttp = this.stream.xhttp;\n                params.set(\"path\", xhttp.path);\n                params.set(\"host\", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host'));\n                params.set(\"mode\", xhttp.mode);\n                break;\n        }\n\n        if (security === 'tls') {\n            params.set(\"security\", \"tls\");\n            if (this.stream.isTls) {\n                params.set(\"fp\", this.stream.tls.settings.fingerprint);\n                params.set(\"alpn\", this.stream.tls.alpn);\n                if (this.stream.tls.settings.echConfigList?.length > 0) {\n                    params.set(\"ech\", this.stream.tls.settings.echConfigList);\n                }\n                if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {\n                    params.set(\"sni\", this.stream.tls.sni);\n                }\n            }\n        }\n\n        else if (security === 'reality') {\n            params.set(\"security\", \"reality\");\n            params.set(\"pbk\", this.stream.reality.settings.publicKey);\n            params.set(\"fp\", this.stream.reality.settings.fingerprint);\n            if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {\n                params.set(\"sni\", this.stream.reality.serverNames.split(\",\")[0]);\n            }\n            if (this.stream.reality.shortIds.length > 0) {\n                params.set(\"sid\", this.stream.reality.shortIds.split(\",\")[0]);\n            }\n            if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {\n                params.set(\"spx\", this.stream.reality.settings.spiderX);\n            }\n            if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {\n                params.set(\"pqv\", this.stream.reality.settings.mldsa65Verify);\n            }\n        }\n\n        else {\n            params.set(\"security\", \"none\");\n        }\n\n        const link = `trojan://${clientPassword}@${address}:${port}`;\n        const url = new URL(link);\n        for (const [key, value] of params) {\n            url.searchParams.set(key, value)\n        }\n        url.hash = encodeURIComponent(remark);\n        return url.toString();\n    }\n\n    getWireguardLink(address, port, remark, peerId) {\n        let txt = `[Interface]\\n`\n        txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\\n`\n        txt += `Address = ${this.settings.peers[peerId].allowedIPs[0]}\\n`\n        txt += `DNS = 1.1.1.1, 1.0.0.1\\n`\n        if (this.settings.mtu) {\n            txt += `MTU = ${this.settings.mtu}\\n`\n        }\n        txt += `\\n# ${remark}\\n`\n        txt += `[Peer]\\n`\n        txt += `PublicKey = ${this.settings.pubKey}\\n`\n        txt += `AllowedIPs = 0.0.0.0/0, ::/0\\n`\n        txt += `Endpoint = ${address}:${port}`\n        if (this.settings.peers[peerId].psk) {\n            txt += `\\nPresharedKey = ${this.settings.peers[peerId].psk}`\n        }\n        if (this.settings.peers[peerId].keepAlive) {\n            txt += `\\nPersistentKeepalive = ${this.settings.peers[peerId].keepAlive}\\n`\n        }\n        return txt;\n    }\n\n    genLink(address = '', port = this.port, forceTls = 'same', remark = '', client) {\n        switch (this.protocol) {\n            case Protocols.VMESS:\n                return this.genVmessLink(address, port, forceTls, remark, client.id, client.security);\n            case Protocols.VLESS:\n                return this.genVLESSLink(address, port, forceTls, remark, client.id, client.flow);\n            case Protocols.SHADOWSOCKS:\n                return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '');\n            case Protocols.TROJAN:\n                return this.genTrojanLink(address, port, forceTls, remark, client.password);\n            default: return '';\n        }\n    }\n\n    genAllLinks(remark = '', remarkModel = '-ieo', client) {\n        let result = [];\n        let email = client ? client.email : '';\n        let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== \"0.0.0.0\" ? this.listen : location.hostname;\n        let port = this.port;\n        const separationChar = remarkModel.charAt(0);\n        const orderChars = remarkModel.slice(1);\n        let orders = {\n            'i': remark,\n            'e': email,\n            'o': '',\n        };\n        if (ObjectUtil.isArrEmpty(this.stream.externalProxy)) {\n            let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar);\n            result.push({\n                remark: r,\n                link: this.genLink(addr, port, 'same', r, client)\n            });\n        } else {\n            this.stream.externalProxy.forEach((ep) => {\n                orders['o'] = ep.remark;\n                let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar);\n                result.push({\n                    remark: r,\n                    link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client)\n                });\n            });\n        }\n        return result;\n    }\n\n    genInboundLinks(remark = '', remarkModel = '-ieo') {\n        let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== \"0.0.0.0\" ? this.listen : location.hostname;\n        if (this.clients) {\n            let links = [];\n            this.clients.forEach((client) => {\n                this.genAllLinks(remark, remarkModel, client).forEach(l => {\n                    links.push(l.link);\n                })\n            });\n            return links.join('\\r\\n');\n        } else {\n            if (this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(addr, this.port, 'same', remark);\n            if (this.protocol == Protocols.WIREGUARD) {\n                let links = [];\n                this.settings.peers.forEach((p, index) => {\n                    links.push(this.getWireguardLink(addr, this.port, remark + remarkModel.charAt(0) + (index + 1), index));\n                });\n                return links.join('\\r\\n');\n            }\n            return '';\n        }\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound(\n            json.port,\n            json.listen,\n            json.protocol,\n            Inbound.Settings.fromJson(json.protocol, json.settings),\n            StreamSettings.fromJson(json.streamSettings),\n            json.tag,\n            Sniffing.fromJson(json.sniffing),\n            json.clientStats\n        )\n    }\n\n    toJson() {\n        let streamSettings;\n        if (this.canEnableStream() || this.stream?.sockopt) {\n            streamSettings = this.stream.toJson();\n        }\n        return {\n            port: this.port,\n            listen: this.listen,\n            protocol: this.protocol,\n            settings: this.settings instanceof XrayCommonClass ? this.settings.toJson() : this.settings,\n            streamSettings: streamSettings,\n            tag: this.tag,\n            sniffing: this.sniffing.toJson(),\n            clientStats: this.clientStats\n        };\n    }\n}\n\nInbound.Settings = class extends XrayCommonClass {\n    constructor(protocol) {\n        super();\n        this.protocol = protocol;\n    }\n\n    static getSettings(protocol) {\n        switch (protocol) {\n            case Protocols.VMESS: return new Inbound.VmessSettings(protocol);\n            case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);\n            case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);\n            case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);\n            case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);\n            case Protocols.MIXED: return new Inbound.MixedSettings(protocol);\n            case Protocols.HTTP: return new Inbound.HttpSettings(protocol);\n            case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);\n            case Protocols.TUN: return new Inbound.TunSettings(protocol);\n            default: return null;\n        }\n    }\n\n    static fromJson(protocol, json) {\n        switch (protocol) {\n            case Protocols.VMESS: return Inbound.VmessSettings.fromJson(json);\n            case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);\n            case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);\n            case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);\n            case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);\n            case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);\n            case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);\n            case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);\n            case Protocols.TUN: return Inbound.TunSettings.fromJson(json);\n            default: return null;\n        }\n    }\n\n    toJson() {\n        return {};\n    }\n};\n\nInbound.VmessSettings = class extends Inbound.Settings {\n    constructor(protocol,\n        vmesses = [new Inbound.VmessSettings.VMESS()]) {\n        super(protocol);\n        this.vmesses = vmesses;\n    }\n\n    indexOfVmessById(id) {\n        return this.vmesses.findIndex(VMESS => VMESS.id === id);\n    }\n\n    addVmess(VMESS) {\n        if (this.indexOfVmessById(VMESS.id) >= 0) {\n            return false;\n        }\n        this.vmesses.push(VMESS);\n    }\n\n    delVmess(VMESS) {\n        const i = this.indexOfVmessById(VMESS.id);\n        if (i >= 0) {\n            this.vmesses.splice(i, 1);\n        }\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.VmessSettings(\n            Protocols.VMESS,\n            json.clients.map(client => Inbound.VmessSettings.VMESS.fromJson(client)),\n        );\n    }\n\n    toJson() {\n        return {\n            clients: Inbound.VmessSettings.toJsonArray(this.vmesses),\n        };\n    }\n};\n\nInbound.VmessSettings.VMESS = class extends XrayCommonClass {\n    constructor(\n        id = RandomUtil.randomUUID(),\n        security = USERS_SECURITY.AUTO,\n        email = RandomUtil.randomLowerAndNum(8),\n        limitIp = 0,\n        totalGB = 0,\n        expiryTime = 0,\n        enable = true,\n        tgId = '',\n        subId = RandomUtil.randomLowerAndNum(16),\n        comment = '',\n        reset = 0,\n        created_at = undefined,\n        updated_at = undefined\n    ) {\n        super();\n        this.id = id;\n        this.security = security;\n        this.email = email;\n        this.limitIp = limitIp;\n        this.totalGB = totalGB;\n        this.expiryTime = expiryTime;\n        this.enable = enable;\n        this.tgId = tgId;\n        this.subId = subId;\n        this.comment = comment;\n        this.reset = reset;\n        this.created_at = created_at;\n        this.updated_at = updated_at;\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.VmessSettings.VMESS(\n            json.id,\n            json.security,\n            json.email,\n            json.limitIp,\n            json.totalGB,\n            json.expiryTime,\n            json.enable,\n            json.tgId,\n            json.subId,\n            json.comment,\n            json.reset,\n            json.created_at,\n            json.updated_at,\n        );\n    }\n    get _expiryTime() {\n        if (this.expiryTime === 0 || this.expiryTime === \"\") {\n            return null;\n        }\n        if (this.expiryTime < 0) {\n            return this.expiryTime / -86400000;\n        }\n        return moment(this.expiryTime);\n    }\n\n    set _expiryTime(t) {\n        if (t == null || t === \"\") {\n            this.expiryTime = 0;\n        } else {\n            this.expiryTime = t.valueOf();\n        }\n    }\n    get _totalGB() {\n        return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);\n    }\n\n    set _totalGB(gb) {\n        this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);\n    }\n\n};\n\nInbound.VLESSSettings = class extends Inbound.Settings {\n    constructor(\n        protocol,\n        vlesses = [new Inbound.VLESSSettings.VLESS()],\n        decryption = \"none\",\n        encryption = \"none\",\n        fallbacks = [],\n        selectedAuth = undefined,\n        testseed = [900, 500, 900, 256],\n    ) {\n        super(protocol);\n        this.vlesses = vlesses;\n        this.decryption = decryption;\n        this.encryption = encryption;\n        this.fallbacks = fallbacks;\n        this.selectedAuth = selectedAuth;\n        this.testseed = testseed;\n    }\n\n    addFallback() {\n        this.fallbacks.push(new Inbound.VLESSSettings.Fallback());\n    }\n\n    delFallback(index) {\n        this.fallbacks.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        // Ensure testseed is always initialized as an array\n        let testseed = [900, 500, 900, 256];\n        if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) {\n            testseed = json.testseed;\n        }\n\n        const obj = new Inbound.VLESSSettings(\n            Protocols.VLESS,\n            (json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),\n            json.decryption,\n            json.encryption,\n            Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),\n            json.selectedAuth,\n            testseed\n        );\n        return obj;\n    }\n\n\n    toJson() {\n        const json = {\n            clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),\n        };\n\n        if (this.decryption) {\n            json.decryption = this.decryption;\n        }\n\n        if (this.encryption) {\n            json.encryption = this.encryption;\n        }\n\n        if (this.fallbacks && this.fallbacks.length > 0) {\n            json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);\n        }\n        if (this.selectedAuth) {\n            json.selectedAuth = this.selectedAuth;\n        }\n\n        // Only include testseed if at least one client has a flow set\n        const hasFlow = this.vlesses && this.vlesses.some(vless => vless.flow && vless.flow !== '');\n        if (hasFlow && this.testseed && this.testseed.length >= 4) {\n            json.testseed = this.testseed;\n        }\n\n        return json;\n    }\n\n\n};\n\nInbound.VLESSSettings.VLESS = class extends XrayCommonClass {\n    constructor(\n        id = RandomUtil.randomUUID(),\n        flow = '',\n        email = RandomUtil.randomLowerAndNum(8),\n        limitIp = 0,\n        totalGB = 0,\n        expiryTime = 0,\n        enable = true,\n        tgId = '',\n        subId = RandomUtil.randomLowerAndNum(16),\n        comment = '',\n        reset = 0,\n        created_at = undefined,\n        updated_at = undefined\n    ) {\n        super();\n        this.id = id;\n        this.flow = flow;\n        this.email = email;\n        this.limitIp = limitIp;\n        this.totalGB = totalGB;\n        this.expiryTime = expiryTime;\n        this.enable = enable;\n        this.tgId = tgId;\n        this.subId = subId;\n        this.comment = comment;\n        this.reset = reset;\n        this.created_at = created_at;\n        this.updated_at = updated_at;\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.VLESSSettings.VLESS(\n            json.id,\n            json.flow,\n            json.email,\n            json.limitIp,\n            json.totalGB,\n            json.expiryTime,\n            json.enable,\n            json.tgId,\n            json.subId,\n            json.comment,\n            json.reset,\n            json.created_at,\n            json.updated_at,\n        );\n    }\n\n    get _expiryTime() {\n        if (this.expiryTime === 0 || this.expiryTime === \"\") {\n            return null;\n        }\n        if (this.expiryTime < 0) {\n            return this.expiryTime / -86400000;\n        }\n        return moment(this.expiryTime);\n    }\n\n    set _expiryTime(t) {\n        if (t == null || t === \"\") {\n            this.expiryTime = 0;\n        } else {\n            this.expiryTime = t.valueOf();\n        }\n    }\n    get _totalGB() {\n        return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);\n    }\n\n    set _totalGB(gb) {\n        this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);\n    }\n};\nInbound.VLESSSettings.Fallback = class extends XrayCommonClass {\n    constructor(name = \"\", alpn = '', path = '', dest = '', xver = 0) {\n        super();\n        this.name = name;\n        this.alpn = alpn;\n        this.path = path;\n        this.dest = dest;\n        this.xver = xver;\n    }\n\n    toJson() {\n        let xver = this.xver;\n        if (!Number.isInteger(xver)) {\n            xver = 0;\n        }\n        return {\n            name: this.name,\n            alpn: this.alpn,\n            path: this.path,\n            dest: this.dest,\n            xver: xver,\n        }\n    }\n\n    static fromJson(json = []) {\n        const fallbacks = [];\n        for (let fallback of json) {\n            fallbacks.push(new Inbound.VLESSSettings.Fallback(\n                fallback.name,\n                fallback.alpn,\n                fallback.path,\n                fallback.dest,\n                fallback.xver,\n            ))\n        }\n        return fallbacks;\n    }\n};\n\nInbound.TrojanSettings = class extends Inbound.Settings {\n    constructor(protocol,\n        trojans = [new Inbound.TrojanSettings.Trojan()],\n        fallbacks = [],) {\n        super(protocol);\n        this.trojans = trojans;\n        this.fallbacks = fallbacks;\n    }\n\n    addFallback() {\n        this.fallbacks.push(new Inbound.TrojanSettings.Fallback());\n    }\n\n    delFallback(index) {\n        this.fallbacks.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.TrojanSettings(\n            Protocols.TROJAN,\n            json.clients.map(client => Inbound.TrojanSettings.Trojan.fromJson(client)),\n            Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);\n    }\n\n    toJson() {\n        return {\n            clients: Inbound.TrojanSettings.toJsonArray(this.trojans),\n            fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks)\n        };\n    }\n};\n\nInbound.TrojanSettings.Trojan = class extends XrayCommonClass {\n    constructor(\n        password = RandomUtil.randomSeq(10),\n        email = RandomUtil.randomLowerAndNum(8),\n        limitIp = 0,\n        totalGB = 0,\n        expiryTime = 0,\n        enable = true,\n        tgId = '',\n        subId = RandomUtil.randomLowerAndNum(16),\n        comment = '',\n        reset = 0,\n        created_at = undefined,\n        updated_at = undefined\n    ) {\n        super();\n        this.password = password;\n        this.email = email;\n        this.limitIp = limitIp;\n        this.totalGB = totalGB;\n        this.expiryTime = expiryTime;\n        this.enable = enable;\n        this.tgId = tgId;\n        this.subId = subId;\n        this.comment = comment;\n        this.reset = reset;\n        this.created_at = created_at;\n        this.updated_at = updated_at;\n    }\n\n    toJson() {\n        return {\n            password: this.password,\n            email: this.email,\n            limitIp: this.limitIp,\n            totalGB: this.totalGB,\n            expiryTime: this.expiryTime,\n            enable: this.enable,\n            tgId: this.tgId,\n            subId: this.subId,\n            comment: this.comment,\n            reset: this.reset,\n            created_at: this.created_at,\n            updated_at: this.updated_at,\n        };\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.TrojanSettings.Trojan(\n            json.password,\n            json.email,\n            json.limitIp,\n            json.totalGB,\n            json.expiryTime,\n            json.enable,\n            json.tgId,\n            json.subId,\n            json.comment,\n            json.reset,\n            json.created_at,\n            json.updated_at,\n        );\n    }\n\n    get _expiryTime() {\n        if (this.expiryTime === 0 || this.expiryTime === \"\") {\n            return null;\n        }\n        if (this.expiryTime < 0) {\n            return this.expiryTime / -86400000;\n        }\n        return moment(this.expiryTime);\n    }\n\n    set _expiryTime(t) {\n        if (t == null || t === \"\") {\n            this.expiryTime = 0;\n        } else {\n            this.expiryTime = t.valueOf();\n        }\n    }\n    get _totalGB() {\n        return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);\n    }\n\n    set _totalGB(gb) {\n        this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);\n    }\n\n};\n\nInbound.TrojanSettings.Fallback = class extends XrayCommonClass {\n    constructor(name = \"\", alpn = '', path = '', dest = '', xver = 0) {\n        super();\n        this.name = name;\n        this.alpn = alpn;\n        this.path = path;\n        this.dest = dest;\n        this.xver = xver;\n    }\n\n    toJson() {\n        let xver = this.xver;\n        if (!Number.isInteger(xver)) {\n            xver = 0;\n        }\n        return {\n            name: this.name,\n            alpn: this.alpn,\n            path: this.path,\n            dest: this.dest,\n            xver: xver,\n        }\n    }\n\n    static fromJson(json = []) {\n        const fallbacks = [];\n        for (let fallback of json) {\n            fallbacks.push(new Inbound.TrojanSettings.Fallback(\n                fallback.name,\n                fallback.alpn,\n                fallback.path,\n                fallback.dest,\n                fallback.xver,\n            ))\n        }\n        return fallbacks;\n    }\n};\n\nInbound.ShadowsocksSettings = class extends Inbound.Settings {\n    constructor(protocol,\n        method = SSMethods.BLAKE3_AES_256_GCM,\n        password = RandomUtil.randomShadowsocksPassword(),\n        network = 'tcp,udp',\n        shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()],\n        ivCheck = false,\n    ) {\n        super(protocol);\n        this.method = method;\n        this.password = password;\n        this.network = network;\n        this.shadowsockses = shadowsockses;\n        this.ivCheck = ivCheck;\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.ShadowsocksSettings(\n            Protocols.SHADOWSOCKS,\n            json.method,\n            json.password,\n            json.network,\n            json.clients.map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),\n            json.ivCheck,\n        );\n    }\n\n    toJson() {\n        return {\n            method: this.method,\n            password: this.password,\n            network: this.network,\n            clients: Inbound.ShadowsocksSettings.toJsonArray(this.shadowsockses),\n            ivCheck: this.ivCheck,\n        };\n    }\n};\n\nInbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {\n    constructor(\n        method = '',\n        password = RandomUtil.randomShadowsocksPassword(),\n        email = RandomUtil.randomLowerAndNum(8),\n        limitIp = 0,\n        totalGB = 0,\n        expiryTime = 0,\n        enable = true,\n        tgId = '',\n        subId = RandomUtil.randomLowerAndNum(16),\n        comment = '',\n        reset = 0,\n        created_at = undefined,\n        updated_at = undefined\n    ) {\n        super();\n        this.method = method;\n        this.password = password;\n        this.email = email;\n        this.limitIp = limitIp;\n        this.totalGB = totalGB;\n        this.expiryTime = expiryTime;\n        this.enable = enable;\n        this.tgId = tgId;\n        this.subId = subId;\n        this.comment = comment;\n        this.reset = reset;\n        this.created_at = created_at;\n        this.updated_at = updated_at;\n    }\n\n    toJson() {\n        return {\n            method: this.method,\n            password: this.password,\n            email: this.email,\n            limitIp: this.limitIp,\n            totalGB: this.totalGB,\n            expiryTime: this.expiryTime,\n            enable: this.enable,\n            tgId: this.tgId,\n            subId: this.subId,\n            comment: this.comment,\n            reset: this.reset,\n            created_at: this.created_at,\n            updated_at: this.updated_at,\n        };\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.ShadowsocksSettings.Shadowsocks(\n            json.method,\n            json.password,\n            json.email,\n            json.limitIp,\n            json.totalGB,\n            json.expiryTime,\n            json.enable,\n            json.tgId,\n            json.subId,\n            json.comment,\n            json.reset,\n            json.created_at,\n            json.updated_at,\n        );\n    }\n\n    get _expiryTime() {\n        if (this.expiryTime === 0 || this.expiryTime === \"\") {\n            return null;\n        }\n        if (this.expiryTime < 0) {\n            return this.expiryTime / -86400000;\n        }\n        return moment(this.expiryTime);\n    }\n\n    set _expiryTime(t) {\n        if (t == null || t === \"\") {\n            this.expiryTime = 0;\n        } else {\n            this.expiryTime = t.valueOf();\n        }\n    }\n    get _totalGB() {\n        return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);\n    }\n\n    set _totalGB(gb) {\n        this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);\n    }\n\n};\n\nInbound.TunnelSettings = class extends Inbound.Settings {\n    constructor(\n        protocol,\n        address,\n        port,\n        portMap = [],\n        network = 'tcp,udp',\n        followRedirect = false\n    ) {\n        super(protocol);\n        this.address = address;\n        this.port = port;\n        this.portMap = portMap;\n        this.network = network;\n        this.followRedirect = followRedirect;\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.TunnelSettings(\n            Protocols.TUNNEL,\n            json.address,\n            json.port,\n            XrayCommonClass.toHeaders(json.portMap),\n            json.network,\n            json.followRedirect,\n        );\n    }\n\n    toJson() {\n        return {\n            address: this.address,\n            port: this.port,\n            portMap: XrayCommonClass.toV2Headers(this.portMap, false),\n            network: this.network,\n            followRedirect: this.followRedirect,\n        };\n    }\n};\n\nInbound.MixedSettings = class extends Inbound.Settings {\n    constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {\n        super(protocol);\n        this.auth = auth;\n        this.accounts = accounts;\n        this.udp = udp;\n        this.ip = ip;\n    }\n\n    addAccount(account) {\n        this.accounts.push(account);\n    }\n\n    delAccount(index) {\n        this.accounts.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        let accounts;\n        if (json.auth === 'password') {\n            accounts = json.accounts.map(\n                account => Inbound.MixedSettings.SocksAccount.fromJson(account)\n            )\n        }\n        return new Inbound.MixedSettings(\n            Protocols.MIXED,\n            json.auth,\n            accounts,\n            json.udp,\n            json.ip,\n        );\n    }\n\n    toJson() {\n        return {\n            auth: this.auth,\n            accounts: this.auth === 'password' ? this.accounts.map(account => account.toJson()) : undefined,\n            udp: this.udp,\n            ip: this.ip,\n        };\n    }\n};\nInbound.MixedSettings.SocksAccount = class extends XrayCommonClass {\n    constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {\n        super();\n        this.user = user;\n        this.pass = pass;\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);\n    }\n};\n\nInbound.HttpSettings = class extends Inbound.Settings {\n    constructor(\n        protocol,\n        accounts = [new Inbound.HttpSettings.HttpAccount()],\n        allowTransparent = false,\n    ) {\n        super(protocol);\n        this.accounts = accounts;\n        this.allowTransparent = allowTransparent;\n    }\n\n    addAccount(account) {\n        this.accounts.push(account);\n    }\n\n    delAccount(index) {\n        this.accounts.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.HttpSettings(\n            Protocols.HTTP,\n            json.accounts.map(account => Inbound.HttpSettings.HttpAccount.fromJson(account)),\n            json.allowTransparent,\n        );\n    }\n\n    toJson() {\n        return {\n            accounts: Inbound.HttpSettings.toJsonArray(this.accounts),\n            allowTransparent: this.allowTransparent,\n        };\n    }\n};\n\nInbound.HttpSettings.HttpAccount = class extends XrayCommonClass {\n    constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {\n        super();\n        this.user = user;\n        this.pass = pass;\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.HttpSettings.HttpAccount(json.user, json.pass);\n    }\n};\n\nInbound.WireguardSettings = class extends XrayCommonClass {\n    constructor(\n        protocol,\n        mtu = 1420,\n        secretKey = Wireguard.generateKeypair().privateKey,\n        peers = [new Inbound.WireguardSettings.Peer()],\n        noKernelTun = false\n    ) {\n        super(protocol);\n        this.mtu = mtu;\n        this.secretKey = secretKey;\n        this.pubKey = secretKey.length > 0 ? Wireguard.generateKeypair(secretKey).publicKey : '';\n        this.peers = peers;\n        this.noKernelTun = noKernelTun;\n    }\n\n    addPeer() {\n        this.peers.push(new Inbound.WireguardSettings.Peer(null, null, '', ['10.0.0.' + (this.peers.length + 2)]));\n    }\n\n    delPeer(index) {\n        this.peers.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.WireguardSettings(\n            Protocols.WIREGUARD,\n            json.mtu,\n            json.secretKey,\n            json.peers.map(peer => Inbound.WireguardSettings.Peer.fromJson(peer)),\n            json.noKernelTun,\n        );\n    }\n\n    toJson() {\n        return {\n            mtu: this.mtu ?? undefined,\n            secretKey: this.secretKey,\n            peers: Inbound.WireguardSettings.Peer.toJsonArray(this.peers),\n            noKernelTun: this.noKernelTun,\n        };\n    }\n};\n\nInbound.WireguardSettings.Peer = class extends XrayCommonClass {\n    constructor(privateKey, publicKey, psk = '', allowedIPs = ['10.0.0.2/32'], keepAlive = 0) {\n        super();\n        this.privateKey = privateKey\n        this.publicKey = publicKey;\n        if (!this.publicKey) {\n            [this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair())\n        }\n        this.psk = psk;\n        allowedIPs.forEach((a, index) => {\n            if (a.length > 0 && !a.includes('/')) allowedIPs[index] += '/32';\n        })\n        this.allowedIPs = allowedIPs;\n        this.keepAlive = keepAlive;\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.WireguardSettings.Peer(\n            json.privateKey,\n            json.publicKey,\n            json.preSharedKey,\n            json.allowedIPs,\n            json.keepAlive\n        );\n    }\n\n    toJson() {\n        this.allowedIPs.forEach((a, index) => {\n            if (a.length > 0 && !a.includes('/')) this.allowedIPs[index] += '/32';\n        });\n        return {\n            privateKey: this.privateKey,\n            publicKey: this.publicKey,\n            preSharedKey: this.psk.length > 0 ? this.psk : undefined,\n            allowedIPs: this.allowedIPs,\n            keepAlive: this.keepAlive ?? undefined,\n        };\n    }\n};\n\nInbound.TunSettings = class extends Inbound.Settings {\n    constructor(\n        protocol,\n        name = 'xray0',\n        mtu = 1500,\n        userLevel = 0\n    ) {\n        super(protocol);\n        this.name = name;\n        this.mtu = mtu;\n        this.userLevel = userLevel;\n    }\n\n    static fromJson(json = {}) {\n        return new Inbound.TunSettings(\n            Protocols.TUN,\n            json.name ?? 'xray0',\n            json.mtu ?? json.MTU ?? 1500,\n            json.userLevel ?? 0\n        );\n    }\n\n    toJson() {\n        return {\n            name: this.name || 'xray0',\n            mtu: this.mtu || 1500,\n            userLevel: this.userLevel || 0,\n        };\n    }\n};\n"
  },
  {
    "path": "web/assets/js/model/outbound.js",
    "content": "const Protocols = {\n    Freedom: \"freedom\",\n    Blackhole: \"blackhole\",\n    DNS: \"dns\",\n    VMess: \"vmess\",\n    VLESS: \"vless\",\n    Trojan: \"trojan\",\n    Shadowsocks: \"shadowsocks\",\n    Socks: \"socks\",\n    HTTP: \"http\",\n    Wireguard: \"wireguard\",\n    Hysteria: \"hysteria\"\n};\n\nconst SSMethods = {\n    AES_256_GCM: 'aes-256-gcm',\n    AES_128_GCM: 'aes-128-gcm',\n    CHACHA20_POLY1305: 'chacha20-poly1305',\n    CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',\n    XCHACHA20_POLY1305: 'xchacha20-poly1305',\n    XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',\n    BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',\n    BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',\n    BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',\n};\n\nconst TLS_FLOW_CONTROL = {\n    VISION: \"xtls-rprx-vision\",\n    VISION_UDP443: \"xtls-rprx-vision-udp443\",\n};\n\nconst UTLS_FINGERPRINT = {\n    UTLS_CHROME: \"chrome\",\n    UTLS_FIREFOX: \"firefox\",\n    UTLS_SAFARI: \"safari\",\n    UTLS_IOS: \"ios\",\n    UTLS_android: \"android\",\n    UTLS_EDGE: \"edge\",\n    UTLS_360: \"360\",\n    UTLS_QQ: \"qq\",\n    UTLS_RANDOM: \"random\",\n    UTLS_RANDOMIZED: \"randomized\",\n    UTLS_RONDOMIZEDNOALPN: \"randomizednoalpn\",\n    UTLS_UNSAFE: \"unsafe\",\n};\n\nconst ALPN_OPTION = {\n    H3: \"h3\",\n    H2: \"h2\",\n    HTTP1: \"http/1.1\",\n};\n\nconst OutboundDomainStrategies = [\n    \"AsIs\",\n    \"UseIP\",\n    \"UseIPv4\",\n    \"UseIPv6\",\n    \"UseIPv6v4\",\n    \"UseIPv4v6\",\n    \"ForceIP\",\n    \"ForceIPv6v4\",\n    \"ForceIPv6\",\n    \"ForceIPv4v6\",\n    \"ForceIPv4\"\n];\n\nconst WireguardDomainStrategy = [\n    \"ForceIP\",\n    \"ForceIPv4\",\n    \"ForceIPv4v6\",\n    \"ForceIPv6\",\n    \"ForceIPv6v4\"\n];\n\nconst USERS_SECURITY = {\n    AES_128_GCM: \"aes-128-gcm\",\n    CHACHA20_POLY1305: \"chacha20-poly1305\",\n    AUTO: \"auto\",\n    NONE: \"none\",\n    ZERO: \"zero\",\n};\n\nconst MODE_OPTION = {\n    AUTO: \"auto\",\n    PACKET_UP: \"packet-up\",\n    STREAM_UP: \"stream-up\",\n    STREAM_ONE: \"stream-one\",\n};\n\nconst Address_Port_Strategy = {\n    NONE: \"none\",\n    SrvPortOnly: \"srvportonly\",\n    SrvAddressOnly: \"srvaddressonly\",\n    SrvPortAndAddress: \"srvportandaddress\",\n    TxtPortOnly: \"txtportonly\",\n    TxtAddressOnly: \"txtaddressonly\",\n    TxtPortAndAddress: \"txtportandaddress\"\n};\n\nObject.freeze(Protocols);\nObject.freeze(SSMethods);\nObject.freeze(TLS_FLOW_CONTROL);\nObject.freeze(UTLS_FINGERPRINT);\nObject.freeze(ALPN_OPTION);\nObject.freeze(OutboundDomainStrategies);\nObject.freeze(WireguardDomainStrategy);\nObject.freeze(USERS_SECURITY);\nObject.freeze(MODE_OPTION);\nObject.freeze(Address_Port_Strategy);\n\nclass CommonClass {\n\n    static toJsonArray(arr) {\n        return arr.map(obj => obj.toJson());\n    }\n\n    static fromJson() {\n        return new CommonClass();\n    }\n\n    toJson() {\n        return this;\n    }\n\n    toString(format = true) {\n        return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());\n    }\n}\n\nclass TcpStreamSettings extends CommonClass {\n    constructor(type = 'none', host, path) {\n        super();\n        this.type = type;\n        this.host = host;\n        this.path = path;\n    }\n\n    static fromJson(json = {}) {\n        let header = json.header;\n        if (!header) return new TcpStreamSettings();\n        if (header.type == 'http' && header.request) {\n            return new TcpStreamSettings(\n                header.type,\n                header.request.headers.Host.join(','),\n                header.request.path.join(','),\n            );\n        }\n        return new TcpStreamSettings(header.type, '', '');\n    }\n\n    toJson() {\n        return {\n            header: {\n                type: this.type,\n                request: this.type === 'http' ? {\n                    headers: {\n                        Host: ObjectUtil.isEmpty(this.host) ? [] : this.host.split(',')\n                    },\n                    path: ObjectUtil.isEmpty(this.path) ? [\"/\"] : this.path.split(',')\n                } : undefined,\n            }\n        };\n    }\n}\n\nclass KcpStreamSettings extends CommonClass {\n    constructor(\n        mtu = 1350,\n        tti = 20,\n        uplinkCapacity = 5,\n        downlinkCapacity = 20,\n        congestion = false,\n        readBufferSize = 1,\n        writeBufferSize = 1,\n    ) {\n        super();\n        this.mtu = mtu;\n        this.tti = tti;\n        this.upCap = uplinkCapacity;\n        this.downCap = downlinkCapacity;\n        this.congestion = congestion;\n        this.readBuffer = readBufferSize;\n        this.writeBuffer = writeBufferSize;\n    }\n\n    static fromJson(json = {}) {\n        return new KcpStreamSettings(\n            json.mtu,\n            json.tti,\n            json.uplinkCapacity,\n            json.downlinkCapacity,\n            json.congestion,\n            json.readBufferSize,\n            json.writeBufferSize,\n        );\n    }\n\n    toJson() {\n        return {\n            mtu: this.mtu,\n            tti: this.tti,\n            uplinkCapacity: this.upCap,\n            downlinkCapacity: this.downCap,\n            congestion: this.congestion,\n            readBufferSize: this.readBuffer,\n            writeBufferSize: this.writeBuffer,\n        };\n    }\n}\n\nclass WsStreamSettings extends CommonClass {\n    constructor(\n        path = '/',\n        host = '',\n        heartbeatPeriod = 0,\n\n    ) {\n        super();\n        this.path = path;\n        this.host = host;\n        this.heartbeatPeriod = heartbeatPeriod;\n    }\n\n    static fromJson(json = {}) {\n        return new WsStreamSettings(\n            json.path,\n            json.host,\n            json.heartbeatPeriod,\n        );\n    }\n\n    toJson() {\n        return {\n            path: this.path,\n            host: this.host,\n            heartbeatPeriod: this.heartbeatPeriod\n        };\n    }\n}\n\nclass GrpcStreamSettings extends CommonClass {\n    constructor(\n        serviceName = \"\",\n        authority = \"\",\n        multiMode = false\n    ) {\n        super();\n        this.serviceName = serviceName;\n        this.authority = authority;\n        this.multiMode = multiMode;\n    }\n\n    static fromJson(json = {}) {\n        return new GrpcStreamSettings(json.serviceName, json.authority, json.multiMode);\n    }\n\n    toJson() {\n        return {\n            serviceName: this.serviceName,\n            authority: this.authority,\n            multiMode: this.multiMode\n        }\n    }\n}\n\nclass HttpUpgradeStreamSettings extends CommonClass {\n    constructor(path = '/', host = '') {\n        super();\n        this.path = path;\n        this.host = host;\n    }\n\n    static fromJson(json = {}) {\n        return new HttpUpgradeStreamSettings(\n            json.path,\n            json.host,\n        );\n    }\n\n    toJson() {\n        return {\n            path: this.path,\n            host: this.host,\n        };\n    }\n}\n\nclass xHTTPStreamSettings extends CommonClass {\n    constructor(\n        path = '/',\n        host = '',\n        mode = '',\n        noGRPCHeader = false,\n        scMinPostsIntervalMs = \"30\",\n        xmux = {\n            maxConcurrency: \"16-32\",\n            maxConnections: 0,\n            cMaxReuseTimes: 0,\n            hMaxRequestTimes: \"600-900\",\n            hMaxReusableSecs: \"1800-3000\",\n            hKeepAlivePeriod: 0,\n        },\n    ) {\n        super();\n        this.path = path;\n        this.host = host;\n        this.mode = mode;\n        this.noGRPCHeader = noGRPCHeader;\n        this.scMinPostsIntervalMs = scMinPostsIntervalMs;\n        this.xmux = xmux;\n    }\n\n    static fromJson(json = {}) {\n        return new xHTTPStreamSettings(\n            json.path,\n            json.host,\n            json.mode,\n            json.noGRPCHeader,\n            json.scMinPostsIntervalMs,\n            json.xmux\n        );\n    }\n\n    toJson() {\n        return {\n            path: this.path,\n            host: this.host,\n            mode: this.mode,\n            noGRPCHeader: this.noGRPCHeader,\n            scMinPostsIntervalMs: this.scMinPostsIntervalMs,\n            xmux: {\n                maxConcurrency: this.xmux.maxConcurrency,\n                maxConnections: this.xmux.maxConnections,\n                cMaxReuseTimes: this.xmux.cMaxReuseTimes,\n                hMaxRequestTimes: this.xmux.hMaxRequestTimes,\n                hMaxReusableSecs: this.xmux.hMaxReusableSecs,\n                hKeepAlivePeriod: this.xmux.hKeepAlivePeriod,\n            },\n        };\n    }\n}\n\nclass TlsStreamSettings extends CommonClass {\n    constructor(\n        serverName = '',\n        alpn = [],\n        fingerprint = '',\n        echConfigList = '',\n        verifyPeerCertByName = '',\n        pinnedPeerCertSha256 = '',\n    ) {\n        super();\n        this.serverName = serverName;\n        this.alpn = alpn;\n        this.fingerprint = fingerprint;\n        this.echConfigList = echConfigList;\n        this.verifyPeerCertByName = verifyPeerCertByName;\n        this.pinnedPeerCertSha256 = pinnedPeerCertSha256;\n    }\n\n    static fromJson(json = {}) {\n        return new TlsStreamSettings(\n            json.serverName,\n            json.alpn,\n            json.fingerprint,\n            json.echConfigList,\n            json.verifyPeerCertByName,\n            json.pinnedPeerCertSha256,\n        );\n    }\n\n    toJson() {\n        return {\n            serverName: this.serverName,\n            alpn: this.alpn,\n            fingerprint: this.fingerprint,\n            echConfigList: this.echConfigList,\n            verifyPeerCertByName: this.verifyPeerCertByName,\n            pinnedPeerCertSha256: this.pinnedPeerCertSha256\n        };\n    }\n}\n\nclass RealityStreamSettings extends CommonClass {\n    constructor(\n        publicKey = '',\n        fingerprint = '',\n        serverName = '',\n        shortId = '',\n        spiderX = '',\n        mldsa65Verify = ''\n    ) {\n        super();\n        this.publicKey = publicKey;\n        this.fingerprint = fingerprint;\n        this.serverName = serverName;\n        this.shortId = shortId\n        this.spiderX = spiderX;\n        this.mldsa65Verify = mldsa65Verify;\n    }\n    static fromJson(json = {}) {\n        return new RealityStreamSettings(\n            json.publicKey,\n            json.fingerprint,\n            json.serverName,\n            json.shortId,\n            json.spiderX,\n            json.mldsa65Verify\n        );\n    }\n    toJson() {\n        return {\n            publicKey: this.publicKey,\n            fingerprint: this.fingerprint,\n            serverName: this.serverName,\n            shortId: this.shortId,\n            spiderX: this.spiderX,\n            mldsa65Verify: this.mldsa65Verify\n        };\n    }\n};\n\nclass HysteriaStreamSettings extends CommonClass {\n    constructor(\n        version = 2,\n        auth = '',\n        congestion = '',\n        up = '0',\n        down = '0',\n        udphopPort = '',\n        udphopIntervalMin = 30,\n        udphopIntervalMax = 30,\n        initStreamReceiveWindow = 8388608,\n        maxStreamReceiveWindow = 8388608,\n        initConnectionReceiveWindow = 20971520,\n        maxConnectionReceiveWindow = 20971520,\n        maxIdleTimeout = 30,\n        keepAlivePeriod = 0,\n        disablePathMTUDiscovery = false\n    ) {\n        super();\n        this.version = version;\n        this.auth = auth;\n        this.congestion = congestion;\n        this.up = up;\n        this.down = down;\n        this.udphopPort = udphopPort;\n        this.udphopIntervalMin = udphopIntervalMin;\n        this.udphopIntervalMax = udphopIntervalMax;\n        this.initStreamReceiveWindow = initStreamReceiveWindow;\n        this.maxStreamReceiveWindow = maxStreamReceiveWindow;\n        this.initConnectionReceiveWindow = initConnectionReceiveWindow;\n        this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;\n        this.maxIdleTimeout = maxIdleTimeout;\n        this.keepAlivePeriod = keepAlivePeriod;\n        this.disablePathMTUDiscovery = disablePathMTUDiscovery;\n    }\n\n    static fromJson(json = {}) {\n        let udphopPort = '';\n        let udphopIntervalMin = 30;\n        let udphopIntervalMax = 30;\n        if (json.udphop) {\n            udphopPort = json.udphop.port || '';\n            // Backward compatibility: if old 'interval' exists, use it for both min/max\n            if (json.udphop.interval !== undefined) {\n                udphopIntervalMin = json.udphop.interval;\n                udphopIntervalMax = json.udphop.interval;\n            } else {\n                udphopIntervalMin = json.udphop.intervalMin || 30;\n                udphopIntervalMax = json.udphop.intervalMax || 30;\n            }\n        }\n        return new HysteriaStreamSettings(\n            json.version,\n            json.auth,\n            json.congestion,\n            json.up,\n            json.down,\n            udphopPort,\n            udphopIntervalMin,\n            udphopIntervalMax,\n            json.initStreamReceiveWindow,\n            json.maxStreamReceiveWindow,\n            json.initConnectionReceiveWindow,\n            json.maxConnectionReceiveWindow,\n            json.maxIdleTimeout,\n            json.keepAlivePeriod,\n            json.disablePathMTUDiscovery\n        );\n    }\n\n    toJson() {\n        const result = {\n            version: this.version,\n            auth: this.auth,\n            congestion: this.congestion,\n            up: this.up,\n            down: this.down,\n            initStreamReceiveWindow: this.initStreamReceiveWindow,\n            maxStreamReceiveWindow: this.maxStreamReceiveWindow,\n            initConnectionReceiveWindow: this.initConnectionReceiveWindow,\n            maxConnectionReceiveWindow: this.maxConnectionReceiveWindow,\n            maxIdleTimeout: this.maxIdleTimeout,\n            keepAlivePeriod: this.keepAlivePeriod,\n            disablePathMTUDiscovery: this.disablePathMTUDiscovery\n        };\n        if (this.udphopPort) {\n            result.udphop = {\n                port: this.udphopPort,\n                intervalMin: this.udphopIntervalMin,\n                intervalMax: this.udphopIntervalMax\n            };\n        }\n        return result;\n    }\n};\nclass SockoptStreamSettings extends CommonClass {\n    constructor(\n        dialerProxy = \"\",\n        tcpFastOpen = false,\n        tcpKeepAliveInterval = 0,\n        tcpMptcp = false,\n        penetrate = false,\n        addressPortStrategy = Address_Port_Strategy.NONE,\n        trustedXForwardedFor = [],\n    ) {\n        super();\n        this.dialerProxy = dialerProxy;\n        this.tcpFastOpen = tcpFastOpen;\n        this.tcpKeepAliveInterval = tcpKeepAliveInterval;\n        this.tcpMptcp = tcpMptcp;\n        this.penetrate = penetrate;\n        this.addressPortStrategy = addressPortStrategy;\n        this.trustedXForwardedFor = trustedXForwardedFor;\n    }\n\n    static fromJson(json = {}) {\n        if (Object.keys(json).length === 0) return undefined;\n        return new SockoptStreamSettings(\n            json.dialerProxy,\n            json.tcpFastOpen,\n            json.tcpKeepAliveInterval,\n            json.tcpMptcp,\n            json.penetrate,\n            json.addressPortStrategy,\n            json.trustedXForwardedFor || []\n        );\n    }\n\n    toJson() {\n        const result = {\n            dialerProxy: this.dialerProxy,\n            tcpFastOpen: this.tcpFastOpen,\n            tcpKeepAliveInterval: this.tcpKeepAliveInterval,\n            tcpMptcp: this.tcpMptcp,\n            penetrate: this.penetrate,\n            addressPortStrategy: this.addressPortStrategy\n        };\n        if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {\n            result.trustedXForwardedFor = this.trustedXForwardedFor;\n        }\n        return result;\n    }\n}\n\nclass UdpMask extends CommonClass {\n    constructor(type = 'salamander', settings = {}) {\n        super();\n        this.type = type;\n        this.settings = this._getDefaultSettings(type, settings);\n    }\n\n    _getDefaultSettings(type, settings = {}) {\n        switch (type) {\n            case 'salamander':\n            case 'mkcp-aes128gcm':\n                return { password: settings.password || '' };\n            case 'header-dns':\n            case 'xdns':\n                return { domain: settings.domain || '' };\n            case 'mkcp-original':\n            case 'header-dtls':\n            case 'header-srtp':\n            case 'header-utp':\n            case 'header-wechat':\n            case 'header-wireguard':\n                return {}; // No settings needed\n            default:\n                return settings;\n        }\n    }\n\n    static fromJson(json = {}) {\n        return new UdpMask(\n            json.type || 'salamander',\n            json.settings || {}\n        );\n    }\n\n    toJson() {\n        return {\n            type: this.type,\n            settings: (this.settings && Object.keys(this.settings).length > 0) ? this.settings : undefined\n        };\n    }\n}\n\nclass FinalMaskStreamSettings extends CommonClass {\n    constructor(udp = []) {\n        super();\n        this.udp = Array.isArray(udp) ? udp.map(u => new UdpMask(u.type, u.settings)) : [new UdpMask(udp.type, udp.settings)];\n    }\n\n    static fromJson(json = {}) {\n        return new FinalMaskStreamSettings(json.udp || []);\n    }\n\n    toJson() {\n        return {\n            udp: this.udp.map(udp => udp.toJson())\n        };\n\n    }\n}\n\nclass StreamSettings extends CommonClass {\n    constructor(\n        network = 'tcp',\n        security = 'none',\n        tlsSettings = new TlsStreamSettings(),\n        realitySettings = new RealityStreamSettings(),\n        tcpSettings = new TcpStreamSettings(),\n        kcpSettings = new KcpStreamSettings(),\n        wsSettings = new WsStreamSettings(),\n        grpcSettings = new GrpcStreamSettings(),\n        httpupgradeSettings = new HttpUpgradeStreamSettings(),\n        xhttpSettings = new xHTTPStreamSettings(),\n        hysteriaSettings = new HysteriaStreamSettings(),\n        finalmask = new FinalMaskStreamSettings(),\n        sockopt = undefined,\n    ) {\n        super();\n        this.network = network;\n        this.security = security;\n        this.tls = tlsSettings;\n        this.reality = realitySettings;\n        this.tcp = tcpSettings;\n        this.kcp = kcpSettings;\n        this.ws = wsSettings;\n        this.grpc = grpcSettings;\n        this.httpupgrade = httpupgradeSettings;\n        this.xhttp = xhttpSettings;\n        this.hysteria = hysteriaSettings;\n        this.finalmask = finalmask;\n        this.sockopt = sockopt;\n    }\n\n    addUdpMask(type = 'salamander') {\n        this.finalmask.udp.push(new UdpMask(type));\n    }\n\n    delUdpMask(index) {\n        if (this.finalmask.udp) {\n            this.finalmask.udp.splice(index, 1);\n        }\n    }\n\n    get hasFinalMask() {\n        return this.finalmask.udp && this.finalmask.udp.length > 0;\n    }\n\n    get isTls() {\n        return this.security === 'tls';\n    }\n\n    get isReality() {\n        return this.security === \"reality\";\n    }\n\n    get sockoptSwitch() {\n        return this.sockopt != undefined;\n    }\n\n    set sockoptSwitch(value) {\n        this.sockopt = value ? new SockoptStreamSettings() : undefined;\n    }\n\n    static fromJson(json = {}) {\n        return new StreamSettings(\n            json.network,\n            json.security,\n            TlsStreamSettings.fromJson(json.tlsSettings),\n            RealityStreamSettings.fromJson(json.realitySettings),\n            TcpStreamSettings.fromJson(json.tcpSettings),\n            KcpStreamSettings.fromJson(json.kcpSettings),\n            WsStreamSettings.fromJson(json.wsSettings),\n            GrpcStreamSettings.fromJson(json.grpcSettings),\n            HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),\n            xHTTPStreamSettings.fromJson(json.xhttpSettings),\n            HysteriaStreamSettings.fromJson(json.hysteriaSettings),\n            FinalMaskStreamSettings.fromJson(json.finalmask),\n            SockoptStreamSettings.fromJson(json.sockopt),\n        );\n    }\n\n    toJson() {\n        const network = this.network;\n        return {\n            network: network,\n            security: this.security,\n            tlsSettings: this.security == 'tls' ? this.tls.toJson() : undefined,\n            realitySettings: this.security == 'reality' ? this.reality.toJson() : undefined,\n            tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,\n            kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,\n            wsSettings: network === 'ws' ? this.ws.toJson() : undefined,\n            grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,\n            httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,\n            xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,\n            hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,\n            finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,\n            sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,\n        };\n    }\n}\n\nclass Mux extends CommonClass {\n    constructor(enabled = false, concurrency = 8, xudpConcurrency = 16, xudpProxyUDP443 = \"reject\") {\n        super();\n        this.enabled = enabled;\n        this.concurrency = concurrency;\n        this.xudpConcurrency = xudpConcurrency;\n        this.xudpProxyUDP443 = xudpProxyUDP443;\n    }\n\n    static fromJson(json = {}) {\n        if (Object.keys(json).length === 0) return undefined;\n        return new Mux(\n            json.enabled,\n            json.concurrency,\n            json.xudpConcurrency,\n            json.xudpProxyUDP443,\n        );\n    }\n\n    toJson() {\n        return {\n            enabled: this.enabled,\n            concurrency: this.concurrency,\n            xudpConcurrency: this.xudpConcurrency,\n            xudpProxyUDP443: this.xudpProxyUDP443,\n        };\n    }\n}\n\nclass Outbound extends CommonClass {\n    constructor(\n        tag = '',\n        protocol = Protocols.VLESS,\n        settings = null,\n        streamSettings = new StreamSettings(),\n        sendThrough,\n        mux = new Mux(),\n    ) {\n        super();\n        this.tag = tag;\n        this._protocol = protocol;\n        this.settings = settings == null ? Outbound.Settings.getSettings(protocol) : settings;\n        this.stream = streamSettings;\n        this.sendThrough = sendThrough;\n        this.mux = mux;\n    }\n\n    get protocol() {\n        return this._protocol;\n    }\n\n    set protocol(protocol) {\n        this._protocol = protocol;\n        this.settings = Outbound.Settings.getSettings(protocol);\n        this.stream = new StreamSettings();\n    }\n\n    canEnableTls() {\n        if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol)) return false;\n        if (this.protocol === Protocols.Hysteria) return this.stream.network === 'hysteria';\n        return [\"tcp\", \"ws\", \"http\", \"grpc\", \"httpupgrade\", \"xhttp\"].includes(this.stream.network);\n    }\n\n    //this is used for xtls-rprx-vision\n    canEnableTlsFlow() {\n        if ((this.stream.security != 'none') && (this.stream.network === \"tcp\")) {\n            return this.protocol === Protocols.VLESS;\n        }\n        return false;\n    }\n\n    // Vision seed applies only when vision flow is selected\n    canEnableVisionSeed() {\n        if (!this.canEnableTlsFlow()) return false;\n        const flow = this.settings?.flow;\n        return flow === TLS_FLOW_CONTROL.VISION || flow === TLS_FLOW_CONTROL.VISION_UDP443;\n    }\n\n    canEnableReality() {\n        if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;\n        return [\"tcp\", \"http\", \"grpc\", \"xhttp\"].includes(this.stream.network);\n    }\n\n    canEnableStream() {\n        return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol);\n    }\n\n    canEnableMux() {\n        // Disable Mux if flow is set\n        if (this.settings.flow && this.settings.flow !== '') {\n            this.mux.enabled = false;\n            return false;\n        }\n\n        // Disable Mux if network is xhttp\n        if (this.stream.network === 'xhttp') {\n            this.mux.enabled = false;\n            return false;\n        }\n\n        // Allow Mux only for these protocols\n        return [\n            Protocols.VMess,\n            Protocols.VLESS,\n            Protocols.Trojan,\n            Protocols.Shadowsocks,\n            Protocols.HTTP,\n            Protocols.Socks\n        ].includes(this.protocol);\n    }\n\n    hasServers() {\n        return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);\n    }\n\n    hasAddressPort() {\n        return [\n            Protocols.DNS,\n            Protocols.VMess,\n            Protocols.VLESS,\n            Protocols.Trojan,\n            Protocols.Shadowsocks,\n            Protocols.Socks,\n            Protocols.HTTP,\n            Protocols.Hysteria\n        ].includes(this.protocol);\n    }\n\n    hasUsername() {\n        return [Protocols.Socks, Protocols.HTTP].includes(this.protocol);\n    }\n\n    static fromJson(json = {}) {\n        return new Outbound(\n            json.tag,\n            json.protocol,\n            Outbound.Settings.fromJson(json.protocol, json.settings),\n            StreamSettings.fromJson(json.streamSettings),\n            json.sendThrough,\n            Mux.fromJson(json.mux),\n        )\n    }\n\n    toJson() {\n        var stream;\n        if (this.canEnableStream()) {\n            stream = this.stream.toJson();\n        } else {\n            if (this.stream?.sockopt)\n                stream = { sockopt: this.stream.sockopt.toJson() };\n        }\n        let settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;\n        return {\n            protocol: this.protocol,\n            settings: settingsOut,\n            // Only include tag, streamSettings, sendThrough, mux if present and not empty\n            ...(this.tag ? { tag: this.tag } : {}),\n            ...(stream ? { streamSettings: stream } : {}),\n            ...(this.sendThrough ? { sendThrough: this.sendThrough } : {}),\n            ...(this.mux?.enabled ? { mux: this.mux } : {}),\n        };\n    }\n\n    static fromLink(link) {\n        data = link.split('://');\n        if (data.length != 2) return null;\n        switch (data[0].toLowerCase()) {\n            case Protocols.VMess:\n                return this.fromVmessLink(JSON.parse(Base64.decode(data[1])));\n            case Protocols.VLESS:\n            case Protocols.Trojan:\n            case 'ss':\n                return this.fromParamLink(link);\n            case 'hysteria2':\n            case Protocols.Hysteria:\n                return this.fromHysteriaLink(link);\n            default:\n                return null;\n        }\n    }\n\n    static fromVmessLink(json = {}) {\n        let stream = new StreamSettings(json.net, json.tls);\n\n        let network = json.net;\n        if (network === 'tcp') {\n            stream.tcp = new TcpStreamSettings(\n                json.type,\n                json.host ?? '',\n                json.path ?? '');\n        } else if (network === 'kcp') {\n            stream.kcp = new KcpStreamSettings();\n            stream.type = json.type;\n            stream.seed = json.path;\n        } else if (network === 'ws') {\n            stream.ws = new WsStreamSettings(json.path, json.host);\n        } else if (network === 'grpc') {\n            stream.grpc = new GrpcStreamSettings(json.path, json.authority, json.type == 'multi');\n        } else if (network === 'httpupgrade') {\n            stream.httpupgrade = new HttpUpgradeStreamSettings(json.path, json.host);\n        } else if (network === 'xhttp') {\n            stream.xhttp = new xHTTPStreamSettings(json.path, json.host, json.mode);\n        }\n\n        if (json.tls && json.tls == 'tls') {\n            stream.tls = new TlsStreamSettings(\n                json.sni,\n                json.alpn ? json.alpn.split(',') : [],\n                json.fp);\n        }\n\n        const port = json.port * 1;\n\n        return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, port, json.id, json.scy), stream);\n    }\n\n    static fromParamLink(link) {\n        const url = new URL(link);\n        let type = url.searchParams.get('type') ?? 'tcp';\n        let security = url.searchParams.get('security') ?? 'none';\n        let stream = new StreamSettings(type, security);\n\n        let headerType = url.searchParams.get('headerType') ?? undefined;\n        let host = url.searchParams.get('host') ?? undefined;\n        let path = url.searchParams.get('path') ?? undefined;\n        let mode = url.searchParams.get('mode') ?? undefined;\n\n        if (type === 'tcp' || type === 'none') {\n            stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path);\n        } else if (type === 'kcp') {\n            stream.kcp = new KcpStreamSettings();\n            stream.kcp.type = headerType ?? 'none';\n            stream.kcp.seed = path;\n        } else if (type === 'ws') {\n            stream.ws = new WsStreamSettings(path, host);\n        } else if (type === 'grpc') {\n            stream.grpc = new GrpcStreamSettings(\n                url.searchParams.get('serviceName') ?? '',\n                url.searchParams.get('authority') ?? '',\n                url.searchParams.get('mode') == 'multi');\n        } else if (type === 'httpupgrade') {\n            stream.httpupgrade = new HttpUpgradeStreamSettings(path, host);\n        } else if (type === 'xhttp') {\n            stream.xhttp = new xHTTPStreamSettings(path, host, mode);\n        }\n\n        if (security == 'tls') {\n            let fp = url.searchParams.get('fp') ?? 'none';\n            let alpn = url.searchParams.get('alpn');\n            let sni = url.searchParams.get('sni') ?? '';\n            let ech = url.searchParams.get('ech') ?? '';\n            stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, ech);\n        }\n\n        if (security == 'reality') {\n            let pbk = url.searchParams.get('pbk');\n            let fp = url.searchParams.get('fp');\n            let sni = url.searchParams.get('sni') ?? '';\n            let sid = url.searchParams.get('sid') ?? '';\n            let spx = url.searchParams.get('spx') ?? '';\n            let pqv = url.searchParams.get('pqv') ?? '';\n            stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx, pqv);\n        }\n\n        const regex = /([^@]+):\\/\\/([^@]+)@(.+):(\\d+)(.*)$/;\n        const match = link.match(regex);\n\n        if (!match) return null;\n        let [, protocol, userData, address, port,] = match;\n        port *= 1;\n        if (protocol == 'ss') {\n            protocol = 'shadowsocks';\n            userData = atob(userData).split(':');\n        }\n        var settings;\n        switch (protocol) {\n            case Protocols.VLESS:\n                settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none');\n                break;\n            case Protocols.Trojan:\n                settings = new Outbound.TrojanSettings(address, port, userData);\n                break;\n            case Protocols.Shadowsocks:\n                let method = userData.splice(0, 1)[0];\n                settings = new Outbound.ShadowsocksSettings(address, port, userData.join(\":\"), method, true);\n                break;\n            default:\n                return null;\n        }\n        let remark = decodeURIComponent(url.hash);\n        // Remove '#' from url.hash\n        remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;\n        return new Outbound(remark, protocol, settings, stream);\n    }\n\n    static fromHysteriaLink(link) {\n        // Parse hysteria2://password@address:port[?param1=value1&param2=value2...][#remarks]\n        const regex = /^hysteria2?:\\/\\/([^@]+)@([^:?#]+):(\\d+)([^#]*)(#.*)?$/;\n        const match = link.match(regex);\n\n        if (!match) return null;\n\n        let [, password, address, port, params, hash] = match;\n        port = parseInt(port);\n\n        // Parse URL parameters if present\n        let urlParams = new URLSearchParams(params);\n\n        // Create stream settings with hysteria network\n        let stream = new StreamSettings('hysteria', 'none');\n\n        // Set hysteria stream settings\n        stream.hysteria.auth = password;\n        stream.hysteria.congestion = urlParams.get('congestion') ?? '';\n        stream.hysteria.up = urlParams.get('up') ?? '0';\n        stream.hysteria.down = urlParams.get('down') ?? '0';\n        stream.hysteria.udphopPort = urlParams.get('udphopPort') ?? '';\n        // Support both old single interval and new min/max range\n        if (urlParams.has('udphopInterval')) {\n            const interval = parseInt(urlParams.get('udphopInterval'));\n            stream.hysteria.udphopIntervalMin = interval;\n            stream.hysteria.udphopIntervalMax = interval;\n        } else {\n            stream.hysteria.udphopIntervalMin = parseInt(urlParams.get('udphopIntervalMin') ?? '30');\n            stream.hysteria.udphopIntervalMax = parseInt(urlParams.get('udphopIntervalMax') ?? '30');\n        }\n\n        // Optional QUIC parameters\n        if (urlParams.has('initStreamReceiveWindow')) {\n            stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow'));\n        }\n        if (urlParams.has('maxStreamReceiveWindow')) {\n            stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow'));\n        }\n        if (urlParams.has('initConnectionReceiveWindow')) {\n            stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow'));\n        }\n        if (urlParams.has('maxConnectionReceiveWindow')) {\n            stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow'));\n        }\n        if (urlParams.has('maxIdleTimeout')) {\n            stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout'));\n        }\n        if (urlParams.has('keepAlivePeriod')) {\n            stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod'));\n        }\n        if (urlParams.has('disablePathMTUDiscovery')) {\n            stream.hysteria.disablePathMTUDiscovery = urlParams.get('disablePathMTUDiscovery') === 'true';\n        }\n\n        // Create settings\n        let settings = new Outbound.HysteriaSettings(address, port, 2);\n\n        // Extract remark from hash\n        let remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`;\n\n        return new Outbound(remark, Protocols.Hysteria, settings, stream);\n    }\n}\n\nOutbound.Settings = class extends CommonClass {\n    constructor(protocol) {\n        super();\n        this.protocol = protocol;\n    }\n\n    static getSettings(protocol) {\n        switch (protocol) {\n            case Protocols.Freedom: return new Outbound.FreedomSettings();\n            case Protocols.Blackhole: return new Outbound.BlackholeSettings();\n            case Protocols.DNS: return new Outbound.DNSSettings();\n            case Protocols.VMess: return new Outbound.VmessSettings();\n            case Protocols.VLESS: return new Outbound.VLESSSettings();\n            case Protocols.Trojan: return new Outbound.TrojanSettings();\n            case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings();\n            case Protocols.Socks: return new Outbound.SocksSettings();\n            case Protocols.HTTP: return new Outbound.HttpSettings();\n            case Protocols.Wireguard: return new Outbound.WireguardSettings();\n            case Protocols.Hysteria: return new Outbound.HysteriaSettings();\n            default: return null;\n        }\n    }\n\n    static fromJson(protocol, json) {\n        switch (protocol) {\n            case Protocols.Freedom: return Outbound.FreedomSettings.fromJson(json);\n            case Protocols.Blackhole: return Outbound.BlackholeSettings.fromJson(json);\n            case Protocols.DNS: return Outbound.DNSSettings.fromJson(json);\n            case Protocols.VMess: return Outbound.VmessSettings.fromJson(json);\n            case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json);\n            case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json);\n            case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json);\n            case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);\n            case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);\n            case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);\n            case Protocols.Hysteria: return Outbound.HysteriaSettings.fromJson(json);\n            default: return null;\n        }\n    }\n\n    toJson() {\n        return {};\n    }\n};\nOutbound.FreedomSettings = class extends CommonClass {\n    constructor(\n        domainStrategy = '',\n        redirect = '',\n        fragment = {},\n        noises = []\n    ) {\n        super();\n        this.domainStrategy = domainStrategy;\n        this.redirect = redirect;\n        this.fragment = fragment;\n        this.noises = noises;\n    }\n\n    addNoise() {\n        this.noises.push(new Outbound.FreedomSettings.Noise());\n    }\n\n    delNoise(index) {\n        this.noises.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new Outbound.FreedomSettings(\n            json.domainStrategy,\n            json.redirect,\n            json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : undefined,\n            json.noises ? json.noises.map(noise => Outbound.FreedomSettings.Noise.fromJson(noise)) : undefined,\n        );\n    }\n\n    toJson() {\n        return {\n            domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,\n            redirect: ObjectUtil.isEmpty(this.redirect) ? undefined : this.redirect,\n            fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,\n            noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),\n        };\n    }\n};\n\nOutbound.FreedomSettings.Fragment = class extends CommonClass {\n    constructor(\n        packets = '1-3',\n        length = '',\n        interval = '',\n        maxSplit = ''\n    ) {\n        super();\n        this.packets = packets;\n        this.length = length;\n        this.interval = interval;\n        this.maxSplit = maxSplit;\n    }\n\n    static fromJson(json = {}) {\n        return new Outbound.FreedomSettings.Fragment(\n            json.packets,\n            json.length,\n            json.interval,\n            json.maxSplit\n        );\n    }\n};\n\nOutbound.FreedomSettings.Noise = class extends CommonClass {\n    constructor(\n        type = 'rand',\n        packet = '10-20',\n        delay = '10-16',\n        applyTo = 'ip'\n    ) {\n        super();\n        this.type = type;\n        this.packet = packet;\n        this.delay = delay;\n        this.applyTo = applyTo;\n    }\n\n    static fromJson(json = {}) {\n        return new Outbound.FreedomSettings.Noise(\n            json.type,\n            json.packet,\n            json.delay,\n            json.applyTo\n        );\n    }\n\n    toJson() {\n        return {\n            type: this.type,\n            packet: this.packet,\n            delay: this.delay,\n            applyTo: this.applyTo\n        };\n    }\n};\n\nOutbound.BlackholeSettings = class extends CommonClass {\n    constructor(type) {\n        super();\n        this.type = type;\n    }\n\n    static fromJson(json = {}) {\n        return new Outbound.BlackholeSettings(\n            json.response ? json.response.type : undefined,\n        );\n    }\n\n    toJson() {\n        return {\n            response: ObjectUtil.isEmpty(this.type) ? undefined : { type: this.type },\n        };\n    }\n};\nOutbound.DNSSettings = class extends CommonClass {\n    constructor(\n        network = 'udp',\n        address = '',\n        port = 53,\n        nonIPQuery = 'reject',\n        blockTypes = []\n    ) {\n        super();\n        this.network = network;\n        this.address = address;\n        this.port = port;\n        this.nonIPQuery = nonIPQuery;\n        this.blockTypes = blockTypes;\n    }\n\n    static fromJson(json = {}) {\n        return new Outbound.DNSSettings(\n            json.network,\n            json.address,\n            json.port,\n            json.nonIPQuery,\n            json.blockTypes,\n        );\n    }\n};\nOutbound.VmessSettings = class extends CommonClass {\n    constructor(address, port, id, security) {\n        super();\n        this.address = address;\n        this.port = port;\n        this.id = id;\n        this.security = security;\n    }\n\n    static fromJson(json = {}) {\n        if (!ObjectUtil.isArrEmpty(json.vnext)) {\n            const v = json.vnext[0] || {};\n            const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];\n            return new Outbound.VmessSettings(\n                v.address,\n                v.port,\n                u.id,\n                u.security,\n            );\n        }\n    }\n\n    toJson() {\n        return {\n            vnext: [{\n                address: this.address,\n                port: this.port,\n                users: [{\n                    id: this.id,\n                    security: this.security\n                }]\n            }]\n        };\n    }\n};\nOutbound.VLESSSettings = class extends CommonClass {\n    constructor(address, port, id, flow, encryption, testpre = 0, testseed = [900, 500, 900, 256]) {\n        super();\n        this.address = address;\n        this.port = port;\n        this.id = id;\n        this.flow = flow;\n        this.encryption = encryption;\n        this.testpre = testpre;\n        this.testseed = testseed;\n    }\n\n    static fromJson(json = {}) {\n        if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();\n        return new Outbound.VLESSSettings(\n            json.address,\n            json.port,\n            json.id,\n            json.flow,\n            json.encryption,\n            json.testpre || 0,\n            json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256]\n        );\n    }\n\n    toJson() {\n        const result = {\n            address: this.address,\n            port: this.port,\n            id: this.id,\n            flow: this.flow,\n            encryption: this.encryption,\n        };\n        // Only include Vision settings when flow is set\n        if (this.flow && this.flow !== '') {\n            if (this.testpre > 0) {\n                result.testpre = this.testpre;\n            }\n            if (this.testseed && this.testseed.length >= 4) {\n                result.testseed = this.testseed;\n            }\n        }\n        return result;\n    }\n};\nOutbound.TrojanSettings = class extends CommonClass {\n    constructor(address, port, password) {\n        super();\n        this.address = address;\n        this.port = port;\n        this.password = password;\n    }\n\n    static fromJson(json = {}) {\n        if (ObjectUtil.isArrEmpty(json.servers)) return new Outbound.TrojanSettings();\n        return new Outbound.TrojanSettings(\n            json.servers[0].address,\n            json.servers[0].port,\n            json.servers[0].password,\n        );\n    }\n\n    toJson() {\n        return {\n            servers: [{\n                address: this.address,\n                port: this.port,\n                password: this.password,\n            }],\n        };\n    }\n};\nOutbound.ShadowsocksSettings = class extends CommonClass {\n    constructor(address, port, password, method, uot, UoTVersion) {\n        super();\n        this.address = address;\n        this.port = port;\n        this.password = password;\n        this.method = method;\n        this.uot = uot;\n        this.UoTVersion = UoTVersion;\n    }\n\n    static fromJson(json = {}) {\n        let servers = json.servers;\n        if (ObjectUtil.isArrEmpty(servers)) servers = [{}];\n        return new Outbound.ShadowsocksSettings(\n            servers[0].address,\n            servers[0].port,\n            servers[0].password,\n            servers[0].method,\n            servers[0].uot,\n            servers[0].UoTVersion,\n        );\n    }\n\n    toJson() {\n        return {\n            servers: [{\n                address: this.address,\n                port: this.port,\n                password: this.password,\n                method: this.method,\n                uot: this.uot,\n                UoTVersion: this.UoTVersion,\n            }],\n        };\n    }\n};\n\nOutbound.SocksSettings = class extends CommonClass {\n    constructor(address, port, user, pass) {\n        super();\n        this.address = address;\n        this.port = port;\n        this.user = user;\n        this.pass = pass;\n    }\n\n    static fromJson(json = {}) {\n        let servers = json.servers;\n        if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }];\n        return new Outbound.SocksSettings(\n            servers[0].address,\n            servers[0].port,\n            ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,\n            ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,\n        );\n    }\n\n    toJson() {\n        return {\n            servers: [{\n                address: this.address,\n                port: this.port,\n                users: ObjectUtil.isEmpty(this.user) ? [] : [{ user: this.user, pass: this.pass }],\n            }],\n        };\n    }\n};\nOutbound.HttpSettings = class extends CommonClass {\n    constructor(address, port, user, pass) {\n        super();\n        this.address = address;\n        this.port = port;\n        this.user = user;\n        this.pass = pass;\n    }\n\n    static fromJson(json = {}) {\n        let servers = json.servers;\n        if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }];\n        return new Outbound.HttpSettings(\n            servers[0].address,\n            servers[0].port,\n            ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,\n            ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,\n        );\n    }\n\n    toJson() {\n        return {\n            servers: [{\n                address: this.address,\n                port: this.port,\n                users: ObjectUtil.isEmpty(this.user) ? [] : [{ user: this.user, pass: this.pass }],\n            }],\n        };\n    }\n};\n\nOutbound.WireguardSettings = class extends CommonClass {\n    constructor(\n        mtu = 1420,\n        secretKey = '',\n        address = [''],\n        workers = 2,\n        domainStrategy = '',\n        reserved = '',\n        peers = [new Outbound.WireguardSettings.Peer()],\n        noKernelTun = false,\n    ) {\n        super();\n        this.mtu = mtu;\n        this.secretKey = secretKey;\n        this.pubKey = secretKey.length > 0 ? Wireguard.generateKeypair(secretKey).publicKey : '';\n        this.address = Array.isArray(address) ? address.join(',') : address;\n        this.workers = workers;\n        this.domainStrategy = domainStrategy;\n        this.reserved = Array.isArray(reserved) ? reserved.join(',') : reserved;\n        this.peers = peers;\n        this.noKernelTun = noKernelTun;\n    }\n\n    addPeer() {\n        this.peers.push(new Outbound.WireguardSettings.Peer());\n    }\n\n    delPeer(index) {\n        this.peers.splice(index, 1);\n    }\n\n    static fromJson(json = {}) {\n        return new Outbound.WireguardSettings(\n            json.mtu,\n            json.secretKey,\n            json.address,\n            json.workers,\n            json.domainStrategy,\n            json.reserved,\n            json.peers.map(peer => Outbound.WireguardSettings.Peer.fromJson(peer)),\n            json.noKernelTun,\n        );\n    }\n\n    toJson() {\n        return {\n            mtu: this.mtu ?? undefined,\n            secretKey: this.secretKey,\n            address: this.address ? this.address.split(\",\") : [],\n            workers: this.workers ?? undefined,\n            domainStrategy: WireguardDomainStrategy.includes(this.domainStrategy) ? this.domainStrategy : undefined,\n            reserved: this.reserved ? this.reserved.split(\",\").map(Number) : undefined,\n            peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers),\n            noKernelTun: this.noKernelTun,\n        };\n    }\n};\n\nOutbound.WireguardSettings.Peer = class extends CommonClass {\n    constructor(\n        publicKey = '',\n        psk = '',\n        allowedIPs = ['0.0.0.0/0', '::/0'],\n        endpoint = '',\n        keepAlive = 0\n    ) {\n        super();\n        this.publicKey = publicKey;\n        this.psk = psk;\n        this.allowedIPs = allowedIPs;\n        this.endpoint = endpoint;\n        this.keepAlive = keepAlive;\n    }\n\n    static fromJson(json = {}) {\n        return new Outbound.WireguardSettings.Peer(\n            json.publicKey,\n            json.preSharedKey,\n            json.allowedIPs,\n            json.endpoint,\n            json.keepAlive\n        );\n    }\n\n    toJson() {\n        return {\n            publicKey: this.publicKey,\n            preSharedKey: this.psk.length > 0 ? this.psk : undefined,\n            allowedIPs: this.allowedIPs ? this.allowedIPs : undefined,\n            endpoint: this.endpoint,\n            keepAlive: this.keepAlive ?? undefined,\n        };\n    }\n};\n\nOutbound.HysteriaSettings = class extends CommonClass {\n    constructor(address = '', port = 443, version = 2) {\n        super();\n        this.address = address;\n        this.port = port;\n        this.version = version;\n    }\n\n    static fromJson(json = {}) {\n        if (Object.keys(json).length === 0) return new Outbound.HysteriaSettings();\n        return new Outbound.HysteriaSettings(\n            json.address,\n            json.port,\n            json.version\n        );\n    }\n\n    toJson() {\n        return {\n            address: this.address,\n            port: this.port,\n            version: this.version\n        };\n    }\n};"
  },
  {
    "path": "web/assets/js/model/reality_targets.js",
    "content": "// List of popular services for VLESS Reality Target/SNI randomization\r\nconst REALITY_TARGETS = [\r\n    { target: 'www.apple.com:443', sni: 'www.apple.com' },\r\n    { target: 'www.icloud.com:443', sni: 'www.icloud.com' },\r\n    { target: 'www.amazon.com:443', sni: 'www.amazon.com' },\r\n    { target: 'aws.amazon.com:443', sni: 'aws.amazon.com' },\r\n    { target: 'www.oracle.com:443', sni: 'www.oracle.com' },\r\n    { target: 'www.nvidia.com:443', sni: 'www.nvidia.com' },\r\n    { target: 'www.amd.com:443', sni: 'www.amd.com' },\r\n    { target: 'www.intel.com:443', sni: 'www.intel.com' },\r\n    { target: 'www.tesla.com:443', sni: 'www.tesla.com' },\r\n    { target: 'www.sony.com:443', sni: 'www.sony.com' }\r\n];\r\n\r\n/**\r\n * Returns a random Reality target configuration from the predefined list\r\n * @returns {Object} Object with target and sni properties\r\n */\r\nfunction getRandomRealityTarget() {\r\n    const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length);\r\n    const selected = REALITY_TARGETS[randomIndex];\r\n    // Return a copy to avoid reference issues\r\n    return {\r\n        target: selected.target,\r\n        sni: selected.sni\r\n    };\r\n}\r\n"
  },
  {
    "path": "web/assets/js/model/setting.js",
    "content": "class AllSetting {\n\n    constructor(data) {\n        this.webListen = \"\";\n        this.webDomain = \"\";\n        this.webPort = 2053;\n        this.webCertFile = \"\";\n        this.webKeyFile = \"\";\n        this.webBasePath = \"/\";\n        this.sessionMaxAge = 360;\n        this.pageSize = 25;\n        this.expireDiff = 0;\n        this.trafficDiff = 0;\n        this.remarkModel = \"-ieo\";\n        this.datepicker = \"gregorian\";\n        this.tgBotEnable = false;\n        this.tgBotToken = \"\";\n        this.tgBotProxy = \"\";\n        this.tgBotAPIServer = \"\";\n        this.tgBotChatId = \"\";\n        this.tgRunTime = \"@daily\";\n        this.tgBotBackup = false;\n        this.tgBotLoginNotify = true;\n        this.tgCpu = 80;\n        this.tgLang = \"en-US\";\n        this.twoFactorEnable = false;\n        this.twoFactorToken = \"\";\n        this.xrayTemplateConfig = \"\";\n        this.subEnable = true;\n        this.subJsonEnable = false;\n        this.subTitle = \"\";\n        this.subSupportUrl = \"\";\n        this.subProfileUrl = \"\";\n        this.subAnnounce = \"\";\n        this.subEnableRouting = true;\n        this.subRoutingRules = \"\";\n        this.subListen = \"\";\n        this.subPort = 2096;\n        this.subPath = \"/sub/\";\n        this.subJsonPath = \"/json/\";\n        this.subDomain = \"\";\n        this.externalTrafficInformEnable = false;\n        this.externalTrafficInformURI = \"\";\n        this.subCertFile = \"\";\n        this.subKeyFile = \"\";\n        this.subUpdates = 12;\n        this.subEncrypt = true;\n        this.subShowInfo = true;\n        this.subURI = \"\";\n        this.subJsonURI = \"\";\n        this.subJsonFragment = \"\";\n        this.subJsonNoises = \"\";\n        this.subJsonMux = \"\";\n        this.subJsonRules = \"\";\n\n        this.timeLocation = \"Local\";\n\n        // LDAP settings\n        this.ldapEnable = false;\n        this.ldapHost = \"\";\n        this.ldapPort = 389;\n        this.ldapUseTLS = false;\n        this.ldapBindDN = \"\";\n        this.ldapPassword = \"\";\n        this.ldapBaseDN = \"\";\n        this.ldapUserFilter = \"(objectClass=person)\";\n        this.ldapUserAttr = \"mail\";\n        this.ldapVlessField = \"vless_enabled\";\n        this.ldapSyncCron = \"@every 1m\";\n        this.ldapFlagField = \"\";\n        this.ldapTruthyValues = \"true,1,yes,on\";\n        this.ldapInvertFlag = false;\n        this.ldapInboundTags = \"\";\n        this.ldapAutoCreate = false;\n        this.ldapAutoDelete = false;\n        this.ldapDefaultTotalGB = 0;\n        this.ldapDefaultExpiryDays = 0;\n        this.ldapDefaultLimitIP = 0;\n\n        if (data == null) {\n            return\n        }\n        ObjectUtil.cloneProps(this, data);\n    }\n\n    equals(other) {\n        return ObjectUtil.equals(this, other);\n    }\n}"
  },
  {
    "path": "web/assets/js/subscription.js",
    "content": "(function () {\n  // Vue app for Subscription page\n  const el = document.getElementById('subscription-data');\n  if (!el) return;\n  const textarea = document.getElementById('subscription-links');\n  const rawLinks = (textarea?.value || '').split('\\n').filter(Boolean);\n\n  const data = {\n    sId: el.getAttribute('data-sid') || '',\n    subUrl: el.getAttribute('data-sub-url') || '',\n    subJsonUrl: el.getAttribute('data-subjson-url') || '',\n    download: el.getAttribute('data-download') || '',\n    upload: el.getAttribute('data-upload') || '',\n    used: el.getAttribute('data-used') || '',\n    total: el.getAttribute('data-total') || '',\n    remained: el.getAttribute('data-remained') || '',\n    expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000,\n    lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0),\n    downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0,\n    uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0,\n    totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0,\n    datepicker: el.getAttribute('data-datepicker') || 'gregorian',\n  };\n\n  // Normalize lastOnline to milliseconds if it looks like seconds\n  if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) {\n    data.lastOnlineMs *= 1000;\n  }\n\n  function renderLink(item) {\n    return (\n      Vue.h('a-list-item', {}, [\n        Vue.h('a-space', { props: { size: 'small' } }, [\n          Vue.h('a-button', { props: { size: 'small' }, on: { click: () => copy(item) } }, [Vue.h('a-icon', { props: { type: 'copy' } })]),\n          Vue.h('span', { class: 'break-all' }, item)\n        ])\n      ])\n    );\n  }\n\n  function copy(text) {\n    ClipboardManager.copyText(text).then(ok => {\n      const messageType = ok ? 'success' : 'error';\n      Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed');\n    });\n  }\n\n  function open(url) {\n    window.location.href = url;\n  }\n\n  function drawQR(value) {\n    try {\n      new QRious({ element: document.getElementById('qrcode'), value, size: 220 });\n    } catch (e) {\n      console.warn(e);\n    }\n  }\n\n  // Try to extract a human label (email/ps) from different link types\n  function linkName(link, idx) {\n    try {\n      if (link.startsWith('vmess://')) {\n        const json = JSON.parse(atob(link.replace('vmess://', '')));\n        if (json.ps) return json.ps;\n        if (json.add && json.id) return json.add; // fallback host\n      } else if (link.startsWith('vless://') || link.startsWith('trojan://')) {\n        const hashIdx = link.indexOf('#');\n        if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));\n        const qIdx = link.indexOf('?');\n        if (qIdx !== -1) {\n          const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;\n          if (qs.get('remark')) return qs.get('remark');\n          if (qs.get('email')) return qs.get('email');\n        }\n        const at = link.indexOf('@');\n        const protSep = link.indexOf('://');\n        if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);\n      } else if (link.startsWith('ss://')) {\n        const hashIdx = link.indexOf('#');\n        if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));\n      }\n    } catch (e) { /* ignore and fallback */ }\n    return 'Link ' + (idx + 1);\n  }\n\n  const app = new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#app',\n    data: {\n      themeSwitcher,\n      app: data,\n      links: rawLinks,\n      lang: '',\n      viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024),\n    },\n    async mounted() {\n      this.lang = LanguageManager.getLanguage();\n      const tpl = document.getElementById('subscription-data');\n      const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';\n      if (sj) this.app.subJsonUrl = sj;\n      drawQR(this.app.subUrl);\n      try {\n        const elJson = document.getElementById('qrcode-subjson');\n        if (elJson && this.app.subJsonUrl) {\n          new QRious({ element: elJson, value: this.app.subJsonUrl, size: 220 });\n        }\n      } catch (e) { /* ignore */ }\n      this._onResize = () => { this.viewportWidth = window.innerWidth; };\n      window.addEventListener('resize', this._onResize);\n    },\n    beforeDestroy() {\n      if (this._onResize) window.removeEventListener('resize', this._onResize);\n    },\n    computed: {\n      isMobile() {\n        return this.viewportWidth < 576;\n      },\n      isUnlimited() {\n        return !this.app.totalByte;\n      },\n      isActive() {\n        const now = Date.now();\n        const expiryOk = !this.app.expireMs || this.app.expireMs >= now;\n        const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;\n        return expiryOk && trafficOk;\n      },\n      shadowrocketUrl() {\n        const rawUrl = this.app.subUrl + '?flag=shadowrocket';\n        const base64Url = btoa(rawUrl);\n        const remark = encodeURIComponent(this.app.sId || 'Subscription');\n        return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;\n      },\n      v2boxUrl() {\n        return `v2box://install-sub?url=${encodeURIComponent(this.app.subUrl)}&name=${encodeURIComponent(this.app.sId)}`;\n      },\n      streisandUrl() {\n        return `streisand://import/${encodeURIComponent(this.app.subUrl)}`;\n      },\n      v2raytunUrl() {\n        return this.app.subUrl;\n      },\n      npvtunUrl() {\n        return this.app.subUrl;\n      },\n      happUrl() {\n        return `happ://add/${this.app.subUrl}`;\n      }\n    },\n    methods: {\n      renderLink,\n      copy,\n      open,\n      linkName,\n      i18nLabel(key) {\n        return '{{ i18n \"' + key + '\" }}';\n      },\n    },\n  });\n})();\n"
  },
  {
    "path": "web/assets/js/util/index.js",
    "content": "class Msg {\n    constructor(success = false, msg = \"\", obj = null) {\n        this.success = success;\n        this.msg = msg;\n        this.obj = obj;\n    }\n}\n\nclass HttpUtil {\n    static _handleMsg(msg) {\n        if (!(msg instanceof Msg) || msg.msg === \"\") {\n            return;\n        }\n        const messageType = msg.success ? 'success' : 'error';\n        Vue.prototype.$message[messageType](msg.msg);\n    }\n\n    static _respToMsg(resp) {\n        if (!resp || !resp.data) {\n            return new Msg(false, 'No response data');\n        }\n        const { data } = resp;\n        if (data == null) {\n            return new Msg(true);\n        }\n        if (typeof data === 'object' && 'success' in data) {\n            return new Msg(data.success, data.msg, data.obj);\n        }\n        return typeof data === 'object' ? data : new Msg(false, 'unknown data:', data);\n    }\n\n    static async get(url, params, options = {}) {\n        try {\n            const resp = await axios.get(url, { params, ...options });\n            const msg = this._respToMsg(resp);\n            this._handleMsg(msg);\n            return msg;\n        } catch (error) {\n            console.error('GET request failed:', error);\n            const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');\n            this._handleMsg(errorMsg);\n            return errorMsg;\n        }\n    }\n\n    static async post(url, data, options = {}) {\n        try {\n            const resp = await axios.post(url, data, options);\n            const msg = this._respToMsg(resp);\n            this._handleMsg(msg);\n            return msg;\n        } catch (error) {\n            console.error('POST request failed:', error);\n            const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');\n            this._handleMsg(errorMsg);\n            return errorMsg;\n        }\n    }\n\n    static async postWithModal(url, data, modal) {\n        if (modal) {\n            modal.loading(true);\n        }\n        const msg = await this.post(url, data);\n        if (modal) {\n            modal.loading(false);\n            if (msg instanceof Msg && msg.success) {\n                modal.close();\n            }\n        }\n        return msg;\n    }\n}\n\nclass PromiseUtil {\n    static async sleep(timeout) {\n        await new Promise(resolve => {\n            setTimeout(resolve, timeout)\n        });\n    }\n}\n\nclass RandomUtil {\n    static getSeq({ type = \"default\", hasNumbers = true, hasLowercase = true, hasUppercase = true } = {}) {\n        let seq = '';\n\n        switch (type) {\n            case \"hex\":\n                seq += \"0123456789abcdef\";\n                break;\n            default:\n                if (hasNumbers) seq += \"0123456789\";\n                if (hasLowercase) seq += \"abcdefghijklmnopqrstuvwxyz\";\n                if (hasUppercase) seq += \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n                break;\n        }\n\n        return seq;\n    }\n\n    static randomInteger(min, max) {\n        const range = max - min + 1;\n        const randomBuffer = new Uint32Array(1);\n        window.crypto.getRandomValues(randomBuffer);\n        return Math.floor((randomBuffer[0] / (0xFFFFFFFF + 1)) * range) + min;\n    }\n\n    static randomSeq(count, options = {}) {\n        const seq = this.getSeq(options);\n        const seqLength = seq.length;\n        const randomValues = new Uint32Array(count);\n        window.crypto.getRandomValues(randomValues);\n        return Array.from(randomValues, v => seq[v % seqLength]).join('');\n    }\n\n    static randomShortIds() {\n        const lengths = [2, 4, 6, 8, 10, 12, 14, 16].sort(() => Math.random() - 0.5);\n\n        return lengths.map(len => this.randomSeq(len, { type: \"hex\" })).join(',');\n    }\n\n    static randomLowerAndNum(len) {\n        return this.randomSeq(len, { hasUppercase: false });\n    }\n\n    static randomUUID() {\n        if (window.location.protocol === \"https:\") {\n            return window.crypto.randomUUID();\n        } else {\n            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'\n                .replace(/[xy]/g, function (c) {\n                    const randomValues = new Uint8Array(1);\n                    window.crypto.getRandomValues(randomValues);\n                    let randomValue = randomValues[0] % 16;\n                    let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);\n                    return calculatedValue.toString(16);\n                });\n        }\n    }\n\n    static randomShadowsocksPassword(method = SSMethods.BLAKE3_AES_256_GCM) {\n        let length = 32;\n\n        if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) {\n            length = 16;\n        }\n\n        const array = new Uint8Array(length);\n\n        window.crypto.getRandomValues(array);\n\n        return Base64.alternativeEncode(String.fromCharCode(...array));\n    }\n\n    static randomBase32String(length = 16) {\n        const array = new Uint8Array(length);\n\n        window.crypto.getRandomValues(array);\n\n        const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n        let result = '';\n        let bits = 0;\n        let buffer = 0;\n\n        for (let i = 0; i < array.length; i++) {\n            buffer = (buffer << 8) | array[i];\n            bits += 8;\n\n            while (bits >= 5) {\n                bits -= 5;\n                result += base32Chars[(buffer >>> bits) & 0x1F];\n            }\n        }\n\n        if (bits > 0) {\n            result += base32Chars[(buffer << (5 - bits)) & 0x1F];\n        }\n\n        return result;\n    }\n}\n\nclass ObjectUtil {\n    static getPropIgnoreCase(obj, prop) {\n        for (const name in obj) {\n            if (!obj.hasOwnProperty(name)) {\n                continue;\n            }\n            if (name.toLowerCase() === prop.toLowerCase()) {\n                return obj[name];\n            }\n        }\n        return undefined;\n    }\n\n    static deepSearch(obj, key) {\n        if (obj instanceof Array) {\n            for (let i = 0; i < obj.length; ++i) {\n                if (this.deepSearch(obj[i], key)) {\n                    return true;\n                }\n            }\n        } else if (obj instanceof Object) {\n            for (let name in obj) {\n                if (!obj.hasOwnProperty(name)) {\n                    continue;\n                }\n                if (this.deepSearch(obj[name], key)) {\n                    return true;\n                }\n            }\n        } else {\n            return this.isEmpty(obj) ? false : obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;\n        }\n        return false;\n    }\n\n    static isEmpty(obj) {\n        return obj === null || obj === undefined || obj === '';\n    }\n\n    static isArrEmpty(arr) {\n        return !this.isEmpty(arr) && arr.length === 0;\n    }\n\n    static copyArr(dest, src) {\n        dest.splice(0);\n        for (const item of src) {\n            dest.push(item);\n        }\n    }\n\n    static clone(obj) {\n        let newObj;\n        if (obj instanceof Array) {\n            newObj = [];\n            this.copyArr(newObj, obj);\n        } else if (obj instanceof Object) {\n            newObj = {};\n            for (const key of Object.keys(obj)) {\n                newObj[key] = obj[key];\n            }\n        } else {\n            newObj = obj;\n        }\n        return newObj;\n    }\n\n    static deepClone(obj) {\n        let newObj;\n        if (obj instanceof Array) {\n            newObj = [];\n            for (const item of obj) {\n                newObj.push(this.deepClone(item));\n            }\n        } else if (obj instanceof Object) {\n            newObj = {};\n            for (const key of Object.keys(obj)) {\n                newObj[key] = this.deepClone(obj[key]);\n            }\n        } else {\n            newObj = obj;\n        }\n        return newObj;\n    }\n\n    static cloneProps(dest, src, ...ignoreProps) {\n        if (dest == null || src == null) {\n            return;\n        }\n        const ignoreEmpty = this.isArrEmpty(ignoreProps);\n        for (const key of Object.keys(src)) {\n            if (!src.hasOwnProperty(key)) {\n                continue;\n            } else if (!dest.hasOwnProperty(key)) {\n                continue;\n            } else if (src[key] === undefined) {\n                continue;\n            }\n            if (ignoreEmpty) {\n                dest[key] = src[key];\n            } else {\n                let ignore = false;\n                for (let i = 0; i < ignoreProps.length; ++i) {\n                    if (key === ignoreProps[i]) {\n                        ignore = true;\n                        break;\n                    }\n                }\n                if (!ignore) {\n                    dest[key] = src[key];\n                }\n            }\n        }\n    }\n\n    static delProps(obj, ...props) {\n        for (const prop of props) {\n            if (prop in obj) {\n                delete obj[prop];\n            }\n        }\n    }\n\n    static execute(func, ...args) {\n        if (!this.isEmpty(func) && typeof func === 'function') {\n            func(...args);\n        }\n    }\n\n    static orDefault(obj, defaultValue) {\n        if (obj == null) {\n            return defaultValue;\n        }\n        return obj;\n    }\n\n    static equals(a, b) {\n        // shallow, symmetric comparison so newly added fields also affect equality\n        const aKeys = Object.keys(a);\n        const bKeys = Object.keys(b);\n        if (aKeys.length !== bKeys.length) return false;\n        for (const key of aKeys) {\n            if (!Object.prototype.hasOwnProperty.call(b, key)) return false;\n            if (a[key] !== b[key]) return false;\n        }\n        return true;\n    }\n}\n\nclass Wireguard {\n    static gf(init) {\n        var r = new Float64Array(16);\n        if (init) {\n            for (var i = 0; i < init.length; ++i)\n                r[i] = init[i];\n        }\n        return r;\n    }\n\n    static pack(o, n) {\n        var b, m = this.gf(), t = this.gf();\n        for (var i = 0; i < 16; ++i)\n            t[i] = n[i];\n        this.carry(t);\n        this.carry(t);\n        this.carry(t);\n        for (var j = 0; j < 2; ++j) {\n            m[0] = t[0] - 0xffed;\n            for (var i = 1; i < 15; ++i) {\n                m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);\n                m[i - 1] &= 0xffff;\n            }\n            m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);\n            b = (m[15] >> 16) & 1;\n            m[14] &= 0xffff;\n            this.cswap(t, m, 1 - b);\n        }\n        for (var i = 0; i < 16; ++i) {\n            o[2 * i] = t[i] & 0xff;\n            o[2 * i + 1] = t[i] >> 8;\n        }\n    }\n\n    static carry(o) {\n        var c;\n        for (var i = 0; i < 16; ++i) {\n            o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);\n            o[i] &= 0xffff;\n        }\n    }\n\n    static cswap(p, q, b) {\n        var t, c = ~(b - 1);\n        for (var i = 0; i < 16; ++i) {\n            t = c & (p[i] ^ q[i]);\n            p[i] ^= t;\n            q[i] ^= t;\n        }\n    }\n\n    static add(o, a, b) {\n        for (var i = 0; i < 16; ++i)\n            o[i] = (a[i] + b[i]) | 0;\n    }\n\n    static subtract(o, a, b) {\n        for (var i = 0; i < 16; ++i)\n            o[i] = (a[i] - b[i]) | 0;\n    }\n\n    static multmod(o, a, b) {\n        var t = new Float64Array(31);\n        for (var i = 0; i < 16; ++i) {\n            for (var j = 0; j < 16; ++j)\n                t[i + j] += a[i] * b[j];\n        }\n        for (var i = 0; i < 15; ++i)\n            t[i] += 38 * t[i + 16];\n        for (var i = 0; i < 16; ++i)\n            o[i] = t[i];\n        this.carry(o);\n        this.carry(o);\n    }\n\n    static invert(o, i) {\n        var c = this.gf();\n        for (var a = 0; a < 16; ++a)\n            c[a] = i[a];\n        for (var a = 253; a >= 0; --a) {\n            this.multmod(c, c, c);\n            if (a !== 2 && a !== 4)\n                this.multmod(c, c, i);\n        }\n        for (var a = 0; a < 16; ++a)\n            o[a] = c[a];\n    }\n\n    static clamp(z) {\n        z[31] = (z[31] & 127) | 64;\n        z[0] &= 248;\n    }\n\n    static generatePublicKey(privateKey) {\n        var r, z = new Uint8Array(32);\n        var a = this.gf([1]),\n            b = this.gf([9]),\n            c = this.gf(),\n            d = this.gf([1]),\n            e = this.gf(),\n            f = this.gf(),\n            _121665 = this.gf([0xdb41, 1]),\n            _9 = this.gf([9]);\n        for (var i = 0; i < 32; ++i)\n            z[i] = privateKey[i];\n        this.clamp(z);\n        for (var i = 254; i >= 0; --i) {\n            r = (z[i >>> 3] >>> (i & 7)) & 1;\n            this.cswap(a, b, r);\n            this.cswap(c, d, r);\n            this.add(e, a, c);\n            this.subtract(a, a, c);\n            this.add(c, b, d);\n            this.subtract(b, b, d);\n            this.multmod(d, e, e);\n            this.multmod(f, a, a);\n            this.multmod(a, c, a);\n            this.multmod(c, b, e);\n            this.add(e, a, c);\n            this.subtract(a, a, c);\n            this.multmod(b, a, a);\n            this.subtract(c, d, f);\n            this.multmod(a, c, _121665);\n            this.add(a, a, d);\n            this.multmod(c, c, a);\n            this.multmod(a, d, f);\n            this.multmod(d, b, _9);\n            this.multmod(b, e, e);\n            this.cswap(a, b, r);\n            this.cswap(c, d, r);\n        }\n        this.invert(c, c);\n        this.multmod(a, a, c);\n        this.pack(z, a);\n        return z;\n    }\n\n    static generatePresharedKey() {\n        var privateKey = new Uint8Array(32);\n        window.crypto.getRandomValues(privateKey);\n        return privateKey;\n    }\n\n    static generatePrivateKey() {\n        var privateKey = this.generatePresharedKey();\n        this.clamp(privateKey);\n        return privateKey;\n    }\n\n    static encodeBase64(dest, src) {\n        var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);\n        for (var i = 0; i < 4; ++i)\n            dest[i] = input[i] + 65 +\n                (((25 - input[i]) >> 8) & 6) -\n                (((51 - input[i]) >> 8) & 75) -\n                (((61 - input[i]) >> 8) & 15) +\n                (((62 - input[i]) >> 8) & 3);\n    }\n\n    static keyToBase64(key) {\n        var i, base64 = new Uint8Array(44);\n        for (i = 0; i < 32 / 3; ++i)\n            this.encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));\n        this.encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));\n        base64[43] = 61;\n        return String.fromCharCode.apply(null, base64);\n    }\n\n    static keyFromBase64(encoded) {\n        const binaryStr = atob(encoded);\n        const bytes = new Uint8Array(binaryStr.length);\n        for (let i = 0; i < binaryStr.length; i++) {\n            bytes[i] = binaryStr.charCodeAt(i);\n        }\n        return bytes;\n    }\n\n    static generateKeypair(secretKey = '') {\n        var privateKey = secretKey.length > 0 ? this.keyFromBase64(secretKey) : this.generatePrivateKey();\n        var publicKey = this.generatePublicKey(privateKey);\n        return {\n            publicKey: this.keyToBase64(publicKey),\n            privateKey: secretKey.length > 0 ? secretKey : this.keyToBase64(privateKey)\n        };\n    }\n}\n\nclass ClipboardManager {\n    static copyText(content = \"\") {\n        // !! here old way of copying is used because not everyone can afford https connection\n        return new Promise((resolve) => {\n            try {\n                const textarea = window.document.createElement('textarea');\n\n                textarea.style.fontSize = '12pt';\n                textarea.style.border = '0';\n                textarea.style.padding = '0';\n                textarea.style.margin = '0';\n                textarea.style.position = 'absolute';\n                textarea.style.left = '-9999px';\n                textarea.style.top = `${window.pageYOffset || document.documentElement.scrollTop}px`;\n                textarea.setAttribute('readonly', '');\n                textarea.value = content;\n\n                window.document.body.appendChild(textarea);\n\n                textarea.select();\n                window.document.execCommand(\"copy\");\n\n                window.document.body.removeChild(textarea);\n\n                resolve(true)\n            } catch {\n                resolve(false)\n            }\n        })\n    }\n}\n\nclass Base64 {\n    static encode(content = \"\", safe = false) {\n        if (safe) {\n            return Base64.encode(content)\n                .replace(/\\+/g, '-')\n                .replace(/=/g, '')\n                .replace(/\\//g, '_')\n        }\n\n        return window.btoa(\n            String.fromCharCode(...new TextEncoder().encode(content))\n        )\n    }\n\n    static alternativeEncode(content) {\n        return window.btoa(\n            content\n        )\n    }\n\n    static decode(content = \"\") {\n        return new TextDecoder()\n            .decode(\n                Uint8Array.from(window.atob(content), c => c.charCodeAt(0))\n            )\n    }\n}\n\nclass SizeFormatter {\n    static ONE_KB = 1024;\n    static ONE_MB = this.ONE_KB * 1024;\n    static ONE_GB = this.ONE_MB * 1024;\n    static ONE_TB = this.ONE_GB * 1024;\n    static ONE_PB = this.ONE_TB * 1024;\n\n    static sizeFormat(size) {\n        if (size <= 0) return \"0 B\";\n        if (size < this.ONE_KB) return size.toFixed(0) + \" B\";\n        if (size < this.ONE_MB) return (size / this.ONE_KB).toFixed(2) + \" KB\";\n        if (size < this.ONE_GB) return (size / this.ONE_MB).toFixed(2) + \" MB\";\n        if (size < this.ONE_TB) return (size / this.ONE_GB).toFixed(2) + \" GB\";\n        if (size < this.ONE_PB) return (size / this.ONE_TB).toFixed(2) + \" TB\";\n        return (size / this.ONE_PB).toFixed(2) + \" PB\";\n    }\n}\n\nclass CPUFormatter {\n    static cpuSpeedFormat(speed) {\n        return speed > 1000 ? (speed / 1000).toFixed(2) + \" GHz\" : speed.toFixed(2) + \" MHz\";\n    }\n\n    static cpuCoreFormat(cores) {\n        return cores === 1 ? \"1 Core\" : cores + \" Cores\";\n    }\n}\n\nclass TimeFormatter {\n    static formatSecond(second) {\n        if (second < 60) return second.toFixed(0) + 's';\n        if (second < 3600) return (second / 60).toFixed(0) + 'm';\n        if (second < 3600 * 24) return (second / 3600).toFixed(0) + 'h';\n        let day = Math.floor(second / 3600 / 24);\n        let remain = ((second / 3600) - (day * 24)).toFixed(0);\n        return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');\n    }\n}\n\nclass NumberFormatter {\n    static addZero(num) {\n        return num < 10 ? \"0\" + num : num;\n    }\n\n    static toFixed(num, n) {\n        n = Math.pow(10, n);\n        return Math.floor(num * n) / n;\n    }\n}\n\nclass Utils {\n    static debounce(fn, delay) {\n        let timeoutID = null;\n        return function () {\n            clearTimeout(timeoutID);\n            let args = arguments;\n            let that = this;\n            timeoutID = setTimeout(() => fn.apply(that, args), delay);\n        };\n    }\n}\n\nclass CookieManager {\n    static getCookie(cname) {\n        let name = cname + '=';\n        let ca = document.cookie.split(';');\n        for (let c of ca) {\n            c = c.trim();\n            if (c.indexOf(name) === 0) {\n                return decodeURIComponent(c.substring(name.length, c.length));\n            }\n        }\n        return '';\n    }\n\n    static setCookie(cname, cvalue, exdays) {\n        const d = new Date();\n        d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);\n        let expires = 'expires=' + d.toUTCString();\n        document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';\n    }\n}\n\nclass ColorUtils {\n    static usageColor(data, threshold, total) {\n        switch (true) {\n            case data === null: return \"purple\";\n            case total < 0: return \"green\";\n            case total == 0: return \"purple\";\n            case data < total - threshold: return \"green\";\n            case data < total: return \"orange\";\n            default: return \"red\";\n        }\n    }\n\n    static clientUsageColor(clientStats, trafficDiff) {\n        switch (true) {\n            case !clientStats || clientStats.total == 0: return \"#7a316f\";\n            case clientStats.up + clientStats.down < clientStats.total - trafficDiff: return \"#008771\";\n            case clientStats.up + clientStats.down < clientStats.total: return \"#f37b24\";\n            default: return \"#cf3c3c\";\n        }\n    }\n\n    static userExpiryColor(threshold, client, isDark = false) {\n        if (!client.enable) return isDark ? '#2c3950' : '#bcbcbc';\n        let now = new Date().getTime(), expiry = client.expiryTime;\n        switch (true) {\n            case expiry === null: return \"#7a316f\";\n            case expiry < 0: return \"#008771\";\n            case expiry == 0: return \"#7a316f\";\n            case now < expiry - threshold: return \"#008771\";\n            case now < expiry: return \"#f37b24\";\n            default: return \"#cf3c3c\";\n        }\n    }\n}\n\nclass ArrayUtils {\n    static doAllItemsExist(array1, array2) {\n        return array1.every(item => array2.includes(item));\n    }\n}\n\nclass URLBuilder {\n    static buildURL({ host, port, isTLS, base, path }) {\n        if (!host || host.length === 0) host = window.location.hostname;\n        if (!port || port.length === 0) port = window.location.port;\n        if (isTLS === undefined) isTLS = window.location.protocol === \"https:\";\n\n        const protocol = isTLS ? \"https:\" : \"http:\";\n        port = String(port);\n        if (port === \"\" || (isTLS && port === \"443\") || (!isTLS && port === \"80\")) {\n            port = \"\";\n        } else {\n            port = `:${port}`;\n        }\n\n        return `${protocol}//${host}${port}${base}${path}`;\n    }\n}\n\nclass LanguageManager {\n    static supportedLanguages = [\n        {\n            name: \"العربية\",\n            value: \"ar-EG\",\n            icon: \"🇪🇬\",\n        },\n        {\n            name: \"English\",\n            value: \"en-US\",\n            icon: \"🇺🇸\",\n        },\n        {\n            name: \"فارسی\",\n            value: \"fa-IR\",\n            icon: \"🇮🇷\",\n        },\n        {\n            name: \"简体中文\",\n            value: \"zh-CN\",\n            icon: \"🇨🇳\",\n        },\n        {\n            name: \"繁體中文\",\n            value: \"zh-TW\",\n            icon: \"🇹🇼\",\n        },\n        {\n            name: \"日本語\",\n            value: \"ja-JP\",\n            icon: \"🇯🇵\",\n        },\n        {\n            name: \"Русский\",\n            value: \"ru-RU\",\n            icon: \"🇷🇺\",\n        },\n        {\n            name: \"Tiếng Việt\",\n            value: \"vi-VN\",\n            icon: \"🇻🇳\",\n        },\n        {\n            name: \"Español\",\n            value: \"es-ES\",\n            icon: \"🇪🇸\",\n        },\n        {\n            name: \"Indonesian\",\n            value: \"id-ID\",\n            icon: \"🇮🇩\",\n        },\n        {\n            name: \"Український\",\n            value: \"uk-UA\",\n            icon: \"🇺🇦\",\n        },\n        {\n            name: \"Türkçe\",\n            value: \"tr-TR\",\n            icon: \"🇹🇷\",\n        },\n        {\n            name: \"Português\",\n            value: \"pt-BR\",\n            icon: \"🇧🇷\",\n        }\n    ]\n\n    static getLanguage() {\n        let lang = CookieManager.getCookie(\"lang\");\n\n        if (!lang) {\n            if (window.navigator) {\n                lang = window.navigator.language || window.navigator.userLanguage;\n\n                const simularLangs = [\n                    [\"ar\", this.supportedLanguages[0].value],\n                    [\"fa\", this.supportedLanguages[2].value],\n                    [\"ja\", this.supportedLanguages[5].value],\n                    [\"ru\", this.supportedLanguages[6].value],\n                    [\"vi\", this.supportedLanguages[7].value],\n                    [\"es\", this.supportedLanguages[8].value],\n                    [\"id\", this.supportedLanguages[9].value],\n                    [\"uk\", this.supportedLanguages[10].value],\n                    [\"tr\", this.supportedLanguages[11].value],\n                    [\"pt\", this.supportedLanguages[12].value],\n                ]\n\n                simularLangs.forEach((pair) => {\n                    if (lang === pair[0]) {\n                        lang = pair[1];\n                    }\n                });\n\n                if (LanguageManager.isSupportLanguage(lang)) {\n                    CookieManager.setCookie(\"lang\", lang, 150);\n                } else {\n                    CookieManager.setCookie(\"lang\", \"en-US\", 150);\n                    window.location.reload();\n                }\n            } else {\n                CookieManager.setCookie(\"lang\", \"en-US\", 150);\n                window.location.reload();\n            }\n        }\n\n        return lang;\n    }\n\n    static setLanguage(language) {\n        if (!LanguageManager.isSupportLanguage(language)) {\n            language = \"en-US\";\n        }\n\n        CookieManager.setCookie(\"lang\", language, 150);\n        window.location.reload();\n    }\n\n    static isSupportLanguage(language) {\n        const languageFilter = LanguageManager.supportedLanguages.filter((lang) => {\n            return lang.value === language\n        })\n\n        return languageFilter.length > 0;\n    }\n}\n\nconst MediaQueryMixin = {\n    data() {\n        return {\n            isMobile: window.innerWidth <= 768,\n        };\n    },\n    methods: {\n        updateDeviceType() {\n            this.isMobile = window.innerWidth <= 768;\n        },\n    },\n    mounted() {\n        window.addEventListener('resize', this.updateDeviceType);\n    },\n    beforeDestroy() {\n        window.removeEventListener('resize', this.updateDeviceType);\n    },\n}\n\nclass FileManager {\n    static downloadTextFile(content, filename = 'file.txt', options = { type: \"text/plain\" }) {\n        let link = window.document.createElement('a');\n\n        link.download = filename;\n        link.style.border = '0';\n        link.style.padding = '0';\n        link.style.margin = '0';\n        link.style.position = 'absolute';\n        link.style.left = '-9999px';\n        link.style.top = `${window.pageYOffset || window.document.documentElement.scrollTop}px`;\n        link.href = URL.createObjectURL(new Blob([content], options));\n        link.click();\n\n        URL.revokeObjectURL(link.href);\n\n        link.remove();\n    }\n}\n\nclass IntlUtil {\n    static formatDate(date) {\n        const language = LanguageManager.getLanguage()\n\n        let intlOptions = {\n            year: \"numeric\",\n            month: \"numeric\",\n            day: \"numeric\",\n            hour: \"numeric\",\n            minute: \"numeric\",\n            second: \"numeric\"\n        }\n\n        const intl = new Intl.DateTimeFormat(\n            language,\n            intlOptions\n        )\n\n        return intl.format(new Date(date))\n    }\n    static formatRelativeTime(date) {\n        const language = LanguageManager.getLanguage()\n        const now = new Date()\n\n        // Handle delayed start (negative expiryTime values)\n        const diff = date < 0\n            ? Math.round(date / (1000 * 60 * 60 * 24))\n            : Math.round((date - now) / (1000 * 60 * 60 * 24))\n        const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' })\n\n        return formatter.format(diff, 'day');\n    }\n}"
  },
  {
    "path": "web/assets/js/websocket.js",
    "content": "/**\n * WebSocket client for real-time updates\n */\nclass WebSocketClient {\n  constructor(basePath = '') {\n    this.basePath = basePath;\n    this.ws = null;\n    this.reconnectAttempts = 0;\n    this.maxReconnectAttempts = 10;\n    this.reconnectDelay = 1000;\n    this.listeners = new Map();\n    this.isConnected = false;\n    this.shouldReconnect = true;\n  }\n\n  connect() {\n    if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {\n      return;\n    }\n\n    this.shouldReconnect = true;\n\n    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n    // Ensure basePath ends with '/' for proper URL construction\n    let basePath = this.basePath || '';\n    if (basePath && !basePath.endsWith('/')) {\n      basePath += '/';\n    }\n    const wsUrl = `${protocol}//${window.location.host}${basePath}ws`;\n    \n    console.log('WebSocket connecting to:', wsUrl, 'basePath:', this.basePath);\n    \n    try {\n      this.ws = new WebSocket(wsUrl);\n      \n      this.ws.onopen = () => {\n        console.log('WebSocket connected');\n        this.isConnected = true;\n        this.reconnectAttempts = 0;\n        this.emit('connected');\n      };\n\n      this.ws.onmessage = (event) => {\n        try {\n          // Validate message size (prevent memory issues)\n          const maxMessageSize = 10 * 1024 * 1024; // 10MB\n          if (event.data && event.data.length > maxMessageSize) {\n            console.error('WebSocket message too large:', event.data.length, 'bytes');\n            this.ws.close();\n            return;\n          }\n          \n          const message = JSON.parse(event.data);\n          if (!message || typeof message !== 'object') {\n            console.error('Invalid WebSocket message format');\n            return;\n          }\n          \n          this.handleMessage(message);\n        } catch (e) {\n          console.error('Failed to parse WebSocket message:', e);\n        }\n      };\n\n      this.ws.onerror = (error) => {\n        console.error('WebSocket error:', error);\n        this.emit('error', error);\n      };\n\n      this.ws.onclose = () => {\n        console.log('WebSocket disconnected');\n        this.isConnected = false;\n        this.emit('disconnected');\n        \n        if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {\n          this.reconnectAttempts++;\n          const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);\n          console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);\n          setTimeout(() => this.connect(), delay);\n        }\n      };\n    } catch (e) {\n      console.error('Failed to create WebSocket connection:', e);\n      this.emit('error', e);\n    }\n  }\n\n  handleMessage(message) {\n    const { type, payload, time } = message;\n    \n    // Emit to specific type listeners\n    this.emit(type, payload, time);\n    \n    // Emit to all listeners\n    this.emit('message', { type, payload, time });\n  }\n\n  on(event, callback) {\n    if (!this.listeners.has(event)) {\n      this.listeners.set(event, []);\n    }\n    const callbacks = this.listeners.get(event);\n    if (!callbacks.includes(callback)) {\n      callbacks.push(callback);\n    }\n  }\n\n  off(event, callback) {\n    if (!this.listeners.has(event)) {\n      return;\n    }\n    const callbacks = this.listeners.get(event);\n    const index = callbacks.indexOf(callback);\n    if (index > -1) {\n      callbacks.splice(index, 1);\n    }\n  }\n\n  emit(event, ...args) {\n    if (this.listeners.has(event)) {\n      this.listeners.get(event).forEach(callback => {\n        try {\n          callback(...args);\n        } catch (e) {\n          console.error('Error in WebSocket event handler:', e);\n        }\n      });\n    }\n  }\n\n  disconnect() {\n    this.shouldReconnect = false;\n    if (this.ws) {\n      this.ws.close();\n      this.ws = null;\n    }\n  }\n\n  send(data) {\n    if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n      this.ws.send(JSON.stringify(data));\n    } else {\n      console.warn('WebSocket is not connected');\n    }\n  }\n}\n\n// Create global WebSocket client instance\n// Safely get basePath from global scope (defined in page.html)\nwindow.wsClient = new WebSocketClient(typeof basePath !== 'undefined' ? basePath : '');\n"
  },
  {
    "path": "web/controller/api.go",
    "content": "package controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/session\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.\ntype APIController struct {\n\tBaseController\n\tinboundController *InboundController\n\tserverController  *ServerController\n\tTgbot             service.Tgbot\n}\n\n// NewAPIController creates a new APIController instance and initializes its routes.\nfunc NewAPIController(g *gin.RouterGroup) *APIController {\n\ta := &APIController{}\n\ta.initRouter(g)\n\treturn a\n}\n\n// checkAPIAuth is a middleware that returns 404 for unauthenticated API requests\n// to hide the existence of API endpoints from unauthorized users\nfunc (a *APIController) checkAPIAuth(c *gin.Context) {\n\tif !session.IsLogin(c) {\n\t\tc.AbortWithStatus(http.StatusNotFound)\n\t\treturn\n\t}\n\tc.Next()\n}\n\n// initRouter sets up the API routes for inbounds, server, and other endpoints.\nfunc (a *APIController) initRouter(g *gin.RouterGroup) {\n\t// Main API group\n\tapi := g.Group(\"/panel/api\")\n\tapi.Use(a.checkAPIAuth)\n\n\t// Inbounds API\n\tinbounds := api.Group(\"/inbounds\")\n\ta.inboundController = NewInboundController(inbounds)\n\n\t// Server API\n\tserver := api.Group(\"/server\")\n\ta.serverController = NewServerController(server)\n\n\t// Extra routes\n\tapi.GET(\"/backuptotgbot\", a.BackuptoTgbot)\n}\n\n// BackuptoTgbot sends a backup of the panel data to Telegram bot admins.\nfunc (a *APIController) BackuptoTgbot(c *gin.Context) {\n\ta.Tgbot.SendBackupToAdmins()\n}\n"
  },
  {
    "path": "web/controller/base.go",
    "content": "// Package controller provides HTTP request handlers and controllers for the 3x-ui web management panel.\n// It handles routing, authentication, and API endpoints for managing Xray inbounds, settings, and more.\npackage controller\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/locale\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/session\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// BaseController provides common functionality for all controllers, including authentication checks.\ntype BaseController struct{}\n\n// checkLogin is a middleware that verifies user authentication and handles unauthorized access.\nfunc (a *BaseController) checkLogin(c *gin.Context) {\n\tif !session.IsLogin(c) {\n\t\tif isAjax(c) {\n\t\t\tpureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, \"pages.login.loginAgain\"))\n\t\t} else {\n\t\t\tc.Redirect(http.StatusTemporaryRedirect, c.GetString(\"base_path\"))\n\t\t}\n\t\tc.Abort()\n\t} else {\n\t\tc.Next()\n\t}\n}\n\n// I18nWeb retrieves an internationalized message for the web interface based on the current locale.\nfunc I18nWeb(c *gin.Context, name string, params ...string) string {\n\tanyfunc, funcExists := c.Get(\"I18n\")\n\tif !funcExists {\n\t\tlogger.Warning(\"I18n function not exists in gin context!\")\n\t\treturn \"\"\n\t}\n\ti18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string)\n\tmsg := i18nFunc(locale.Web, name, params...)\n\treturn msg\n}\n"
  },
  {
    "path": "web/controller/inbound.go",
    "content": "package controller\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/session\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/websocket\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// InboundController handles HTTP requests related to Xray inbounds management.\ntype InboundController struct {\n\tinboundService service.InboundService\n\txrayService    service.XrayService\n}\n\n// NewInboundController creates a new InboundController and sets up its routes.\nfunc NewInboundController(g *gin.RouterGroup) *InboundController {\n\ta := &InboundController{}\n\ta.initRouter(g)\n\treturn a\n}\n\n// initRouter initializes the routes for inbound-related operations.\nfunc (a *InboundController) initRouter(g *gin.RouterGroup) {\n\n\tg.GET(\"/list\", a.getInbounds)\n\tg.GET(\"/get/:id\", a.getInbound)\n\tg.GET(\"/getClientTraffics/:email\", a.getClientTraffics)\n\tg.GET(\"/getClientTrafficsById/:id\", a.getClientTrafficsById)\n\n\tg.POST(\"/add\", a.addInbound)\n\tg.POST(\"/del/:id\", a.delInbound)\n\tg.POST(\"/update/:id\", a.updateInbound)\n\tg.POST(\"/clientIps/:email\", a.getClientIps)\n\tg.POST(\"/clearClientIps/:email\", a.clearClientIps)\n\tg.POST(\"/addClient\", a.addInboundClient)\n\tg.POST(\"/:id/delClient/:clientId\", a.delInboundClient)\n\tg.POST(\"/updateClient/:clientId\", a.updateInboundClient)\n\tg.POST(\"/:id/resetClientTraffic/:email\", a.resetClientTraffic)\n\tg.POST(\"/resetAllTraffics\", a.resetAllTraffics)\n\tg.POST(\"/resetAllClientTraffics/:id\", a.resetAllClientTraffics)\n\tg.POST(\"/delDepletedClients/:id\", a.delDepletedClients)\n\tg.POST(\"/import\", a.importInbound)\n\tg.POST(\"/onlines\", a.onlines)\n\tg.POST(\"/lastOnline\", a.lastOnline)\n\tg.POST(\"/updateClientTraffic/:email\", a.updateClientTraffic)\n\tg.POST(\"/:id/delClientByEmail/:email\", a.delInboundClientByEmail)\n}\n\n// getInbounds retrieves the list of inbounds for the logged-in user.\nfunc (a *InboundController) getInbounds(c *gin.Context) {\n\tuser := session.GetLoginUser(c)\n\tinbounds, err := a.inboundService.GetInbounds(user.Id)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.obtain\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, inbounds, nil)\n}\n\n// getInbound retrieves a specific inbound by its ID.\nfunc (a *InboundController) getInbound(c *gin.Context) {\n\tid, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"get\"), err)\n\t\treturn\n\t}\n\tinbound, err := a.inboundService.GetInbound(id)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.obtain\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, inbound, nil)\n}\n\n// getClientTraffics retrieves client traffic information by email.\nfunc (a *InboundController) getClientTraffics(c *gin.Context) {\n\temail := c.Param(\"email\")\n\tclientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.trafficGetError\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, clientTraffics, nil)\n}\n\n// getClientTrafficsById retrieves client traffic information by inbound ID.\nfunc (a *InboundController) getClientTrafficsById(c *gin.Context) {\n\tid := c.Param(\"id\")\n\tclientTraffics, err := a.inboundService.GetClientTrafficByID(id)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.trafficGetError\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, clientTraffics, nil)\n}\n\n// addInbound creates a new inbound configuration.\nfunc (a *InboundController) addInbound(c *gin.Context) {\n\tinbound := &model.Inbound{}\n\terr := c.ShouldBind(inbound)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundCreateSuccess\"), err)\n\t\treturn\n\t}\n\tuser := session.GetLoginUser(c)\n\tinbound.UserId = user.Id\n\tif inbound.Listen == \"\" || inbound.Listen == \"0.0.0.0\" || inbound.Listen == \"::\" || inbound.Listen == \"::0\" {\n\t\tinbound.Tag = fmt.Sprintf(\"inbound-%v\", inbound.Port)\n\t} else {\n\t\tinbound.Tag = fmt.Sprintf(\"inbound-%v:%v\", inbound.Listen, inbound.Port)\n\t}\n\n\tinbound, needRestart, err := a.inboundService.AddInbound(inbound)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tjsonMsgObj(c, I18nWeb(c, \"pages.inbounds.toasts.inboundCreateSuccess\"), inbound, nil)\n\tif needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n\t// Broadcast inbounds update via WebSocket\n\tinbounds, _ := a.inboundService.GetInbounds(user.Id)\n\twebsocket.BroadcastInbounds(inbounds)\n}\n\n// delInbound deletes an inbound configuration by its ID.\nfunc (a *InboundController) delInbound(c *gin.Context) {\n\tid, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundDeleteSuccess\"), err)\n\t\treturn\n\t}\n\tneedRestart, err := a.inboundService.DelInbound(id)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tjsonMsgObj(c, I18nWeb(c, \"pages.inbounds.toasts.inboundDeleteSuccess\"), id, nil)\n\tif needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n\t// Broadcast inbounds update via WebSocket\n\tuser := session.GetLoginUser(c)\n\tinbounds, _ := a.inboundService.GetInbounds(user.Id)\n\twebsocket.BroadcastInbounds(inbounds)\n}\n\n// updateInbound updates an existing inbound configuration.\nfunc (a *InboundController) updateInbound(c *gin.Context) {\n\tid, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\tinbound := &model.Inbound{\n\t\tId: id,\n\t}\n\terr = c.ShouldBind(inbound)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\tinbound, needRestart, err := a.inboundService.UpdateInbound(inbound)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tjsonMsgObj(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), inbound, nil)\n\tif needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n\t// Broadcast inbounds update via WebSocket\n\tuser := session.GetLoginUser(c)\n\tinbounds, _ := a.inboundService.GetInbounds(user.Id)\n\twebsocket.BroadcastInbounds(inbounds)\n}\n\n// getClientIps retrieves the IP addresses associated with a client by email.\nfunc (a *InboundController) getClientIps(c *gin.Context) {\n\temail := c.Param(\"email\")\n\n\tips, err := a.inboundService.GetInboundClientIps(email)\n\tif err != nil || ips == \"\" {\n\t\tjsonObj(c, \"No IP Record\", nil)\n\t\treturn\n\t}\n\n\t// Prefer returning a normalized string list for consistent UI rendering\n\ttype ipWithTimestamp struct {\n\t\tIP        string `json:\"ip\"`\n\t\tTimestamp int64  `json:\"timestamp\"`\n\t}\n\n\tvar ipsWithTime []ipWithTimestamp\n\tif err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {\n\t\tformatted := make([]string, 0, len(ipsWithTime))\n\t\tfor _, item := range ipsWithTime {\n\t\t\tif item.IP == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif item.Timestamp > 0 {\n\t\t\t\tts := time.Unix(item.Timestamp, 0).Local().Format(\"2006-01-02 15:04:05\")\n\t\t\t\tformatted = append(formatted, fmt.Sprintf(\"%s (%s)\", item.IP, ts))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tformatted = append(formatted, item.IP)\n\t\t}\n\t\tjsonObj(c, formatted, nil)\n\t\treturn\n\t}\n\n\tvar oldIps []string\n\tif err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {\n\t\tjsonObj(c, oldIps, nil)\n\t\treturn\n\t}\n\n\t// If parsing fails, return as string\n\tjsonObj(c, ips, nil)\n}\n\n// clearClientIps clears the IP addresses for a client by email.\nfunc (a *InboundController) clearClientIps(c *gin.Context) {\n\temail := c.Param(\"email\")\n\n\terr := a.inboundService.ClearClientIps(email)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.updateSuccess\"), err)\n\t\treturn\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.logCleanSuccess\"), nil)\n}\n\n// addInboundClient adds a new client to an existing inbound.\nfunc (a *InboundController) addInboundClient(c *gin.Context) {\n\tdata := &model.Inbound{}\n\terr := c.ShouldBind(data)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\n\tneedRestart, err := a.inboundService.AddInboundClient(data)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundClientAddSuccess\"), nil)\n\tif needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n}\n\n// delInboundClient deletes a client from an inbound by inbound ID and client ID.\nfunc (a *InboundController) delInboundClient(c *gin.Context) {\n\tid, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\tclientId := c.Param(\"clientId\")\n\n\tneedRestart, err := a.inboundService.DelInboundClient(id, clientId)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundClientDeleteSuccess\"), nil)\n\tif needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n}\n\n// updateInboundClient updates a client's configuration in an inbound.\nfunc (a *InboundController) updateInboundClient(c *gin.Context) {\n\tclientId := c.Param(\"clientId\")\n\n\tinbound := &model.Inbound{}\n\terr := c.ShouldBind(inbound)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\n\tneedRestart, err := a.inboundService.UpdateInboundClient(inbound, clientId)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundClientUpdateSuccess\"), nil)\n\tif needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n}\n\n// resetClientTraffic resets the traffic counter for a specific client in an inbound.\nfunc (a *InboundController) resetClientTraffic(c *gin.Context) {\n\tid, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\temail := c.Param(\"email\")\n\n\tneedRestart, err := a.inboundService.ResetClientTraffic(id, email)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.resetInboundClientTrafficSuccess\"), nil)\n\tif needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n}\n\n// resetAllTraffics resets all traffic counters across all inbounds.\nfunc (a *InboundController) resetAllTraffics(c *gin.Context) {\n\terr := a.inboundService.ResetAllTraffics()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t} else {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.resetAllTrafficSuccess\"), nil)\n}\n\n// resetAllClientTraffics resets traffic counters for all clients in a specific inbound.\nfunc (a *InboundController) resetAllClientTraffics(c *gin.Context) {\n\tid, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\n\terr = a.inboundService.ResetAllClientTraffics(id)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t} else {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.resetAllClientTrafficSuccess\"), nil)\n}\n\n// importInbound imports an inbound configuration from provided data.\nfunc (a *InboundController) importInbound(c *gin.Context) {\n\tinbound := &model.Inbound{}\n\terr := json.Unmarshal([]byte(c.PostForm(\"data\")), inbound)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tuser := session.GetLoginUser(c)\n\tinbound.Id = 0\n\tinbound.UserId = user.Id\n\tif inbound.Listen == \"\" || inbound.Listen == \"0.0.0.0\" || inbound.Listen == \"::\" || inbound.Listen == \"::0\" {\n\t\tinbound.Tag = fmt.Sprintf(\"inbound-%v\", inbound.Port)\n\t} else {\n\t\tinbound.Tag = fmt.Sprintf(\"inbound-%v:%v\", inbound.Listen, inbound.Port)\n\t}\n\n\tfor index := range inbound.ClientStats {\n\t\tinbound.ClientStats[index].Id = 0\n\t\tinbound.ClientStats[index].Enable = true\n\t}\n\n\tneedRestart := false\n\tinbound, needRestart, err = a.inboundService.AddInbound(inbound)\n\tjsonMsgObj(c, I18nWeb(c, \"pages.inbounds.toasts.inboundCreateSuccess\"), inbound, err)\n\tif err == nil && needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n}\n\n// delDepletedClients deletes clients in an inbound who have exhausted their traffic limits.\nfunc (a *InboundController) delDepletedClients(c *gin.Context) {\n\tid, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\terr = a.inboundService.DelDepletedClients(id)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.delDepletedClientsSuccess\"), nil)\n}\n\n// onlines retrieves the list of currently online clients.\nfunc (a *InboundController) onlines(c *gin.Context) {\n\tjsonObj(c, a.inboundService.GetOnlineClients(), nil)\n}\n\n// lastOnline retrieves the last online timestamps for clients.\nfunc (a *InboundController) lastOnline(c *gin.Context) {\n\tdata, err := a.inboundService.GetClientsLastOnline()\n\tjsonObj(c, data, err)\n}\n\n// updateClientTraffic updates the traffic statistics for a client by email.\nfunc (a *InboundController) updateClientTraffic(c *gin.Context) {\n\temail := c.Param(\"email\")\n\n\t// Define the request structure for traffic update\n\ttype TrafficUpdateRequest struct {\n\t\tUpload   int64 `json:\"upload\"`\n\t\tDownload int64 `json:\"download\"`\n\t}\n\n\tvar request TrafficUpdateRequest\n\terr := c.ShouldBindJSON(&request)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundUpdateSuccess\"), err)\n\t\treturn\n\t}\n\n\terr = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\n\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.inboundClientUpdateSuccess\"), nil)\n}\n\n// delInboundClientByEmail deletes a client from an inbound by email address.\nfunc (a *InboundController) delInboundClientByEmail(c *gin.Context) {\n\tinboundId, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, \"Invalid inbound ID\", err)\n\t\treturn\n\t}\n\n\temail := c.Param(\"email\")\n\tneedRestart, err := a.inboundService.DelInboundClientByEmail(inboundId, email)\n\tif err != nil {\n\t\tjsonMsg(c, \"Failed to delete client by email\", err)\n\t\treturn\n\t}\n\n\tjsonMsg(c, \"Client deleted successfully\", nil)\n\tif needRestart {\n\t\ta.xrayService.SetToNeedRestart()\n\t}\n}\n"
  },
  {
    "path": "web/controller/index.go",
    "content": "package controller\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/session\"\n\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// LoginForm represents the login request structure.\ntype LoginForm struct {\n\tUsername      string `json:\"username\" form:\"username\"`\n\tPassword      string `json:\"password\" form:\"password\"`\n\tTwoFactorCode string `json:\"twoFactorCode\" form:\"twoFactorCode\"`\n}\n\n// IndexController handles the main index and login-related routes.\ntype IndexController struct {\n\tBaseController\n\n\tsettingService service.SettingService\n\tuserService    service.UserService\n\ttgbot          service.Tgbot\n}\n\n// NewIndexController creates a new IndexController and initializes its routes.\nfunc NewIndexController(g *gin.RouterGroup) *IndexController {\n\ta := &IndexController{}\n\ta.initRouter(g)\n\treturn a\n}\n\n// initRouter sets up the routes for index, login, logout, and two-factor authentication.\nfunc (a *IndexController) initRouter(g *gin.RouterGroup) {\n\tg.GET(\"/\", a.index)\n\tg.GET(\"/logout\", a.logout)\n\n\tg.POST(\"/login\", a.login)\n\tg.POST(\"/getTwoFactorEnable\", a.getTwoFactorEnable)\n}\n\n// index handles the root route, redirecting logged-in users to the panel or showing the login page.\nfunc (a *IndexController) index(c *gin.Context) {\n\tif session.IsLogin(c) {\n\t\tc.Redirect(http.StatusTemporaryRedirect, \"panel/\")\n\t\treturn\n\t}\n\thtml(c, \"login.html\", \"pages.login.title\", nil)\n}\n\n// login handles user authentication and session creation.\nfunc (a *IndexController) login(c *gin.Context) {\n\tvar form LoginForm\n\n\tif err := c.ShouldBind(&form); err != nil {\n\t\tpureJsonMsg(c, http.StatusOK, false, I18nWeb(c, \"pages.login.toasts.invalidFormData\"))\n\t\treturn\n\t}\n\tif form.Username == \"\" {\n\t\tpureJsonMsg(c, http.StatusOK, false, I18nWeb(c, \"pages.login.toasts.emptyUsername\"))\n\t\treturn\n\t}\n\tif form.Password == \"\" {\n\t\tpureJsonMsg(c, http.StatusOK, false, I18nWeb(c, \"pages.login.toasts.emptyPassword\"))\n\t\treturn\n\t}\n\n\tuser, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)\n\ttimeStr := time.Now().Format(\"2006-01-02 15:04:05\")\n\tsafeUser := template.HTMLEscapeString(form.Username)\n\tsafePass := template.HTMLEscapeString(form.Password)\n\n\tif user == nil {\n\t\tlogger.Warningf(\"wrong username: \\\"%s\\\", password: \\\"%s\\\", IP: \\\"%s\\\"\", safeUser, safePass, getRemoteIp(c))\n\n\t\tnotifyPass := safePass\n\n\t\tif checkErr != nil && checkErr.Error() == \"invalid 2fa code\" {\n\t\t\ttranslatedError := a.tgbot.I18nBot(\"tgbot.messages.2faFailed\")\n\t\t\tnotifyPass = fmt.Sprintf(\"*** (%s)\", translatedError)\n\t\t}\n\n\t\ta.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0)\n\t\tpureJsonMsg(c, http.StatusOK, false, I18nWeb(c, \"pages.login.toasts.wrongUsernameOrPassword\"))\n\t\treturn\n\t}\n\n\tlogger.Infof(\"%s logged in successfully, Ip Address: %s\\n\", safeUser, getRemoteIp(c))\n\ta.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)\n\n\tsessionMaxAge, err := a.settingService.GetSessionMaxAge()\n\tif err != nil {\n\t\tlogger.Warning(\"Unable to get session's max age from DB\")\n\t}\n\n\tsession.SetMaxAge(c, sessionMaxAge*60)\n\tsession.SetLoginUser(c, user)\n\tif err := sessions.Default(c).Save(); err != nil {\n\t\tlogger.Warning(\"Unable to save session: \", err)\n\t\treturn\n\t}\n\n\tlogger.Infof(\"%s logged in successfully\", safeUser)\n\tjsonMsg(c, I18nWeb(c, \"pages.login.toasts.successLogin\"), nil)\n}\n\n// logout handles user logout by clearing the session and redirecting to the login page.\nfunc (a *IndexController) logout(c *gin.Context) {\n\tuser := session.GetLoginUser(c)\n\tif user != nil {\n\t\tlogger.Infof(\"%s logged out successfully\", user.Username)\n\t}\n\tsession.ClearSession(c)\n\tif err := sessions.Default(c).Save(); err != nil {\n\t\tlogger.Warning(\"Unable to save session after clearing:\", err)\n\t}\n\tc.Redirect(http.StatusTemporaryRedirect, c.GetString(\"base_path\"))\n}\n\n// getTwoFactorEnable retrieves the current status of two-factor authentication.\nfunc (a *IndexController) getTwoFactorEnable(c *gin.Context) {\n\tstatus, err := a.settingService.GetTwoFactorEnable()\n\tif err == nil {\n\t\tjsonObj(c, status, nil)\n\t}\n}\n"
  },
  {
    "path": "web/controller/server.go",
    "content": "package controller\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/web/global\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/websocket\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nvar filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\\-.]+$`)\n\n// ServerController handles server management and status-related operations.\ntype ServerController struct {\n\tBaseController\n\n\tserverService  service.ServerService\n\tsettingService service.SettingService\n\n\tlastStatus *service.Status\n\n\tlastVersions        []string\n\tlastGetVersionsTime int64 // unix seconds\n}\n\n// NewServerController creates a new ServerController, initializes routes, and starts background tasks.\nfunc NewServerController(g *gin.RouterGroup) *ServerController {\n\ta := &ServerController{}\n\ta.initRouter(g)\n\ta.startTask()\n\treturn a\n}\n\n// initRouter sets up the routes for server status, Xray management, and utility endpoints.\nfunc (a *ServerController) initRouter(g *gin.RouterGroup) {\n\n\tg.GET(\"/status\", a.status)\n\tg.GET(\"/cpuHistory/:bucket\", a.getCpuHistoryBucket)\n\tg.GET(\"/getXrayVersion\", a.getXrayVersion)\n\tg.GET(\"/getConfigJson\", a.getConfigJson)\n\tg.GET(\"/getDb\", a.getDb)\n\tg.GET(\"/getNewUUID\", a.getNewUUID)\n\tg.GET(\"/getNewX25519Cert\", a.getNewX25519Cert)\n\tg.GET(\"/getNewmldsa65\", a.getNewmldsa65)\n\tg.GET(\"/getNewmlkem768\", a.getNewmlkem768)\n\tg.GET(\"/getNewVlessEnc\", a.getNewVlessEnc)\n\n\tg.POST(\"/stopXrayService\", a.stopXrayService)\n\tg.POST(\"/restartXrayService\", a.restartXrayService)\n\tg.POST(\"/installXray/:version\", a.installXray)\n\tg.POST(\"/updateGeofile\", a.updateGeofile)\n\tg.POST(\"/updateGeofile/:fileName\", a.updateGeofile)\n\tg.POST(\"/logs/:count\", a.getLogs)\n\tg.POST(\"/xraylogs/:count\", a.getXrayLogs)\n\tg.POST(\"/importDB\", a.importDB)\n\tg.POST(\"/getNewEchCert\", a.getNewEchCert)\n}\n\n// refreshStatus updates the cached server status and collects CPU history.\nfunc (a *ServerController) refreshStatus() {\n\ta.lastStatus = a.serverService.GetStatus(a.lastStatus)\n\t// collect cpu history when status is fresh\n\tif a.lastStatus != nil {\n\t\ta.serverService.AppendCpuSample(time.Now(), a.lastStatus.Cpu)\n\t\t// Broadcast status update via WebSocket\n\t\twebsocket.BroadcastStatus(a.lastStatus)\n\t}\n}\n\n// startTask initiates background tasks for continuous status monitoring.\nfunc (a *ServerController) startTask() {\n\twebServer := global.GetWebServer()\n\tc := webServer.GetCron()\n\tc.AddFunc(\"@every 2s\", func() {\n\t\t// Always refresh to keep CPU history collected continuously.\n\t\t// Sampling is lightweight and capped to ~6 hours in memory.\n\t\ta.refreshStatus()\n\t})\n}\n\n// status returns the current server status information.\nfunc (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }\n\n// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket.\nfunc (a *ServerController) getCpuHistoryBucket(c *gin.Context) {\n\tbucketStr := c.Param(\"bucket\")\n\tbucket, err := strconv.Atoi(bucketStr)\n\tif err != nil || bucket <= 0 {\n\t\tjsonMsg(c, \"invalid bucket\", fmt.Errorf(\"bad bucket\"))\n\t\treturn\n\t}\n\tallowed := map[int]bool{\n\t\t2:   true, // Real-time view\n\t\t30:  true, // 30s intervals\n\t\t60:  true, // 1m intervals\n\t\t120: true, // 2m intervals\n\t\t180: true, // 3m intervals\n\t\t300: true, // 5m intervals\n\t}\n\tif !allowed[bucket] {\n\t\tjsonMsg(c, \"invalid bucket\", fmt.Errorf(\"unsupported bucket\"))\n\t\treturn\n\t}\n\tpoints := a.serverService.AggregateCpuHistory(bucket, 60)\n\tjsonObj(c, points, nil)\n}\n\n// getXrayVersion retrieves available Xray versions, with caching for 1 minute.\nfunc (a *ServerController) getXrayVersion(c *gin.Context) {\n\tnow := time.Now().Unix()\n\tif now-a.lastGetVersionsTime <= 60 { // 1 minute cache\n\t\tjsonObj(c, a.lastVersions, nil)\n\t\treturn\n\t}\n\n\tversions, err := a.serverService.GetXrayVersions()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"getVersion\"), err)\n\t\treturn\n\t}\n\n\ta.lastVersions = versions\n\ta.lastGetVersionsTime = now\n\n\tjsonObj(c, versions, nil)\n}\n\n// installXray installs or updates Xray to the specified version.\nfunc (a *ServerController) installXray(c *gin.Context) {\n\tversion := c.Param(\"version\")\n\terr := a.serverService.UpdateXray(version)\n\tjsonMsg(c, I18nWeb(c, \"pages.index.xraySwitchVersionPopover\"), err)\n}\n\n// updateGeofile updates the specified geo file for Xray.\nfunc (a *ServerController) updateGeofile(c *gin.Context) {\n\tfileName := c.Param(\"fileName\")\n\n\t// Validate the filename for security (prevent path traversal attacks)\n\tif fileName != \"\" && !a.serverService.IsValidGeofileName(fileName) {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.index.geofileUpdatePopover\"),\n\t\t\tfmt.Errorf(\"invalid filename: contains unsafe characters or path traversal patterns\"))\n\t\treturn\n\t}\n\n\terr := a.serverService.UpdateGeofile(fileName)\n\tjsonMsg(c, I18nWeb(c, \"pages.index.geofileUpdatePopover\"), err)\n}\n\n// stopXrayService stops the Xray service.\nfunc (a *ServerController) stopXrayService(c *gin.Context) {\n\terr := a.serverService.StopXrayService()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.xray.stopError\"), err)\n\t\twebsocket.BroadcastXrayState(\"error\", err.Error())\n\t\treturn\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.xray.stopSuccess\"), err)\n\twebsocket.BroadcastXrayState(\"stop\", \"\")\n\twebsocket.BroadcastNotification(\n\t\tI18nWeb(c, \"pages.xray.stopSuccess\"),\n\t\t\"Xray service has been stopped\",\n\t\t\"warning\",\n\t)\n}\n\n// restartXrayService restarts the Xray service.\nfunc (a *ServerController) restartXrayService(c *gin.Context) {\n\terr := a.serverService.RestartXrayService()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.xray.restartError\"), err)\n\t\twebsocket.BroadcastXrayState(\"error\", err.Error())\n\t\treturn\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.xray.restartSuccess\"), err)\n\twebsocket.BroadcastXrayState(\"running\", \"\")\n\twebsocket.BroadcastNotification(\n\t\tI18nWeb(c, \"pages.xray.restartSuccess\"),\n\t\t\"Xray service has been restarted successfully\",\n\t\t\"success\",\n\t)\n}\n\n// getLogs retrieves the application logs based on count, level, and syslog filters.\nfunc (a *ServerController) getLogs(c *gin.Context) {\n\tcount := c.Param(\"count\")\n\tlevel := c.PostForm(\"level\")\n\tsyslog := c.PostForm(\"syslog\")\n\tlogs := a.serverService.GetLogs(count, level, syslog)\n\tjsonObj(c, logs, nil)\n}\n\n// getXrayLogs retrieves Xray logs with filtering options for direct, blocked, and proxy traffic.\nfunc (a *ServerController) getXrayLogs(c *gin.Context) {\n\tcount := c.Param(\"count\")\n\tfilter := c.PostForm(\"filter\")\n\tshowDirect := c.PostForm(\"showDirect\")\n\tshowBlocked := c.PostForm(\"showBlocked\")\n\tshowProxy := c.PostForm(\"showProxy\")\n\n\tvar freedoms []string\n\tvar blackholes []string\n\n\t//getting tags for freedom and blackhole outbounds\n\tconfig, err := a.settingService.GetDefaultXrayConfig()\n\tif err == nil && config != nil {\n\t\tif cfgMap, ok := config.(map[string]any); ok {\n\t\t\tif outbounds, ok := cfgMap[\"outbounds\"].([]any); ok {\n\t\t\t\tfor _, outbound := range outbounds {\n\t\t\t\t\tif obMap, ok := outbound.(map[string]any); ok {\n\t\t\t\t\t\tswitch obMap[\"protocol\"] {\n\t\t\t\t\t\tcase \"freedom\":\n\t\t\t\t\t\t\tif tag, ok := obMap[\"tag\"].(string); ok {\n\t\t\t\t\t\t\t\tfreedoms = append(freedoms, tag)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tcase \"blackhole\":\n\t\t\t\t\t\t\tif tag, ok := obMap[\"tag\"].(string); ok {\n\t\t\t\t\t\t\t\tblackholes = append(blackholes, tag)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(freedoms) == 0 {\n\t\tfreedoms = []string{\"direct\"}\n\t}\n\tif len(blackholes) == 0 {\n\t\tblackholes = []string{\"blocked\"}\n\t}\n\n\tlogs := a.serverService.GetXrayLogs(count, filter, showDirect, showBlocked, showProxy, freedoms, blackholes)\n\tjsonObj(c, logs, nil)\n}\n\n// getConfigJson retrieves the Xray configuration as JSON.\nfunc (a *ServerController) getConfigJson(c *gin.Context) {\n\tconfigJson, err := a.serverService.GetConfigJson()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.index.getConfigError\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, configJson, nil)\n}\n\n// getDb downloads the database file.\nfunc (a *ServerController) getDb(c *gin.Context) {\n\tdb, err := a.serverService.GetDb()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.index.getDatabaseError\"), err)\n\t\treturn\n\t}\n\n\tfilename := \"x-ui.db\"\n\n\tif !isValidFilename(filename) {\n\t\tc.AbortWithError(http.StatusBadRequest, fmt.Errorf(\"invalid filename\"))\n\t\treturn\n\t}\n\n\t// Set the headers for the response\n\tc.Header(\"Content-Type\", \"application/octet-stream\")\n\tc.Header(\"Content-Disposition\", \"attachment; filename=\"+filename)\n\n\t// Write the file contents to the response\n\tc.Writer.Write(db)\n}\n\nfunc isValidFilename(filename string) bool {\n\t// Validate that the filename only contains allowed characters\n\treturn filenameRegex.MatchString(filename)\n}\n\n// importDB imports a database file and restarts the Xray service.\nfunc (a *ServerController) importDB(c *gin.Context) {\n\t// Get the file from the request body\n\tfile, _, err := c.Request.FormFile(\"db\")\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.index.readDatabaseError\"), err)\n\t\treturn\n\t}\n\tdefer file.Close()\n\t// Always restart Xray before return\n\tdefer a.serverService.RestartXrayService()\n\t// lastGetStatusTime removed; no longer needed\n\t// Import it\n\terr = a.serverService.ImportDB(file)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.index.importDatabaseError\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, I18nWeb(c, \"pages.index.importDatabaseSuccess\"), nil)\n}\n\n// getNewX25519Cert generates a new X25519 certificate.\nfunc (a *ServerController) getNewX25519Cert(c *gin.Context) {\n\tcert, err := a.serverService.GetNewX25519Cert()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.getNewX25519CertError\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, cert, nil)\n}\n\n// getNewmldsa65 generates a new ML-DSA-65 key.\nfunc (a *ServerController) getNewmldsa65(c *gin.Context) {\n\tcert, err := a.serverService.GetNewmldsa65()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.getNewmldsa65Error\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, cert, nil)\n}\n\n// getNewEchCert generates a new ECH certificate for the given SNI.\nfunc (a *ServerController) getNewEchCert(c *gin.Context) {\n\tsni := c.PostForm(\"sni\")\n\tcert, err := a.serverService.GetNewEchCert(sni)\n\tif err != nil {\n\t\tjsonMsg(c, \"get ech certificate\", err)\n\t\treturn\n\t}\n\tjsonObj(c, cert, nil)\n}\n\n// getNewVlessEnc generates a new VLESS encryption key.\nfunc (a *ServerController) getNewVlessEnc(c *gin.Context) {\n\tout, err := a.serverService.GetNewVlessEnc()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.inbounds.toasts.getNewVlessEncError\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, out, nil)\n}\n\n// getNewUUID generates a new UUID.\nfunc (a *ServerController) getNewUUID(c *gin.Context) {\n\tuuidResp, err := a.serverService.GetNewUUID()\n\tif err != nil {\n\t\tjsonMsg(c, \"Failed to generate UUID\", err)\n\t\treturn\n\t}\n\n\tjsonObj(c, uuidResp, nil)\n}\n\n// getNewmlkem768 generates a new ML-KEM-768 key.\nfunc (a *ServerController) getNewmlkem768(c *gin.Context) {\n\tout, err := a.serverService.GetNewmlkem768()\n\tif err != nil {\n\t\tjsonMsg(c, \"Failed to generate mlkem768 keys\", err)\n\t\treturn\n\t}\n\tjsonObj(c, out, nil)\n}\n"
  },
  {
    "path": "web/controller/setting.go",
    "content": "package controller\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/util/crypto\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/entity\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/session\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// updateUserForm represents the form for updating user credentials.\ntype updateUserForm struct {\n\tOldUsername string `json:\"oldUsername\" form:\"oldUsername\"`\n\tOldPassword string `json:\"oldPassword\" form:\"oldPassword\"`\n\tNewUsername string `json:\"newUsername\" form:\"newUsername\"`\n\tNewPassword string `json:\"newPassword\" form:\"newPassword\"`\n}\n\n// SettingController handles settings and user management operations.\ntype SettingController struct {\n\tsettingService service.SettingService\n\tuserService    service.UserService\n\tpanelService   service.PanelService\n}\n\n// NewSettingController creates a new SettingController and initializes its routes.\nfunc NewSettingController(g *gin.RouterGroup) *SettingController {\n\ta := &SettingController{}\n\ta.initRouter(g)\n\treturn a\n}\n\n// initRouter sets up the routes for settings management.\nfunc (a *SettingController) initRouter(g *gin.RouterGroup) {\n\tg = g.Group(\"/setting\")\n\n\tg.POST(\"/all\", a.getAllSetting)\n\tg.POST(\"/defaultSettings\", a.getDefaultSettings)\n\tg.POST(\"/update\", a.updateSetting)\n\tg.POST(\"/updateUser\", a.updateUser)\n\tg.POST(\"/restartPanel\", a.restartPanel)\n\tg.GET(\"/getDefaultJsonConfig\", a.getDefaultXrayConfig)\n}\n\n// getAllSetting retrieves all current settings.\nfunc (a *SettingController) getAllSetting(c *gin.Context) {\n\tallSetting, err := a.settingService.GetAllSetting()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.getSettings\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, allSetting, nil)\n}\n\n// getDefaultSettings retrieves the default settings based on the host.\nfunc (a *SettingController) getDefaultSettings(c *gin.Context) {\n\tresult, err := a.settingService.GetDefaultSettings(c.Request.Host)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.getSettings\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, result, nil)\n}\n\n// updateSetting updates all settings with the provided data.\nfunc (a *SettingController) updateSetting(c *gin.Context) {\n\tallSetting := &entity.AllSetting{}\n\terr := c.ShouldBind(allSetting)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.modifySettings\"), err)\n\t\treturn\n\t}\n\terr = a.settingService.UpdateAllSetting(allSetting)\n\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.modifySettings\"), err)\n}\n\n// updateUser updates the current user's username and password.\nfunc (a *SettingController) updateUser(c *gin.Context) {\n\tform := &updateUserForm{}\n\terr := c.ShouldBind(form)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.modifySettings\"), err)\n\t\treturn\n\t}\n\tuser := session.GetLoginUser(c)\n\tif user.Username != form.OldUsername || !crypto.CheckPasswordHash(user.Password, form.OldPassword) {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.modifyUserError\"), errors.New(I18nWeb(c, \"pages.settings.toasts.originalUserPassIncorrect\")))\n\t\treturn\n\t}\n\tif form.NewUsername == \"\" || form.NewPassword == \"\" {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.modifyUserError\"), errors.New(I18nWeb(c, \"pages.settings.toasts.userPassMustBeNotEmpty\")))\n\t\treturn\n\t}\n\terr = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)\n\tif err == nil {\n\t\tuser.Username = form.NewUsername\n\t\tuser.Password, _ = crypto.HashPasswordAsBcrypt(form.NewPassword)\n\t\tsession.SetLoginUser(c, user)\n\t}\n\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.modifyUser\"), err)\n}\n\n// restartPanel restarts the panel service after a delay.\nfunc (a *SettingController) restartPanel(c *gin.Context) {\n\terr := a.panelService.RestartPanel(time.Second * 3)\n\tjsonMsg(c, I18nWeb(c, \"pages.settings.restartPanelSuccess\"), err)\n}\n\n// getDefaultXrayConfig retrieves the default Xray configuration.\nfunc (a *SettingController) getDefaultXrayConfig(c *gin.Context) {\n\tdefaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.getSettings\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, defaultJsonConfig, nil)\n}\n"
  },
  {
    "path": "web/controller/util.go",
    "content": "package controller\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/entity\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// getRemoteIp extracts the real IP address from the request headers or remote address.\nfunc getRemoteIp(c *gin.Context) string {\n\tvalue := c.GetHeader(\"X-Real-IP\")\n\tif value != \"\" {\n\t\treturn value\n\t}\n\tvalue = c.GetHeader(\"X-Forwarded-For\")\n\tif value != \"\" {\n\t\tips := strings.Split(value, \",\")\n\t\treturn ips[0]\n\t}\n\taddr := c.Request.RemoteAddr\n\tip, _, _ := net.SplitHostPort(addr)\n\treturn ip\n}\n\n// jsonMsg sends a JSON response with a message and error status.\nfunc jsonMsg(c *gin.Context, msg string, err error) {\n\tjsonMsgObj(c, msg, nil, err)\n}\n\n// jsonObj sends a JSON response with an object and error status.\nfunc jsonObj(c *gin.Context, obj any, err error) {\n\tjsonMsgObj(c, \"\", obj, err)\n}\n\n// jsonMsgObj sends a JSON response with a message, object, and error status.\nfunc jsonMsgObj(c *gin.Context, msg string, obj any, err error) {\n\tm := entity.Msg{\n\t\tObj: obj,\n\t}\n\tif err == nil {\n\t\tm.Success = true\n\t\tif msg != \"\" {\n\t\t\tm.Msg = msg\n\t\t}\n\t} else {\n\t\tm.Success = false\n\t\tm.Msg = msg + \" (\" + err.Error() + \")\"\n\t\tlogger.Warning(msg+\" \"+I18nWeb(c, \"fail\")+\": \", err)\n\t}\n\tc.JSON(http.StatusOK, m)\n}\n\n// pureJsonMsg sends a pure JSON message response with custom status code.\nfunc pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) {\n\tc.JSON(statusCode, entity.Msg{\n\t\tSuccess: success,\n\t\tMsg:     msg,\n\t})\n}\n\n// html renders an HTML template with the provided data and title.\nfunc html(c *gin.Context, name string, title string, data gin.H) {\n\tif data == nil {\n\t\tdata = gin.H{}\n\t}\n\tdata[\"title\"] = title\n\thost := c.GetHeader(\"X-Forwarded-Host\")\n\tif host == \"\" {\n\t\thost = c.GetHeader(\"X-Real-IP\")\n\t}\n\tif host == \"\" {\n\t\tvar err error\n\t\thost, _, err = net.SplitHostPort(c.Request.Host)\n\t\tif err != nil {\n\t\t\thost = c.Request.Host\n\t\t}\n\t}\n\tdata[\"host\"] = host\n\tdata[\"request_uri\"] = c.Request.RequestURI\n\tdata[\"base_path\"] = c.GetString(\"base_path\")\n\tc.HTML(http.StatusOK, name, getContext(data))\n}\n\n// getContext adds version and other context data to the provided gin.H.\nfunc getContext(h gin.H) gin.H {\n\ta := gin.H{\n\t\t\"cur_ver\": config.GetVersion(),\n\t}\n\tfor key, value := range h {\n\t\ta[key] = value\n\t}\n\treturn a\n}\n\n// isAjax checks if the request is an AJAX request.\nfunc isAjax(c *gin.Context) bool {\n\treturn c.GetHeader(\"X-Requested-With\") == \"XMLHttpRequest\"\n}\n"
  },
  {
    "path": "web/controller/websocket.go",
    "content": "package controller\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/session\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/websocket\"\n\n\t\"github.com/gin-gonic/gin\"\n\tws \"github.com/gorilla/websocket\"\n)\n\nconst (\n\t// Time allowed to write a message to the peer\n\twriteWait = 10 * time.Second\n\n\t// Time allowed to read the next pong message from the peer\n\tpongWait = 60 * time.Second\n\n\t// Send pings to peer with this period (must be less than pongWait)\n\tpingPeriod = (pongWait * 9) / 10\n\n\t// Maximum message size allowed from peer\n\tmaxMessageSize = 512\n)\n\nvar upgrader = ws.Upgrader{\n\tReadBufferSize:  4096, // Increased from 1024 for better performance\n\tWriteBufferSize: 4096, // Increased from 1024 for better performance\n\tCheckOrigin: func(r *http.Request) bool {\n\t\t// Check origin for security\n\t\torigin := r.Header.Get(\"Origin\")\n\t\tif origin == \"\" {\n\t\t\t// Allow connections without Origin header (same-origin requests)\n\t\t\treturn true\n\t\t}\n\t\t// Get the host from the request\n\t\thost := r.Host\n\t\t// Extract scheme and host from origin\n\t\toriginURL := origin\n\t\t// Simple check: origin should match the request host\n\t\t// This prevents cross-origin WebSocket hijacking\n\t\tif strings.HasPrefix(originURL, \"http://\") || strings.HasPrefix(originURL, \"https://\") {\n\t\t\t// Extract host from origin\n\t\t\toriginHost := strings.TrimPrefix(strings.TrimPrefix(originURL, \"http://\"), \"https://\")\n\t\t\tif idx := strings.Index(originHost, \"/\"); idx != -1 {\n\t\t\t\toriginHost = originHost[:idx]\n\t\t\t}\n\t\t\tif idx := strings.Index(originHost, \":\"); idx != -1 {\n\t\t\t\toriginHost = originHost[:idx]\n\t\t\t}\n\t\t\t// Compare hosts (without port)\n\t\t\trequestHost := host\n\t\t\tif idx := strings.Index(requestHost, \":\"); idx != -1 {\n\t\t\t\trequestHost = requestHost[:idx]\n\t\t\t}\n\t\t\treturn originHost == requestHost || originHost == \"\" || requestHost == \"\"\n\t\t}\n\t\treturn false\n\t},\n}\n\n// WebSocketController handles WebSocket connections for real-time updates\ntype WebSocketController struct {\n\tBaseController\n\thub *websocket.Hub\n}\n\n// NewWebSocketController creates a new WebSocket controller\nfunc NewWebSocketController(hub *websocket.Hub) *WebSocketController {\n\treturn &WebSocketController{\n\t\thub: hub,\n\t}\n}\n\n// HandleWebSocket handles WebSocket connections\nfunc (w *WebSocketController) HandleWebSocket(c *gin.Context) {\n\t// Check authentication\n\tif !session.IsLogin(c) {\n\t\tlogger.Warningf(\"Unauthorized WebSocket connection attempt from %s\", getRemoteIp(c))\n\t\tc.AbortWithStatus(http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\t// Upgrade connection to WebSocket\n\tconn, err := upgrader.Upgrade(c.Writer, c.Request, nil)\n\tif err != nil {\n\t\tlogger.Error(\"Failed to upgrade WebSocket connection:\", err)\n\t\treturn\n\t}\n\n\t// Create client\n\tclientID := uuid.New().String()\n\tclient := &websocket.Client{\n\t\tID:     clientID,\n\t\tHub:    w.hub,\n\t\tSend:   make(chan []byte, 512), // Increased from 256 to 512 to prevent overflow\n\t\tTopics: make(map[websocket.MessageType]bool),\n\t}\n\n\t// Register client\n\tw.hub.Register(client)\n\tlogger.Debugf(\"WebSocket client %s registered from %s\", clientID, getRemoteIp(c))\n\n\t// Start goroutines for reading and writing\n\tgo w.writePump(client, conn)\n\tgo w.readPump(client, conn)\n}\n\n// readPump pumps messages from the WebSocket connection to the hub\nfunc (w *WebSocketController) readPump(client *websocket.Client, conn *ws.Conn) {\n\tdefer func() {\n\t\tif r := common.Recover(\"WebSocket readPump panic\"); r != nil {\n\t\t\tlogger.Error(\"WebSocket readPump panic recovered:\", r)\n\t\t}\n\t\tw.hub.Unregister(client)\n\t\tconn.Close()\n\t}()\n\n\tconn.SetReadDeadline(time.Now().Add(pongWait))\n\tconn.SetPongHandler(func(string) error {\n\t\tconn.SetReadDeadline(time.Now().Add(pongWait))\n\t\treturn nil\n\t})\n\tconn.SetReadLimit(maxMessageSize)\n\n\tfor {\n\t\t_, message, err := conn.ReadMessage()\n\t\tif err != nil {\n\t\t\tif ws.IsUnexpectedCloseError(err, ws.CloseGoingAway, ws.CloseAbnormalClosure) {\n\t\t\t\tlogger.Debugf(\"WebSocket read error for client %s: %v\", client.ID, err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\t// Validate message size\n\t\tif len(message) > maxMessageSize {\n\t\t\tlogger.Warningf(\"WebSocket message from client %s exceeds max size: %d bytes\", client.ID, len(message))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Handle incoming messages (e.g., subscription requests)\n\t\t// For now, we'll just log them\n\t\tlogger.Debugf(\"Received WebSocket message from client %s: %s\", client.ID, string(message))\n\t}\n}\n\n// writePump pumps messages from the hub to the WebSocket connection\nfunc (w *WebSocketController) writePump(client *websocket.Client, conn *ws.Conn) {\n\tticker := time.NewTicker(pingPeriod)\n\tdefer func() {\n\t\tif r := common.Recover(\"WebSocket writePump panic\"); r != nil {\n\t\t\tlogger.Error(\"WebSocket writePump panic recovered:\", r)\n\t\t}\n\t\tticker.Stop()\n\t\tconn.Close()\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase message, ok := <-client.Send:\n\t\t\tconn.SetWriteDeadline(time.Now().Add(writeWait))\n\t\t\tif !ok {\n\t\t\t\t// Hub closed the channel\n\t\t\t\tconn.WriteMessage(ws.CloseMessage, []byte{})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Send each message individually (no batching)\n\t\t\t// This ensures each JSON message is sent separately and can be parsed correctly\n\t\t\tif err := conn.WriteMessage(ws.TextMessage, message); err != nil {\n\t\t\t\tlogger.Debugf(\"WebSocket write error for client %s: %v\", client.ID, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\tcase <-ticker.C:\n\t\t\tconn.SetWriteDeadline(time.Now().Add(writeWait))\n\t\t\tif err := conn.WriteMessage(ws.PingMessage, nil); err != nil {\n\t\t\t\tlogger.Debugf(\"WebSocket ping error for client %s: %v\", client.ID, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "web/controller/xray_setting.go",
    "content": "package controller\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// XraySettingController handles Xray configuration and settings operations.\ntype XraySettingController struct {\n\tXraySettingService service.XraySettingService\n\tSettingService     service.SettingService\n\tInboundService     service.InboundService\n\tOutboundService    service.OutboundService\n\tXrayService        service.XrayService\n\tWarpService        service.WarpService\n}\n\n// NewXraySettingController creates a new XraySettingController and initializes its routes.\nfunc NewXraySettingController(g *gin.RouterGroup) *XraySettingController {\n\ta := &XraySettingController{}\n\ta.initRouter(g)\n\treturn a\n}\n\n// initRouter sets up the routes for Xray settings management.\nfunc (a *XraySettingController) initRouter(g *gin.RouterGroup) {\n\tg = g.Group(\"/xray\")\n\tg.GET(\"/getDefaultJsonConfig\", a.getDefaultXrayConfig)\n\tg.GET(\"/getOutboundsTraffic\", a.getOutboundsTraffic)\n\tg.GET(\"/getXrayResult\", a.getXrayResult)\n\n\tg.POST(\"/\", a.getXraySetting)\n\tg.POST(\"/warp/:action\", a.warp)\n\tg.POST(\"/update\", a.updateSetting)\n\tg.POST(\"/resetOutboundsTraffic\", a.resetOutboundsTraffic)\n\tg.POST(\"/testOutbound\", a.testOutbound)\n}\n\n// getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL.\nfunc (a *XraySettingController) getXraySetting(c *gin.Context) {\n\txraySetting, err := a.SettingService.GetXrayConfigTemplate()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.getSettings\"), err)\n\t\treturn\n\t}\n\tinboundTags, err := a.InboundService.GetInboundTags()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.getSettings\"), err)\n\t\treturn\n\t}\n\toutboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl()\n\tif outboundTestUrl == \"\" {\n\t\toutboundTestUrl = \"https://www.google.com/generate_204\"\n\t}\n\txrayResponse := map[string]interface{}{\n\t\t\"xraySetting\":     json.RawMessage(xraySetting),\n\t\t\"inboundTags\":     json.RawMessage(inboundTags),\n\t\t\"outboundTestUrl\": outboundTestUrl,\n\t}\n\tresult, err := json.Marshal(xrayResponse)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.getSettings\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, string(result), nil)\n}\n\n// updateSetting updates the Xray configuration settings.\nfunc (a *XraySettingController) updateSetting(c *gin.Context) {\n\txraySetting := c.PostForm(\"xraySetting\")\n\tif err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.modifySettings\"), err)\n\t\treturn\n\t}\n\toutboundTestUrl := c.PostForm(\"outboundTestUrl\")\n\tif outboundTestUrl == \"\" {\n\t\toutboundTestUrl = \"https://www.google.com/generate_204\"\n\t}\n\t_ = a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl)\n\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.modifySettings\"), nil)\n}\n\n// getDefaultXrayConfig retrieves the default Xray configuration.\nfunc (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {\n\tdefaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.getSettings\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, defaultJsonConfig, nil)\n}\n\n// getXrayResult retrieves the current Xray service result.\nfunc (a *XraySettingController) getXrayResult(c *gin.Context) {\n\tjsonObj(c, a.XrayService.GetXrayResult(), nil)\n}\n\n// warp handles Warp-related operations based on the action parameter.\nfunc (a *XraySettingController) warp(c *gin.Context) {\n\taction := c.Param(\"action\")\n\tvar resp string\n\tvar err error\n\tswitch action {\n\tcase \"data\":\n\t\tresp, err = a.WarpService.GetWarpData()\n\tcase \"del\":\n\t\terr = a.WarpService.DelWarpData()\n\tcase \"config\":\n\t\tresp, err = a.WarpService.GetWarpConfig()\n\tcase \"reg\":\n\t\tskey := c.PostForm(\"privateKey\")\n\t\tpkey := c.PostForm(\"publicKey\")\n\t\tresp, err = a.WarpService.RegWarp(skey, pkey)\n\tcase \"license\":\n\t\tlicense := c.PostForm(\"license\")\n\t\tresp, err = a.WarpService.SetWarpLicense(license)\n\t}\n\n\tjsonObj(c, resp, err)\n}\n\n// getOutboundsTraffic retrieves the traffic statistics for outbounds.\nfunc (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {\n\toutboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.getOutboundTrafficError\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, outboundsTraffic, nil)\n}\n\n// resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.\nfunc (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {\n\ttag := c.PostForm(\"tag\")\n\terr := a.OutboundService.ResetOutboundTraffic(tag)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"pages.settings.toasts.resetOutboundTrafficError\"), err)\n\t\treturn\n\t}\n\tjsonObj(c, \"\", nil)\n}\n\n// testOutbound tests an outbound configuration and returns the delay/response time.\n// Optional form \"allOutbounds\": JSON array of all outbounds; used to resolve sockopt.dialerProxy dependencies.\nfunc (a *XraySettingController) testOutbound(c *gin.Context) {\n\toutboundJSON := c.PostForm(\"outbound\")\n\tallOutboundsJSON := c.PostForm(\"allOutbounds\")\n\n\tif outboundJSON == \"\" {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), common.NewError(\"outbound parameter is required\"))\n\t\treturn\n\t}\n\n\t// Load the test URL from server settings to prevent SSRF via user-controlled URLs\n\ttestURL, _ := a.SettingService.GetXrayOutboundTestUrl()\n\n\tresult, err := a.OutboundService.TestOutbound(outboundJSON, testURL, allOutboundsJSON)\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), err)\n\t\treturn\n\t}\n\n\tjsonObj(c, result, nil)\n}\n"
  },
  {
    "path": "web/controller/xui.go",
    "content": "package controller\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n)\n\n// XUIController is the main controller for the X-UI panel, managing sub-controllers.\ntype XUIController struct {\n\tBaseController\n\n\tsettingController     *SettingController\n\txraySettingController *XraySettingController\n}\n\n// NewXUIController creates a new XUIController and initializes its routes.\nfunc NewXUIController(g *gin.RouterGroup) *XUIController {\n\ta := &XUIController{}\n\ta.initRouter(g)\n\treturn a\n}\n\n// initRouter sets up the main panel routes and initializes sub-controllers.\nfunc (a *XUIController) initRouter(g *gin.RouterGroup) {\n\tg = g.Group(\"/panel\")\n\tg.Use(a.checkLogin)\n\n\tg.GET(\"/\", a.index)\n\tg.GET(\"/inbounds\", a.inbounds)\n\tg.GET(\"/settings\", a.settings)\n\tg.GET(\"/xray\", a.xraySettings)\n\n\ta.settingController = NewSettingController(g)\n\ta.xraySettingController = NewXraySettingController(g)\n}\n\n// index renders the main panel index page.\nfunc (a *XUIController) index(c *gin.Context) {\n\thtml(c, \"index.html\", \"pages.index.title\", nil)\n}\n\n// inbounds renders the inbounds management page.\nfunc (a *XUIController) inbounds(c *gin.Context) {\n\thtml(c, \"inbounds.html\", \"pages.inbounds.title\", nil)\n}\n\n// settings renders the settings management page.\nfunc (a *XUIController) settings(c *gin.Context) {\n\thtml(c, \"settings.html\", \"pages.settings.title\", nil)\n}\n\n// xraySettings renders the Xray settings page.\nfunc (a *XUIController) xraySettings(c *gin.Context) {\n\thtml(c, \"xray.html\", \"pages.xray.title\", nil)\n}\n"
  },
  {
    "path": "web/entity/entity.go",
    "content": "// Package entity defines data structures and entities used by the web layer of the 3x-ui panel.\npackage entity\n\nimport (\n\t\"crypto/tls\"\n\t\"math\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n)\n\n// Msg represents a standard API response message with success status, message text, and optional data object.\ntype Msg struct {\n\tSuccess bool   `json:\"success\"` // Indicates if the operation was successful\n\tMsg     string `json:\"msg\"`     // Response message text\n\tObj     any    `json:\"obj\"`     // Optional data object\n}\n\n// AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.\ntype AllSetting struct {\n\t// Web server settings\n\tWebListen     string `json:\"webListen\" form:\"webListen\"`         // Web server listen IP address\n\tWebDomain     string `json:\"webDomain\" form:\"webDomain\"`         // Web server domain for domain validation\n\tWebPort       int    `json:\"webPort\" form:\"webPort\"`             // Web server port number\n\tWebCertFile   string `json:\"webCertFile\" form:\"webCertFile\"`     // Path to SSL certificate file for web server\n\tWebKeyFile    string `json:\"webKeyFile\" form:\"webKeyFile\"`       // Path to SSL private key file for web server\n\tWebBasePath   string `json:\"webBasePath\" form:\"webBasePath\"`     // Base path for web panel URLs\n\tSessionMaxAge int    `json:\"sessionMaxAge\" form:\"sessionMaxAge\"` // Session maximum age in minutes\n\n\t// UI settings\n\tPageSize    int    `json:\"pageSize\" form:\"pageSize\"`       // Number of items per page in lists\n\tExpireDiff  int    `json:\"expireDiff\" form:\"expireDiff\"`   // Expiration warning threshold in days\n\tTrafficDiff int    `json:\"trafficDiff\" form:\"trafficDiff\"` // Traffic warning threshold percentage\n\tRemarkModel string `json:\"remarkModel\" form:\"remarkModel\"` // Remark model pattern for inbounds\n\tDatepicker  string `json:\"datepicker\" form:\"datepicker\"`   // Date picker format\n\n\t// Telegram bot settings\n\tTgBotEnable      bool   `json:\"tgBotEnable\" form:\"tgBotEnable\"`           // Enable Telegram bot notifications\n\tTgBotToken       string `json:\"tgBotToken\" form:\"tgBotToken\"`             // Telegram bot token\n\tTgBotProxy       string `json:\"tgBotProxy\" form:\"tgBotProxy\"`             // Proxy URL for Telegram bot\n\tTgBotAPIServer   string `json:\"tgBotAPIServer\" form:\"tgBotAPIServer\"`     // Custom API server for Telegram bot\n\tTgBotChatId      string `json:\"tgBotChatId\" form:\"tgBotChatId\"`           // Telegram chat ID for notifications\n\tTgRunTime        string `json:\"tgRunTime\" form:\"tgRunTime\"`               // Cron schedule for Telegram notifications\n\tTgBotBackup      bool   `json:\"tgBotBackup\" form:\"tgBotBackup\"`           // Enable database backup via Telegram\n\tTgBotLoginNotify bool   `json:\"tgBotLoginNotify\" form:\"tgBotLoginNotify\"` // Send login notifications\n\tTgCpu            int    `json:\"tgCpu\" form:\"tgCpu\"`                       // CPU usage threshold for alerts\n\tTgLang           string `json:\"tgLang\" form:\"tgLang\"`                     // Telegram bot language\n\n\t// Security settings\n\tTimeLocation    string `json:\"timeLocation\" form:\"timeLocation\"`       // Time zone location\n\tTwoFactorEnable bool   `json:\"twoFactorEnable\" form:\"twoFactorEnable\"` // Enable two-factor authentication\n\tTwoFactorToken  string `json:\"twoFactorToken\" form:\"twoFactorToken\"`   // Two-factor authentication token\n\n\t// Subscription server settings\n\tSubEnable                   bool   `json:\"subEnable\" form:\"subEnable\"`                                     // Enable subscription server\n\tSubJsonEnable               bool   `json:\"subJsonEnable\" form:\"subJsonEnable\"`                             // Enable JSON subscription endpoint\n\tSubTitle                    string `json:\"subTitle\" form:\"subTitle\"`                                       // Subscription title\n\tSubSupportUrl               string `json:\"subSupportUrl\" form:\"subSupportUrl\"`                             // Subscription support URL\n\tSubProfileUrl               string `json:\"subProfileUrl\" form:\"subProfileUrl\"`                             // Subscription profile URL\n\tSubAnnounce                 string `json:\"subAnnounce\" form:\"subAnnounce\"`                                 // Subscription announce\n\tSubEnableRouting            bool   `json:\"subEnableRouting\" form:\"subEnableRouting\"`                       // Enable routing for subscription\n\tSubRoutingRules             string `json:\"subRoutingRules\" form:\"subRoutingRules\"`                         // Subscription global routing rules (Only for Happ)\n\tSubListen                   string `json:\"subListen\" form:\"subListen\"`                                     // Subscription server listen IP\n\tSubPort                     int    `json:\"subPort\" form:\"subPort\"`                                         // Subscription server port\n\tSubPath                     string `json:\"subPath\" form:\"subPath\"`                                         // Base path for subscription URLs\n\tSubDomain                   string `json:\"subDomain\" form:\"subDomain\"`                                     // Domain for subscription server validation\n\tSubCertFile                 string `json:\"subCertFile\" form:\"subCertFile\"`                                 // SSL certificate file for subscription server\n\tSubKeyFile                  string `json:\"subKeyFile\" form:\"subKeyFile\"`                                   // SSL private key file for subscription server\n\tSubUpdates                  int    `json:\"subUpdates\" form:\"subUpdates\"`                                   // Subscription update interval in minutes\n\tExternalTrafficInformEnable bool   `json:\"externalTrafficInformEnable\" form:\"externalTrafficInformEnable\"` // Enable external traffic reporting\n\tExternalTrafficInformURI    string `json:\"externalTrafficInformURI\" form:\"externalTrafficInformURI\"`       // URI for external traffic reporting\n\tSubEncrypt                  bool   `json:\"subEncrypt\" form:\"subEncrypt\"`                                   // Encrypt subscription responses\n\tSubShowInfo                 bool   `json:\"subShowInfo\" form:\"subShowInfo\"`                                 // Show client information in subscriptions\n\tSubURI                      string `json:\"subURI\" form:\"subURI\"`                                           // Subscription server URI\n\tSubJsonPath                 string `json:\"subJsonPath\" form:\"subJsonPath\"`                                 // Path for JSON subscription endpoint\n\tSubJsonURI                  string `json:\"subJsonURI\" form:\"subJsonURI\"`                                   // JSON subscription server URI\n\tSubJsonFragment             string `json:\"subJsonFragment\" form:\"subJsonFragment\"`                         // JSON subscription fragment configuration\n\tSubJsonNoises               string `json:\"subJsonNoises\" form:\"subJsonNoises\"`                             // JSON subscription noise configuration\n\tSubJsonMux                  string `json:\"subJsonMux\" form:\"subJsonMux\"`                                   // JSON subscription mux configuration\n\tSubJsonRules                string `json:\"subJsonRules\" form:\"subJsonRules\"`\n\n\t// LDAP settings\n\tLdapEnable     bool   `json:\"ldapEnable\" form:\"ldapEnable\"`\n\tLdapHost       string `json:\"ldapHost\" form:\"ldapHost\"`\n\tLdapPort       int    `json:\"ldapPort\" form:\"ldapPort\"`\n\tLdapUseTLS     bool   `json:\"ldapUseTLS\" form:\"ldapUseTLS\"`\n\tLdapBindDN     string `json:\"ldapBindDN\" form:\"ldapBindDN\"`\n\tLdapPassword   string `json:\"ldapPassword\" form:\"ldapPassword\"`\n\tLdapBaseDN     string `json:\"ldapBaseDN\" form:\"ldapBaseDN\"`\n\tLdapUserFilter string `json:\"ldapUserFilter\" form:\"ldapUserFilter\"`\n\tLdapUserAttr   string `json:\"ldapUserAttr\" form:\"ldapUserAttr\"` // e.g., mail or uid\n\tLdapVlessField string `json:\"ldapVlessField\" form:\"ldapVlessField\"`\n\tLdapSyncCron   string `json:\"ldapSyncCron\" form:\"ldapSyncCron\"`\n\t// Generic flag configuration\n\tLdapFlagField         string `json:\"ldapFlagField\" form:\"ldapFlagField\"`\n\tLdapTruthyValues      string `json:\"ldapTruthyValues\" form:\"ldapTruthyValues\"`\n\tLdapInvertFlag        bool   `json:\"ldapInvertFlag\" form:\"ldapInvertFlag\"`\n\tLdapInboundTags       string `json:\"ldapInboundTags\" form:\"ldapInboundTags\"`\n\tLdapAutoCreate        bool   `json:\"ldapAutoCreate\" form:\"ldapAutoCreate\"`\n\tLdapAutoDelete        bool   `json:\"ldapAutoDelete\" form:\"ldapAutoDelete\"`\n\tLdapDefaultTotalGB    int    `json:\"ldapDefaultTotalGB\" form:\"ldapDefaultTotalGB\"`\n\tLdapDefaultExpiryDays int    `json:\"ldapDefaultExpiryDays\" form:\"ldapDefaultExpiryDays\"`\n\tLdapDefaultLimitIP    int    `json:\"ldapDefaultLimitIP\" form:\"ldapDefaultLimitIP\"`\n\t// JSON subscription routing rules\n}\n\n// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.\nfunc (s *AllSetting) CheckValid() error {\n\tif s.WebListen != \"\" {\n\t\tip := net.ParseIP(s.WebListen)\n\t\tif ip == nil {\n\t\t\treturn common.NewError(\"web listen is not valid ip:\", s.WebListen)\n\t\t}\n\t}\n\n\tif s.SubListen != \"\" {\n\t\tip := net.ParseIP(s.SubListen)\n\t\tif ip == nil {\n\t\t\treturn common.NewError(\"Sub listen is not valid ip:\", s.SubListen)\n\t\t}\n\t}\n\n\tif s.WebPort <= 0 || s.WebPort > math.MaxUint16 {\n\t\treturn common.NewError(\"web port is not a valid port:\", s.WebPort)\n\t}\n\n\tif s.SubPort <= 0 || s.SubPort > math.MaxUint16 {\n\t\treturn common.NewError(\"Sub port is not a valid port:\", s.SubPort)\n\t}\n\n\tif (s.SubPort == s.WebPort) && (s.WebListen == s.SubListen) {\n\t\treturn common.NewError(\"Sub and Web could not use same ip:port, \", s.SubListen, \":\", s.SubPort, \" & \", s.WebListen, \":\", s.WebPort)\n\t}\n\n\tif s.WebCertFile != \"\" || s.WebKeyFile != \"\" {\n\t\t_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)\n\t\tif err != nil {\n\t\t\treturn common.NewErrorf(\"cert file <%v> or key file <%v> invalid: %v\", s.WebCertFile, s.WebKeyFile, err)\n\t\t}\n\t}\n\n\tif s.SubCertFile != \"\" || s.SubKeyFile != \"\" {\n\t\t_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)\n\t\tif err != nil {\n\t\t\treturn common.NewErrorf(\"cert file <%v> or key file <%v> invalid: %v\", s.SubCertFile, s.SubKeyFile, err)\n\t\t}\n\t}\n\n\tif !strings.HasPrefix(s.WebBasePath, \"/\") {\n\t\ts.WebBasePath = \"/\" + s.WebBasePath\n\t}\n\tif !strings.HasSuffix(s.WebBasePath, \"/\") {\n\t\ts.WebBasePath += \"/\"\n\t}\n\tif !strings.HasPrefix(s.SubPath, \"/\") {\n\t\ts.SubPath = \"/\" + s.SubPath\n\t}\n\tif !strings.HasSuffix(s.SubPath, \"/\") {\n\t\ts.SubPath += \"/\"\n\t}\n\n\tif !strings.HasPrefix(s.SubJsonPath, \"/\") {\n\t\ts.SubJsonPath = \"/\" + s.SubJsonPath\n\t}\n\tif !strings.HasSuffix(s.SubJsonPath, \"/\") {\n\t\ts.SubJsonPath += \"/\"\n\t}\n\n\t_, err := time.LoadLocation(s.TimeLocation)\n\tif err != nil {\n\t\treturn common.NewError(\"time location not exist:\", s.TimeLocation)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "web/global/global.go",
    "content": "// Package global provides global variables and interfaces for accessing web and subscription servers.\npackage global\n\nimport (\n\t\"context\"\n\t_ \"unsafe\"\n\n\t\"github.com/robfig/cron/v3\"\n)\n\nvar (\n\twebServer WebServer\n\tsubServer SubServer\n)\n\n// WebServer interface defines methods for accessing the web server instance.\ntype WebServer interface {\n\tGetCron() *cron.Cron     // Get the cron scheduler\n\tGetCtx() context.Context // Get the server context\n\tGetWSHub() any           // Get the WebSocket hub (using any to avoid circular dependency)\n}\n\n// SubServer interface defines methods for accessing the subscription server instance.\ntype SubServer interface {\n\tGetCtx() context.Context // Get the server context\n}\n\n// SetWebServer sets the global web server instance.\nfunc SetWebServer(s WebServer) {\n\twebServer = s\n}\n\n// GetWebServer returns the global web server instance.\nfunc GetWebServer() WebServer {\n\treturn webServer\n}\n\n// SetSubServer sets the global subscription server instance.\nfunc SetSubServer(s SubServer) {\n\tsubServer = s\n}\n\n// GetSubServer returns the global subscription server instance.\nfunc GetSubServer() SubServer {\n\treturn subServer\n}\n"
  },
  {
    "path": "web/global/hashStorage.go",
    "content": "package global\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"regexp\"\n\t\"sync\"\n\t\"time\"\n)\n\n// HashEntry represents a stored hash entry with its value and timestamp.\ntype HashEntry struct {\n\tHash      string    // MD5 hash string\n\tValue     string    // Original value\n\tTimestamp time.Time // Time when the hash was created\n}\n\n// HashStorage provides thread-safe storage for hash-value pairs with expiration.\ntype HashStorage struct {\n\tsync.RWMutex\n\tData       map[string]HashEntry // Map of hash to entry\n\tExpiration time.Duration        // Expiration duration for entries\n}\n\n// NewHashStorage creates a new HashStorage instance with the specified expiration duration.\nfunc NewHashStorage(expiration time.Duration) *HashStorage {\n\treturn &HashStorage{\n\t\tData:       make(map[string]HashEntry),\n\t\tExpiration: expiration,\n\t}\n}\n\n// SaveHash generates an MD5 hash for the given query string and stores it with a timestamp.\nfunc (h *HashStorage) SaveHash(query string) string {\n\th.Lock()\n\tdefer h.Unlock()\n\n\tmd5Hash := md5.Sum([]byte(query))\n\tmd5HashString := hex.EncodeToString(md5Hash[:])\n\n\tentry := HashEntry{\n\t\tHash:      md5HashString,\n\t\tValue:     query,\n\t\tTimestamp: time.Now(),\n\t}\n\n\th.Data[md5HashString] = entry\n\n\treturn md5HashString\n}\n\n// GetValue retrieves the original value for the given hash, returning true if found.\nfunc (h *HashStorage) GetValue(hash string) (string, bool) {\n\th.RLock()\n\tdefer h.RUnlock()\n\n\tentry, exists := h.Data[hash]\n\n\treturn entry.Value, exists\n}\n\n// IsMD5 checks if the given string is a valid 32-character MD5 hash.\nfunc (h *HashStorage) IsMD5(hash string) bool {\n\tmatch, _ := regexp.MatchString(\"^[a-f0-9]{32}$\", hash)\n\treturn match\n}\n\n// RemoveExpiredHashes removes all hash entries that have exceeded the expiration duration.\nfunc (h *HashStorage) RemoveExpiredHashes() {\n\th.Lock()\n\tdefer h.Unlock()\n\n\tnow := time.Now()\n\n\tfor hash, entry := range h.Data {\n\t\tif now.Sub(entry.Timestamp) > h.Expiration {\n\t\t\tdelete(h.Data, hash)\n\t\t}\n\t}\n}\n\n// Reset clears all stored hash entries.\nfunc (h *HashStorage) Reset() {\n\th.Lock()\n\tdefer h.Unlock()\n\n\th.Data = make(map[string]HashEntry)\n}\n"
  },
  {
    "path": "web/html/common/page.html",
    "content": "{{ define \"page/head_start\" }}\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"renderer\" content=\"webkit\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <meta name=\"robots\" content=\"noindex,nofollow\">\n  <link rel=\"stylesheet\" href=\"{{ .base_path }}assets/ant-design-vue/antd.min.css\">\n  <link rel=\"stylesheet\" href=\"{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}\">\n  <style>\n    [v-cloak] {\n      display: none;\n    }\n    /* vazirmatn-regular - arabic_latin_latin-ext */\n    @font-face {\n      font-display: swap;\n      font-family: 'Vazirmatn';\n      font-style: normal;\n      font-weight: 400;\n      src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');\n      unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;\n    }\n    body {\n      font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n    }\n    \n    /* mobile touch scrolling for tabs */\n    @media (max-width: 576px) {\n      .ant-tabs-nav-container {\n        overflow-x: auto !important;\n        -webkit-overflow-scrolling: touch;\n        scroll-behavior: smooth;\n        overscroll-behavior-x: contain;\n        white-space: nowrap;\n        max-width: 100%;\n        padding: 0 !important; /* Remove padding for arrows */\n      }\n      .ant-tabs-nav-wrap {\n        overflow: visible !important;\n        padding: 0 !important;\n      }\n      .ant-tabs-nav-scroll {\n        overflow: visible !important;\n        box-shadow: none !important;\n      }\n      .ant-tabs-nav {\n         display: flex !important;\n         transform: none !important; /* Disable JS transform */\n         width: auto !important;\n         margin: 0 !important;\n      }\n      .ant-tabs-tab-prev,\n      .ant-tabs-tab-next {\n        display: none !important; /* Hide arrows */\n      }\n      .ant-tabs-nav-container::-webkit-scrollbar {\n        display: none;\n      }\n    }\n  </style>\n  <title>{{ .host }} – {{ i18n .title}}</title>\n{{ end }}\n\n{{ define \"page/head_end\" }}\n</head>\n{{ end }}\n\n{{ define \"page/body_start\" }}\n<body>\n  <div id=\"message\"></div>\n{{ end }}\n\n{{ define \"page/body_scripts\" }}\n<script src=\"{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/moment/moment.min.js\"></script>\n<script src=\"{{ .base_path }}assets/ant-design-vue/antd.min.js\"></script>\n<script src=\"{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/qs/qs.min.js\"></script>\n<script src=\"{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}\"></script>\n<script>\n  const basePath = '{{ .base_path }}';\n  axios.defaults.baseURL = basePath;\n</script>\n<script src=\"{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}\"></script>\n{{ end }}\n  \n{{ define \"page/body_end\" }}\n</body>\n</html>\n{{ end }}"
  },
  {
    "path": "web/html/component/aClientTable.html",
    "content": "{{define \"component/aClientTable\"}}\n<template slot=\"actions\" slot-scope=\"text, client, index\">\n  <a-tooltip>\n    <template slot=\"title\">{{ i18n \"qrCode\" }}</template>\n    <a-icon :style=\"{ fontSize: '22px', marginInlineStart: '14px' }\" class=\"normal-icon\" type=\"qrcode\" v-if=\"record.hasLink()\" @click=\"showQrcode(record.id,client);\"></a-icon>\n  </a-tooltip>\n  <a-tooltip>\n    <template slot=\"title\">{{ i18n \"pages.client.edit\" }}</template>\n    <a-icon :style=\"{ fontSize: '22px' }\" class=\"normal-icon\" type=\"edit\" @click=\"openEditClient(record.id,client);\"></a-icon>\n  </a-tooltip>\n  <a-tooltip>\n    <template slot=\"title\">{{ i18n \"info\" }}</template>\n    <a-icon :style=\"{ fontSize: '22px' }\" class=\"normal-icon\" type=\"info-circle\" @click=\"showInfo(record.id,client);\"></a-icon>\n  </a-tooltip>\n  <a-tooltip>\n    <template slot=\"title\">{{ i18n \"pages.inbounds.resetTraffic\" }}</template>\n    <a-popconfirm @confirm=\"resetClientTraffic(client,record.id,false)\" title='{{ i18n \"pages.inbounds.resetTrafficContent\"}}' :overlay-class-name=\"themeSwitcher.currentTheme\" ok-text='{{ i18n \"reset\"}}' cancel-text='{{ i18n \"cancel\"}}'>\n      <a-icon slot=\"icon\" type=\"question-circle-o\" :style=\"{ color: 'var(--color-primary-100)'}\"></a-icon>\n      <a-icon :style=\"{ fontSize: '22px', cursor: 'pointer' }\" class=\"normal-icon\" type=\"retweet\" v-if=\"client.email.length > 0\"></a-icon>\n    </a-popconfirm>\n  </a-tooltip>\n  <a-tooltip>\n    <template slot=\"title\">\n      <span :style=\"{ color: '#FF4D4F' }\"> {{ i18n \"delete\"}}</span>\n    </template>\n    <a-popconfirm @confirm=\"delClient(record.id,client,false)\" title='{{ i18n \"pages.inbounds.deleteClientContent\"}}' :overlay-class-name=\"themeSwitcher.currentTheme\" ok-text='{{ i18n \"delete\"}}' ok-type=\"danger\" cancel-text='{{ i18n \"cancel\"}}'>\n      <a-icon slot=\"icon\" type=\"question-circle-o\" :style=\"{ color: '#e04141' }\"></a-icon>\n      <a-icon :style=\"{ fontSize: '22px', cursor: 'pointer' }\" class=\"delete-icon\" type=\"delete\" v-if=\"isRemovable(record.id)\"></a-icon>\n    </a-popconfirm>\n  </a-tooltip>\n</template>\n<template slot=\"enable\" slot-scope=\"text, client, index\">\n  <a-switch v-model=\"client.enable\" @change=\"switchEnableClient(record.id,client)\"></a-switch>\n</template>\n<template slot=\"online\" slot-scope=\"text, client, index\">\n  <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n    <template slot=\"content\" >\n      {{ i18n \"lastOnline\" }}: [[ formatLastOnline(client.email) ]]\n    </template>\n    <template v-if=\"client.enable && isClientOnline(client.email)\">\n      <a-tag color=\"green\">{{ i18n \"online\" }}</a-tag>\n    </template>\n    <template v-else>\n      <a-tag>{{ i18n \"offline\" }}</a-tag>\n    </template>\n  </a-popover>\n</template>\n<template slot=\"client\" slot-scope=\"text, client\">\n  <a-space direction=\"horizontal\" :size=\"2\">\n    <a-tooltip>\n      <template slot=\"title\">\n        <template v-if=\"isClientDepleted(record, client.email)\">{{ i18n \"depleted\" }}</template>\n        <template v-else-if=\"!client.enable\">{{ i18n \"disabled\" }}</template>\n        <template v-else-if=\"client.enable && isClientOnline(client.email)\">{{ i18n \"online\" }}</template>\n      </template>\n      <a-badge :class=\"isClientOnline(client.email)? 'online-animation' : ''\" :color=\"client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'\"></a-badge>\n    </a-tooltip>\n    <a-space direction=\"vertical\" :size=\"2\">\n      <span class=\"client-email\">[[ client.email ]]</span>\n      <template v-if=\"client.comment && client.comment.trim()\">\n        <a-tooltip v-if=\"client.comment.length > 50\" :overlay-class-name=\"themeSwitcher.currentTheme\">\n          <template slot=\"title\">\n            [[ client.comment ]]\n          </template>\n          <span class=\"client-comment\">[[ client.comment.substring(0, 47) + '...' ]]</span>\n        </a-tooltip>\n        <span v-else class=\"client-comment\">[[ client.comment ]]</span>\n      </template>\n    </a-space>\n  </a-space>\n</template>\n<template slot=\"traffic\" slot-scope=\"text, client\">\n  <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n    <template slot=\"content\" v-if=\"client.email\">\n      <table cellpadding=\"2\" width=\"100%\">\n        <tr>\n          <td>↑[[ SizeFormatter.sizeFormat(getUpStats(record, client.email)) ]]</td>\n          <td>↓[[ SizeFormatter.sizeFormat(getDownStats(record, client.email)) ]]</td>\n        </tr>\n        <tr v-if=\"client.totalGB > 0\">\n          <td>{{ i18n \"remained\" }}</td>\n          <td>[[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]]</td>\n        </tr>\n      </table>\n    </template>\n    <table>\n      <tr class=\"tr-table-box\">\n        <td class=\"tr-table-rt\"> [[ SizeFormatter.sizeFormat(getSumStats(record, client.email)) ]] </td>\n        <td class=\"tr-table-bar\" v-if=\"!client.enable\">\n          <a-progress :stroke-color=\"themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'\" :show-info=\"false\" :percent=\"statsProgress(record, client.email)\" />\n        </td>\n        <td class=\"tr-table-bar\" v-else-if=\"client.totalGB > 0\">\n          <a-progress :stroke-color=\"clientStatsColor(record, client.email)\" :show-info=\"false\" :status=\"isClientDepleted(record, client.email)? 'exception' : ''\" :percent=\"statsProgress(record, client.email)\" />\n        </td>\n        <td v-else class=\"infinite-bar tr-table-bar\">\n          <a-progress :show-info=\"false\" :percent=\"100\"></a-progress>\n        </td>\n        <td class=\"tr-table-lt\">\n          <template v-if=\"client.totalGB > 0\">[[ client._totalGB + \"GB\" ]]</template>\n          <span v-else class=\"tr-infinity-ch\">&infin;</span>\n        </td>\n      </tr>\n    </table>\n  </a-popover>\n</template>\n\n<template slot=\"allTime\" slot-scope=\"text, client\">\n  <a-tag>[[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]]</a-tag>\n</template>\n<template slot=\"expiryTime\" slot-scope=\"text, client, index\">\n  <template v-if=\"client.expiryTime !=0 && client.reset >0\">\n    <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n      <template slot=\"content\">\n        <span v-if=\"client.expiryTime < 0\">{{ i18n \"pages.client.delayedStart\" }}</span>\n        <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>\n      </template>\n      <table>\n        <tr class=\"tr-table-box\">\n          <td class=\"tr-table-rt\"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>\n          <td class=\"infinite-bar tr-table-bar\">\n            <a-progress :show-info=\"false\" :status=\"isClientDepleted(record, client.email)? 'exception' : ''\" :percent=\"expireProgress(client.expiryTime, client.reset)\" />\n          </td>\n          <td class=\"tr-table-lt\">[[ client.reset + \"d\" ]]</td>\n        </tr>\n      </table>\n    </a-popover>\n  </template>\n  <template v-else>\n    <a-popover v-if=\"client.expiryTime != 0\" :overlay-class-name=\"themeSwitcher.currentTheme\">\n      <template slot=\"content\">\n        <span v-if=\"client.expiryTime < 0\">{{ i18n \"pages.client.delayedStart\" }}</span>\n        <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>\n      </template>\n      <a-tag :style=\"{ minWidth: '50px', border: 'none' }\" :color=\"ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)\"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>\n    </a-popover>\n    <a-tag v-else :color=\"ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)\" :style=\"{ border: 'none' }\" class=\"infinite-tag\">\n      <svg height=\"10px\" width=\"14px\" viewBox=\"0 0 640 512\" fill=\"currentColor\">\n        <path d=\"M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z\" fill=\"currentColor\"></path>\n      </svg>\n    </a-tag>\n  </template>\n</template>\n<template slot=\"actionMenu\" slot-scope=\"text, client, index\">\n  <a-dropdown :trigger=\"['click']\">\n    <a-icon @click=\"e => e.preventDefault()\" type=\"ellipsis\" :style=\"{ fontSize: '20px' }\"></a-icon>\n    <a-menu slot=\"overlay\" :theme=\"themeSwitcher.currentTheme\">\n      <a-menu-item v-if=\"record.hasLink()\" @click=\"showQrcode(record.id,client);\">\n        <a-icon :style=\"{ fontSize: '14px' }\" type=\"qrcode\"></a-icon>\n        {{ i18n \"qrCode\" }}\n      </a-menu-item>\n      <a-menu-item @click=\"openEditClient(record.id,client);\">\n        <a-icon :style=\"{ fontSize: '14px' }\" type=\"edit\"></a-icon>\n        {{ i18n \"pages.client.edit\" }}\n      </a-menu-item>\n      <a-menu-item @click=\"showInfo(record.id,client);\">\n        <a-icon :style=\"{ fontSize: '14px' }\" type=\"info-circle\"></a-icon>\n        {{ i18n \"info\" }}\n      </a-menu-item>\n      <a-menu-item @click=\"resetClientTraffic(client,record.id)\" v-if=\"client.email.length > 0\">\n        <a-icon :style=\"{ fontSize: '14px' }\" type=\"retweet\"></a-icon>\n        {{ i18n \"pages.inbounds.resetTraffic\" }}\n      </a-menu-item>\n      <a-menu-item v-if=\"isRemovable(record.id)\" @click=\"delClient(record.id,client)\">\n        <a-icon :style=\"{ fontSize: '14px' }\" type=\"delete\"></a-icon>\n        <span :style=\"{ color: '#FF4D4F' }\"> {{ i18n \"delete\"}}</span>\n      </a-menu-item>\n      <a-menu-item>\n        <a-switch v-model=\"client.enable\" size=\"small\" @change=\"switchEnableClient(record.id,client)\"></a-switch>\n        {{ i18n \"enable\"}}\n      </a-menu-item>\n    </a-menu>\n  </a-dropdown>\n</template>\n<template slot=\"info\" slot-scope=\"text, client, index\">\n  <a-popover placement=\"bottomRight\" :overlay-class-name=\"themeSwitcher.currentTheme\" trigger=\"click\">\n    <template slot=\"content\">\n      <table>\n        <tr>\n          <td colspan=\"3\" :style=\"{ textAlign: 'center' }\">{{ i18n \"pages.inbounds.traffic\" }}</td>\n        </tr>\n        <tr>\n          <td width=\"80px\" :style=\"{ margin: '0', textAlign: 'right', fontSize: '1em' }\"> [[ SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>\n          <td width=\"120px\" v-if=\"!client.enable\">\n            <a-progress :stroke-color=\"themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'\" :show-info=\"false\" :percent=\"statsProgress(record, client.email)\" />\n          </td>\n          <td width=\"120px\" v-else-if=\"client.totalGB > 0\">\n            <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n              <template slot=\"content\" v-if=\"client.email\">\n                <table cellpadding=\"2\" width=\"100%\">\n                  <tr>\n                    <td>↑[[ SizeFormatter.sizeFormat(getUpStats(record, client.email)) ]]</td>\n                    <td>↓[[ SizeFormatter.sizeFormat(getDownStats(record, client.email)) ]]</td>\n                  </tr>\n                  <tr>\n                    <td>{{ i18n \"remained\" }}</td>\n                    <td>[[ SizeFormatter.sizeFormat(getRemStats(record, client.email)) ]]</td>\n                  </tr>\n                </table>\n              </template>\n              <a-progress :stroke-color=\"clientStatsColor(record, client.email)\" :show-info=\"false\" :status=\"isClientDepleted(record, client.email)? 'exception' : ''\" :percent=\"statsProgress(record, client.email)\" />\n            </a-popover>\n          </td>\n          <td width=\"120px\" v-else class=\"infinite-bar\">\n            <a-progress :stroke-color=\"themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'\" :show-info=\"false\" :percent=\"100\"></a-progress>\n          </td>\n          <td width=\"80px\">\n            <template v-if=\"client.totalGB > 0\">[[ client._totalGB + \"GB\" ]]</template>\n            <span v-else class=\"tr-infinity-ch\">&infin;</span>\n          </td>\n        </tr>\n        <tr>\n          <td colspan=\"3\" :style=\"{ textAlign: 'center' }\">\n            <a-divider :style=\"{ margin: '0', borderCollapse: 'separate' }\"></a-divider>\n            {{ i18n \"pages.inbounds.expireDate\" }}\n          </td>\n        </tr>\n        <tr>\n          <template v-if=\"client.expiryTime !=0 && client.reset >0\">\n            <td width=\"80px\" :style=\"{ margin: '0', textAlign: 'right', fontSize: '1em' }\"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>\n            <td width=\"120px\" class=\"infinite-bar\">\n              <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n                <template slot=\"content\">\n                  <span v-if=\"client.expiryTime < 0\">{{ i18n \"pages.client.delayedStart\" }}</span>\n                  <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>\n                </template>\n                <a-progress :show-info=\"false\" :status=\"isClientDepleted(record, client.email)? 'exception' : ''\" :percent=\"expireProgress(client.expiryTime, client.reset)\" />\n              </a-popover>\n            </td>\n            <td width=\"60px\">[[ client.reset + \"d\" ]]</td>\n          </template>\n          <template v-else>\n            <td colspan=\"3\" :style=\"{ textAlign: 'center' }\">\n              <a-popover v-if=\"client.expiryTime != 0\" :overlay-class-name=\"themeSwitcher.currentTheme\">\n                <template slot=\"content\">\n                  <span v-if=\"client.expiryTime < 0\">{{ i18n \"pages.client.delayedStart\" }}</span>\n                  <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>\n                </template>\n                <a-tag :style=\"{ minWidth: '50px', border: 'none' }\" :color=\"ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)\"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>\n              </a-popover>\n              <a-tag v-else :color=\"client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'\" class=\"infinite-tag\">\n                <svg height=\"10px\" width=\"14px\" viewBox=\"0 0 640 512\" fill=\"currentColor\">\n                  <path d=\"M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z\" fill=\"currentColor\"></path>\n                </svg>\n              </a-tag>\n          </template>\n          </td>\n        </tr>\n      </table>\n    </template>\n    <a-badge>\n      <a-icon v-if=\"!client.enable\" slot=\"count\" type=\"pause-circle\" theme=\"filled\" :style=\"{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }\"></a-icon>\n      <a-button shape=\"round\" size=\"small\" :style=\"{ fontSize: '14px', padding: '0 10px' }\">\n        <a-icon type=\"solution\"></a-icon>\n      </a-button>\n    </a-badge>\n  </a-popover>\n</template>\n<template slot=\"createdAt\" slot-scope=\"text, client, index\">\n  <template v-if=\"client.created_at\">\n    [[ IntlUtil.formatDate(client.created_at) ]]\n  </template>\n  <template v-else>\n    -\n  </template>\n</template>\n<template slot=\"updatedAt\" slot-scope=\"text, client, index\">\n  <template v-if=\"client.updated_at\">\n    [[ IntlUtil.formatDate(client.updated_at) ]]\n  </template>\n  <template v-else>\n    -\n  </template>\n</template>\n{{end}}\n"
  },
  {
    "path": "web/html/component/aCustomStatistic.html",
    "content": "{{define \"component/customStatistic\"}}\n<template>\n    <a-statistic :title=\"title\" :value=\"value\">\n        <template #prefix>\n            <slot name=\"prefix\"></slot>\n        </template>\n        <template #suffix>\n            <slot name=\"suffix\"></slot>\n        </template>\n    </a-statistic>\n</template>\n{{end}}\n\n{{define \"component/aCustomStatistic\"}}\n<style>\n  .dark .ant-statistic-content {\n    color: var(--dark-color-text-primary)\n  }\n  .dark .ant-statistic-title {\n    color: rgba(255, 255, 255, 0.55)\n  }\n  .ant-statistic-content {\n    font-size: 16px;\n  }\n</style>\n\n<script>\n  Vue.component('a-custom-statistic', {\n    props: {\n      'title': {\n        type: String,\n        required: false,\n      },\n      'value': {\n        type: String,\n        required: false\n      }\n    },\n    template: `{{template \"component/customStatistic\"}}`,\n  });\n</script>\n{{end}}"
  },
  {
    "path": "web/html/component/aPersianDatepicker.html",
    "content": "{{define \"component/persianDatepickerTemplate\"}}\n<template>\n    <div>\n        <a-input :value=\"value\" type=\"text\" v-model=\"date\" data-jdp class=\"persian-datepicker\"\n            @input=\"$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();\"\n            :placeholder=\"placeholder\">\n            <template #addonAfter>\n                <a-icon type=\"calendar\" :style=\"{ fontSize: '14px', opacity: '0.5' }\" />\n            </template>\n        </a-input>\n    </div>\n</template>\n{{end}}\n\n{{define \"component/aPersianDatepicker\"}}\n<link rel=\"stylesheet\" href=\"{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.css?{{ .cur_ver }}\" />\n<script src=\"{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/persian-datepicker/persian-datepicker.min.js?{{ .cur_ver }}\"></script>\n<script>\n    const persianDatepicker = {};\n\n    Vue.component('a-persian-datepicker', {\n        props: {\n            'format': {\n                type: undefined,\n                required: false,\n            },\n            'value': {\n                type: String,\n                required: false,\n            },\n            'placeholder': {\n                type: String,\n                required: false,\n            },\n        },\n        template: `{{template \"component/persianDatepickerTemplate\"}}`,\n        data() {\n            return {\n                date: '',\n                persianDatepicker,\n            };\n        },\n        watch: {\n            value: function (date) {\n                this.date = this.convertToJalalian(date)\n            }\n        },\n        mounted() {\n            this.date = this.convertToJalalian(this.value)\n            this.listenToDatepicker()\n        },\n        methods: {\n            convertToGregorian(date) {\n                return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) : null\n            },\n            convertToJalalian(date) {\n                return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null\n            },\n            listenToDatepicker() {\n                jalaliDatepicker.startWatch({\n                    time: true,\n                    zIndex: '9999',\n                    hideAfterChange: true,\n                    useDropDownYears: false,\n                    changeMonthRotateYear: true,\n                });\n            },\n        }\n    });\n</script>\n{{end}}"
  },
  {
    "path": "web/html/component/aSettingListItem.html",
    "content": "{{define \"component/settingListItem\"}}\n<a-list-item :style=\"{ padding: padding }\">\n    <a-row :gutter=\"[8,16]\">\n        <a-col :lg=\"24\" :xl=\"12\">\n            <a-list-item-meta>\n                <template #title>\n                    <slot name=\"title\"></slot>\n                </template>\n                <template #description>\n                    <slot name=\"description\"></slot>\n                </template>\n            </a-list-item-meta>\n        </a-col>\n        <a-col :lg=\"24\" :xl=\"12\">\n            <slot name=\"control\"></slot>\n        </a-col>\n    </a-row>\n</a-list-item>\n{{end}}\n\n{{define \"component/aSettingListItem\"}}\n<script>\n    Vue.component('a-setting-list-item', {\n        props: {\n            'paddings': {\n                type: String,\n                required: false,\n                defaultValue: \"default\",\n                validator: function (value) {\n                    return ['small', 'default'].includes(value)\n                }\n            }\n        },\n        template: `{{ template \"component/settingListItem\" }}`,\n        computed: {\n            padding() {\n                switch (this.paddings) {\n                    case \"small\":\n                        return \"10px 20px !important\"\n                        break;\n                    case \"default\":\n                        return \"20px !important\"\n                        break;\n                }\n            }\n        }\n    })\n</script>\n{{end}}\n"
  },
  {
    "path": "web/html/component/aSidebar.html",
    "content": "{{define \"component/sidebar/content\"}}\n<template>\n    <div class=\"ant-sidebar\">\n        <a-layout-sider :theme=\"themeSwitcher.currentTheme\" collapsible :collapsed=\"collapsed\"\n            @collapse=\"(isCollapsed, type) => collapseHandle(isCollapsed, type)\" breakpoint=\"md\">\n            <a-theme-switch></a-theme-switch>\n            <a-menu :theme=\"themeSwitcher.currentTheme\" mode=\"inline\" :selected-keys=\"activeTab\"\n                @click=\"({key}) => openLink(key)\">\n                <a-menu-item v-for=\"tab in tabs\" :key=\"tab.key\">\n                    <a-icon :type=\"tab.icon\"></a-icon>\n                    <span v-text=\"tab.title\"></span>\n                </a-menu-item>\n            </a-menu>\n        </a-layout-sider>\n        <a-drawer placement=\"left\" :closable=\"false\" @close=\"closeDrawer\" :visible=\"visible\"\n            :wrap-class-name=\"themeSwitcher.currentTheme\" :wrap-style=\"{ padding: 0 }\" :style=\"{ height: '100%' }\">\n            <div class=\"drawer-handle\" @click=\"toggleDrawer\" slot=\"handle\">\n                <a-icon :type=\"visible ? 'close' : 'menu-fold'\"></a-icon>\n            </div>\n            <a-theme-switch></a-theme-switch>\n            <a-menu :theme=\"themeSwitcher.currentTheme\" mode=\"inline\" :selected-keys=\"activeTab\"\n                @click=\"({key}) => openLink(key)\">\n                <a-menu-item v-for=\"tab in tabs\" :key=\"tab.key\">\n                    <a-icon :type=\"tab.icon\"></a-icon>\n                    <span v-text=\"tab.title\"></span>\n                </a-menu-item>\n            </a-menu>\n        </a-drawer>\n    </div>\n</template>\n{{end}}\n\n{{define \"component/aSidebar\"}}\n<style>\n    .ant-sidebar>.ant-layout-sider {\n        height: 100%;\n    }\n</style>\n\n<script>\n    const SIDEBAR_COLLAPSED_KEY = \"isSidebarCollapsed\"\n\n    Vue.component('a-sidebar', {\n        data() {\n            return {\n                tabs: [\n                    {\n                        key: '{{ .base_path }}panel/',\n                        icon: 'dashboard',\n                        title: '{{ i18n \"menu.dashboard\"}}'\n                    },\n                    {\n                        key: '{{ .base_path }}panel/inbounds',\n                        icon: 'user',\n                        title: '{{ i18n \"menu.inbounds\"}}'\n                    },\n                    {\n                        key: '{{ .base_path }}panel/settings',\n                        icon: 'setting',\n                        title: '{{ i18n \"menu.settings\"}}'\n                    },\n                    {\n                        key: '{{ .base_path }}panel/xray',\n                        icon: 'tool',\n                        title: '{{ i18n \"menu.xray\"}}'\n                    },\n                    {\n                        key: '{{ .base_path }}logout/',\n                        icon: 'logout',\n                        title: '{{ i18n \"menu.logout\"}}'\n                    },\n                ],\n                activeTab: [\n                    '{{ .request_uri }}'\n                ],\n                visible: false,\n                collapsed: JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY)),\n            }\n        },\n        methods: {\n            openLink(key) {\n                return key.startsWith('http') ? \n                    window.open(key) : \n                    location.href = key\n            },\n            closeDrawer() {\n                this.visible = false;\n            },\n            toggleDrawer() {\n                this.visible = !this.visible;\n            },\n            collapseHandle(collapsed, type) {\n                if (type === \"clickTrigger\") {\n                    localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed);\n\n                    this.collapsed = JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY));\n                }\n            }\n        },\n        template: `{{template \"component/sidebar/content\"}}`,\n    });\n</script>\n{{end}}"
  },
  {
    "path": "web/html/component/aTableSortable.html",
    "content": "{{define \"component/sortableTableTrigger\"}}\n<a-icon type=\"drag\" class=\"sortable-icon\" :style=\"{ cursor: 'move' }\" @mouseup=\"mouseUpHandler\" @mousedown=\"mouseDownHandler\"\n  @click=\"clickHandler\" />\n{{end}}\n\n{{define \"component/aTableSortable\"}}\n<script>\n  const DRAGGABLE_ROW_CLASS = 'draggable-row';\n  const findParentRowElement = (el) => {\n    if (!el || !el.tagName) {\n      return null;\n    } else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) {\n      return el;\n    } else if (el.parentNode) {\n      return findParentRowElement(el.parentNode);\n    } else {\n      return null;\n    }\n  }\n  Vue.component('a-table-sortable', {\n    data() {\n      return {\n        sortingElementIndex: null,\n        newElementIndex: null,\n      };\n    },\n    props: {\n      'data-source': {\n        type: undefined,\n        required: false,\n      },\n      'customRow': {\n        type: undefined,\n        required: false,\n      }\n    },\n    inheritAttrs: false,\n    provide() {\n      const sortable = {}\n      Object.defineProperty(sortable, \"setSortableIndex\", {\n        enumerable: true,\n        get: () => this.setCurrentSortableIndex,\n      });\n      Object.defineProperty(sortable, \"resetSortableIndex\", {\n        enumerable: true,\n        get: () => this.resetSortableIndex,\n      });\n      return {\n        sortable,\n      }\n    },\n    render: function (createElement) {\n      return createElement('a-table', {\n        class: {\n          'ant-table-is-sorting': this.isDragging(),\n        },\n        props: {\n          ...this.$attrs,\n          'data-source': this.records,\n          customRow: (record, index) => this.customRowRender(record, index),\n        },\n        on: this.$listeners,\n        nativeOn: {\n          drop: (e) => this.dropHandler(e),\n        },\n        scopedSlots: this.$scopedSlots,\n        locale: { \n          filterConfirm: `{{ i18n \"confirm\" }}`, \n          filterReset: `{{ i18n \"reset\" }}`, \n          emptyText: `{{ i18n \"noData\" }}` \n        }\n      }, this.$slots.default,)\n    },\n    created() {\n      this.$memoSort = {};\n    },\n    methods: {\n      isDragging() {\n        const currentIndex = this.sortingElementIndex;\n        return currentIndex !== null && currentIndex !== undefined;\n      },\n      resetSortableIndex(e, index) {\n        this.sortingElementIndex = null;\n        this.newElementIndex = null;\n        this.$memoSort = {};\n      },\n      setCurrentSortableIndex(e, index) {\n        this.sortingElementIndex = index;\n      },\n      dragStartHandler(e, index) {\n        if (!this.isDragging()) {\n          e.preventDefault();\n          return;\n        }\n        const hideDragImage = this.$el.cloneNode(true);\n        hideDragImage.id = \"hideDragImage-hide\";\n        hideDragImage.style.opacity = 0;\n        e.dataTransfer.setDragImage(hideDragImage, 0, 0);\n      },\n      dragStopHandler(e, index) {\n        const hideDragImage = document.getElementById('hideDragImage-hide');\n        if (hideDragImage) hideDragImage.remove();\n        this.resetSortableIndex(e, index);\n      },\n      dragOverHandler(e, index) {\n        if (!this.isDragging()) {\n          return;\n        }\n        e.preventDefault();\n        const currentIndex = this.sortingElementIndex;\n        if (index === currentIndex) {\n          this.newElementIndex = null;\n          return;\n        }\n        const row = findParentRowElement(e.target);\n        if (!row) {\n          return;\n        }\n        const rect = row.getBoundingClientRect();\n        const offsetTop = e.pageY - rect.top;\n        if (offsetTop < rect.height / 2) {\n          this.newElementIndex = Math.max(index - 1, 0);\n        } else {\n          this.newElementIndex = index;\n        }\n      },\n      dropHandler(e) {\n        if (this.isDragging()) {\n          this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);\n        }\n      },\n      customRowRender(record, index) {\n        const parentMethodResult = this.customRow?.(record, index) || {};\n        const newIndex = this.newElementIndex;\n        const currentIndex = this.sortingElementIndex;\n        return {\n          ...parentMethodResult,\n          attrs: {\n            ...(parentMethodResult?.attrs || {}),\n            draggable: true,\n          },\n          on: {\n            ...(parentMethodResult?.on || {}),\n            dragstart: (e) => this.dragStartHandler(e, index),\n            dragend: (e) => this.dragStopHandler(e, index),\n            dragover: (e) => this.dragOverHandler(e, index),\n          },\n          class: {\n            ...(parentMethodResult?.class || {}),\n            [DRAGGABLE_ROW_CLASS]: true,\n            ['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) : false,\n          },\n        };\n      }\n    },\n    computed: {\n      records() {\n        const newIndex = this.newElementIndex;\n        const currentIndex = this.sortingElementIndex;\n        if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {\n          return this.dataSource;\n        }\n        if (this.$memoSort.newIndex === newIndex) {\n          return this.$memoSort.list;\n        }\n        let list = [...this.dataSource];\n        list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);\n        this.$memoSort = {\n          newIndex,\n          list,\n        };\n        return list;\n      }\n    }\n  });\n  Vue.component('a-table-sort-trigger', {\n    template: `{{template \"component/sortableTableTrigger\"}}`,\n    props: {\n      'item-index': {\n        type: undefined,\n        required: false\n      }\n    },\n    inject: ['sortable'],\n    methods: {\n      mouseDownHandler(e) {\n        if (this.sortable) {\n          this.sortable.setSortableIndex(e, this.itemIndex);\n        }\n      },\n      mouseUpHandler(e) {\n        if (this.sortable) {\n          this.sortable.resetSortableIndex(e, this.itemIndex);\n        }\n      },\n      clickHandler(e) {\n        e.preventDefault();\n      },\n    }\n  })\n</script>\n<style>\n  @media only screen and (max-width: 767px) {\n    .sortable-icon {\n      display: none;\n    }\n  }\n\n  .ant-table-is-sorting .draggable-row td {\n    background-color: #ffffff !important;\n  }\n\n  .dark .ant-table-is-sorting .draggable-row td {\n    background-color: var(--dark-color-surface-100) !important;\n  }\n\n  .ant-table-is-sorting .dragging td {\n    background-color: rgb(232 244 242) !important;\n    color: rgba(0, 0, 0, 0.3);\n  }\n\n  .dark .ant-table-is-sorting .dragging td {\n    background-color: var(--dark-color-table-hover) !important;\n    color: rgba(255, 255, 255, 0.3);\n  }\n\n  .ant-table-is-sorting .dragging {\n    opacity: 1;\n    box-shadow: 1px -2px 2px #008771;\n    transition: all 0.2s;\n  }\n\n  .ant-table-is-sorting .dragging .ant-table-row-index {\n    opacity: 0.3;\n  }\n</style>\n{{end}}"
  },
  {
    "path": "web/html/component/aThemeSwitch.html",
    "content": "{{define \"component/themeSwitchTemplate\"}}\n<template>\n  <a-menu :theme=\"themeSwitcher.currentTheme\" mode=\"inline\" selected-keys=\"\">\n    <a-sub-menu>\n      <span slot=\"title\">\n        <a-icon type=\"bulb\" :theme=\"themeSwitcher.isDarkTheme ? 'filled' : 'outlined'\"></a-icon>\n        <span>{{ i18n \"menu.theme\" }}</span>\n      </span>\n      <a-menu-item id=\"change-theme\" class=\"ant-menu-theme-switch\" @mousedown=\"themeSwitcher.animationsOff()\">\n        <span>{{ i18n \"menu.dark\" }}</span>\n        <a-switch :style=\"{ marginLeft: '2px' }\" size=\"small\" :default-checked=\"themeSwitcher.isDarkTheme\"\n          @change=\"themeSwitcher.toggleTheme()\"></a-switch>\n      </a-menu-item>\n      <a-menu-item id=\"change-theme-ultra\" v-if=\"themeSwitcher.isDarkTheme\" class=\"ant-menu-theme-switch\"\n        @mousedown=\"themeSwitcher.animationsOffUltra()\">\n        <span>{{ i18n \"menu.ultraDark\" }}</span>\n        <a-checkbox :style=\"{ marginLeft: '2px' }\" :checked=\"themeSwitcher.isUltra\"\n          @click=\"themeSwitcher.toggleUltra()\"></a-checkbox>\n      </a-menu-item>\n    </a-sub-menu>\n  </a-menu>\n</template>\n{{end}}\n\n{{define \"component/themeSwitchTemplateLogin\"}}\n<template>\n  <a-space @mousedown=\"themeSwitcher.animationsOff()\" id=\"change-theme\" direction=\"vertical\" :size=\"10\" :style=\"{ width: '100%' }\">\n    <a-space direction=\"horizontal\" size=\"small\">\n      <a-switch size=\"small\" :default-checked=\"themeSwitcher.isDarkTheme\" @change=\"themeSwitcher.toggleTheme()\"></a-switch>\n      <span>{{ i18n \"menu.dark\" }}</span>\n    </a-space>\n    <a-space v-if=\"themeSwitcher.isDarkTheme\" direction=\"horizontal\" size=\"small\">\n      <a-checkbox :checked=\"themeSwitcher.isUltra\" @click=\"themeSwitcher.toggleUltra()\"></a-checkbox>\n      <span>{{ i18n \"menu.ultraDark\" }}</span>\n    </a-space>\n  </a-space>\n</template>\n{{end}}\n\n{{define \"component/aThemeSwitch\"}}\n<script>\n  function createThemeSwitcher() {\n    const isDarkTheme = localStorage.getItem('dark-mode') === 'true';\n    const isUltra = localStorage.getItem('isUltraDarkThemeEnabled') === 'true';\n    if (isUltra) {\n      document.documentElement.setAttribute('data-theme', 'ultra-dark');\n    }\n    const theme = isDarkTheme ? 'dark' : 'light';\n    document.querySelector('body').setAttribute('class', theme);\n    return {\n      animationsOff() {\n        document.documentElement.setAttribute('data-theme-animations', 'off');\n        const themeAnimations = document.querySelector('#change-theme');\n        themeAnimations.addEventListener('mouseleave', () => {\n          document.documentElement.removeAttribute('data-theme-animations');\n        });\n        themeAnimations.addEventListener('touchend', () => {\n          document.documentElement.removeAttribute('data-theme-animations');\n        });\n      },\n      animationsOffUltra() {\n        document.documentElement.setAttribute('data-theme-animations', 'off');\n        const themeAnimationsUltra = document.querySelector('#change-theme-ultra');\n        themeAnimationsUltra.addEventListener('mouseleave', () => {\n          document.documentElement.removeAttribute('data-theme-animations');\n        });\n        themeAnimationsUltra.addEventListener('touchend', () => {\n          document.documentElement.removeAttribute('data-theme-animations');\n        });\n      },\n      isDarkTheme,\n      isUltra,\n      get currentTheme() {\n        return this.isDarkTheme ? 'dark' : 'light';\n      },\n      toggleTheme() {\n        this.isDarkTheme = !this.isDarkTheme;\n        localStorage.setItem('dark-mode', this.isDarkTheme);\n        document.querySelector('body').setAttribute('class', this.isDarkTheme ? 'dark' : 'light');\n        document.getElementById('message').className = themeSwitcher.currentTheme;\n      },\n      toggleUltra() {\n        this.isUltra = !this.isUltra;\n        if (this.isUltra) {\n          document.documentElement.setAttribute('data-theme', 'ultra-dark');\n        } else {\n          document.documentElement.removeAttribute('data-theme');\n        }\n        localStorage.setItem('isUltraDarkThemeEnabled', this.isUltra.toString());\n      }\n    };\n  }\n  const themeSwitcher = createThemeSwitcher();\n  Vue.component('a-theme-switch', {\n    template: `{{template \"component/themeSwitchTemplate\"}}`,\n    data: () => ({\n      themeSwitcher\n    }),\n    mounted() {\n      this.$message.config({\n        getContainer: () => document.getElementById('message')\n      });\n      document.getElementById('message').className = themeSwitcher.currentTheme;\n    }\n  });\n  Vue.component('a-theme-switch-login', {\n    template: `{{template \"component/themeSwitchTemplateLogin\"}}`,\n    data: () => ({\n      themeSwitcher\n    }),\n    mounted() {\n      this.$message.config({\n        getContainer: () => document.getElementById('message')\n      });\n      document.getElementById('message').className = themeSwitcher.currentTheme;\n    }\n  });\n</script>\n{{end}}"
  },
  {
    "path": "web/html/form/client.html",
    "content": "{{define \"form/client\"}}\n<a-form layout=\"horizontal\" v-if=\"client\" :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='{{ i18n \"pages.inbounds.enable\" }}'>\n        <a-switch v-model=\"client.enable\"></a-switch>\n    </a-form-item>\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.inbounds.emailDesc\" }}</span>\n                </template>\n                {{ i18n \"pages.inbounds.email\" }}\n                <a-icon type=\"sync\" @click=\"client.email = RandomUtil.randomLowerAndNum(9)\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"client.email\"></a-input>\n    </a-form-item>\n    <a-form-item v-if=\"inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS\">\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"reset\" }}</span>\n                </template>\n                {{ i18n \"password\" }}\n                <a-icon v-if=\"inbound.protocol === Protocols.SHADOWSOCKS\" @click=\"client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)\" type=\"sync\"></a-icon>\n                <a-icon v-if=\"inbound.protocol === Protocols.TROJAN\" @click=\"client.password = RandomUtil.randomSeq(10)\"type=\"sync\"> </a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"client.password\"></a-input>\n    </a-form-item>\n    <a-form-item v-if=\"inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS\">\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"reset\" }}</span>\n                </template>\n                ID <a-icon @click=\"client.id = RandomUtil.randomUUID()\" type=\"sync\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"client.id\"></a-input>\n    </a-form-item>\n    <a-form-item v-if=\"inbound.protocol === Protocols.VMESS\" label='{{ i18n \"security\" }}'>\n        <a-select v-model=\"client.security\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"key in USERS_SECURITY\" :value=\"key\">[[ key ]]</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item v-if=\"client.email && app.subSettings?.enable\">\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.inbounds.subscriptionDesc\" }}</span>\n                </template>\n                Subscription\n                <a-icon @click=\"client.subId = RandomUtil.randomLowerAndNum(16)\" type=\"sync\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"client.subId\"></a-input>\n    </a-form-item>\n    <a-form-item v-if=\"client.email && app.tgBotEnable\">\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.inbounds.telegramDesc\" }}</span>\n                </template>\n                Telegram ChatID\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input-number :style=\"{ width: '50%' }\" v-model.number=\"client.tgId\" min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item v-if=\"client.email\" label='{{ i18n \"comment\" }}'>\n        <a-input v-model.trim=\"client.comment\"></a-input>\n    </a-form-item>\n    <a-form-item v-if=\"app.ipLimitEnable\">\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.inbounds.IPLimitDesc\"}}</span>\n                </template>\n                    <span>{{ i18n \"pages.inbounds.IPLimit\"}} </span>\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input-number v-model.number=\"client.limitIp\" min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item v-if=\"app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit\">\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.inbounds.IPLimitlogDesc\" }}</span>\n                </template>\n                    <span>{{ i18n \"pages.inbounds.IPLimitlog\" }} </span>\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-tooltip>\n            <template slot=\"title\">\n                <span>{{ i18n \"pages.inbounds.IPLimitlogclear\" }}</span>\n            </template>\n            <span :style=\"{ color: '#FF4D4F' }\">\n                <a-icon type=\"delete\" @click=\"clearDBClientIps(client.email)\"></a-icon>\n            </span>\n        </a-tooltip>\n        <a-form layout=\"block\">\n            <a-textarea id=\"clientIPs\" readonly @click=\"getDBClientIps(client.email)\" placeholder=\"Click To Get IPs\"\n                :auto-size=\"{ minRows: 5, maxRows: 10 }\">\n            </a-textarea>\n        </a-form>\n    </a-form-item>\n    <a-form-item v-if=\"inbound.canEnableTlsFlow()\" label='Flow'>\n        <a-select v-model=\"client.flow\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"\" selected>{{ i18n \"none\" }}</a-select-option>\n            <a-select-option v-for=\"key in TLS_FLOW_CONTROL\" :value=\"key\">[[ key ]]</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    0 <span>{{ i18n \"pages.inbounds.meansNoLimit\" }}</span>\n                </template>\n                {{ i18n \"pages.inbounds.totalFlow\" }}\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input-number v-model.number=\"client._totalGB\" :min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item v-if=\"isEdit && clientStats\" label='{{ i18n \"usage\" }}'>\n        <a-tag :color=\"ColorUtils.clientUsageColor(clientStats, app.trafficDiff)\">\n            [[ SizeFormatter.sizeFormat(clientStats.up) ]] /\n            [[ SizeFormatter.sizeFormat(clientStats.down) ]]\n            ([[ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]])\n        </a-tag>\n        <a-tooltip>\n            <template slot=\"title\">{{ i18n \"pages.inbounds.resetTraffic\" }}</template>\n            <a-icon type=\"retweet\"\n                @click=\"resetClientTraffic(client.email,clientStats.inboundId,$event.target)\"\n                v-if=\"client.email.length > 0\"></a-icon>\n        </a-tooltip>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.client.delayedStart\" }}'>\n        <a-switch v-model=\"delayedStart\" @click=\"client._expiryTime=0\"></a-switch>\n    </a-form-item>\n    <a-form-item v-if=\"delayedStart\" label='{{ i18n \"pages.client.expireDays\" }}'>\n        <a-input-number v-model.number=\"delayedExpireDays\" :min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item v-else>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">{{ i18n \"pages.inbounds.leaveBlankToNeverExpire\" }}</template>\n                {{ i18n \"pages.inbounds.expireDate\" }}\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-date-picker v-if=\"datepicker == 'gregorian'\" :show-time=\"{ format: 'HH:mm:ss' }\" format=\"YYYY-MM-DD HH:mm:ss\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\" v-model=\"client._expiryTime\"></a-date-picker>\n        <a-persian-datepicker v-else placeholder='{{ i18n \"pages.settings.datepickerPlaceholder\" }}'\n                            value=\"client._expiryTime\" v-model=\"client._expiryTime\"></a-persian-datepicker>\n        <a-tag color=\"red\" v-if=\"isEdit && isExpiry\">Expired</a-tag>\n    </a-form-item>\n    <a-form-item v-if=\"client.expiryTime != 0\">\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">{{ i18n \"pages.client.renewDesc\" }}</template>\n                {{ i18n \"pages.client.renew\" }}\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input-number v-model.number=\"client.reset\" :min=\"0\"></a-input-number>\n    </a-form-item>\n</a-form>\n{{end}}"
  },
  {
    "path": "web/html/form/inbound.html",
    "content": "{{define \"form/inbound\"}}\n<!-- base -->\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\"\n    :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='{{ i18n \"enable\" }}'>\n        <a-switch v-model=\"dbInbound.enable\"></a-switch>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"remark\" }}'>\n        <a-input v-model.trim=\"dbInbound.remark\"></a-input>\n    </a-form-item>\n\n    <a-form-item label='{{ i18n \"protocol\" }}'>\n        <a-select v-model=\"inbound.protocol\" :disabled=\"isEdit\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"p in Protocols\" :key=\"p\" :value=\"p\">[[ p\n                ]]</a-select-option>\n        </a-select>\n    </a-form-item>\n\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.inbounds.monitorDesc\" }}</span>\n                </template>\n                {{ i18n \"monitor\" }}\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"inbound.listen\"></a-input>\n    </a-form-item>\n\n    <a-form-item label='{{ i18n \"pages.inbounds.port\" }}'>\n        <a-input-number v-model.number=\"inbound.port\" :min=\"1\"\n            :max=\"65535\"></a-input-number>\n    </a-form-item>\n\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    0 <span>{{ i18n \"pages.inbounds.meansNoLimit\" }}</span>\n                </template>\n                {{ i18n \"pages.inbounds.totalFlow\" }}\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input-number v-model.number=\"dbInbound.totalGB\"\n            :min=\"0\"></a-input-number>\n    </a-form-item>\n\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.inbounds.periodicTrafficResetDesc\"\n                        }}</span>\n                    <br\n                        v-if=\"dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0\">\n                    <span\n                        v-if=\"dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0\">\n                        <strong>{{ i18n \"pages.inbounds.lastReset\" }}:</strong>\n                        <span>[[\n                            IntlUtil.formatDate(dbInbound.lastTrafficResetTime)\n                            ]]</span>\n                    </span>\n                </template>\n                {{ i18n \"pages.inbounds.periodicTrafficResetTitle\" }}\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-select v-model=\"dbInbound.trafficReset\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"never\">{{ i18n\n                \"pages.inbounds.periodicTrafficReset.never\" }}</a-select-option>\n            <a-select-option value=\"daily\">{{ i18n\n                \"pages.inbounds.periodicTrafficReset.daily\" }}</a-select-option>\n            <a-select-option value=\"weekly\">{{ i18n\n                \"pages.inbounds.periodicTrafficReset.weekly\"\n                }}</a-select-option>\n            <a-select-option value=\"monthly\">{{ i18n\n                \"pages.inbounds.periodicTrafficReset.monthly\"\n                }}</a-select-option>\n        </a-select>\n    </a-form-item>\n\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.inbounds.leaveBlankToNeverExpire\"\n                        }}</span>\n                </template>\n                {{ i18n \"pages.inbounds.expireDate\" }}\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-date-picker :style=\"{ width: '100%' }\"\n            v-if=\"datepicker == 'gregorian'\" :show-time=\"{ format: 'HH:mm:ss' }\"\n            format=\"YYYY-MM-DD HH:mm:ss\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\"\n            v-model=\"dbInbound._expiryTime\"></a-date-picker>\n        <a-persian-datepicker v-else\n            placeholder='{{ i18n \"pages.settings.datepickerPlaceholder\" }}'\n            value=\"dbInbound._expiryTime\" v-model=\"dbInbound._expiryTime\">\n        </a-persian-datepicker>\n    </a-form-item>\n</a-form>\n\n<!-- vmess settings -->\n<template v-if=\"inbound.protocol === Protocols.VMESS\">\n    {{template \"form/vmess\"}}\n</template>\n\n<!-- vless settings -->\n<template v-if=\"inbound.protocol === Protocols.VLESS\">\n    {{template \"form/vless\"}}\n</template>\n\n<!-- trojan settings -->\n<template v-if=\"inbound.protocol === Protocols.TROJAN\">\n    {{template \"form/trojan\"}}\n</template>\n\n<!-- shadowsocks -->\n<template v-if=\"inbound.protocol === Protocols.SHADOWSOCKS\">\n    {{template \"form/shadowsocks\"}}\n</template>\n\n<!-- tunnel -->\n<template v-if=\"inbound.protocol === Protocols.TUNNEL\">\n    {{template \"form/tunnel\"}}\n</template>\n\n<!-- mixed -->\n<template v-if=\"inbound.protocol === Protocols.MIXED\">\n    {{template \"form/mixed\"}}\n</template>\n\n<!-- http -->\n<template v-if=\"inbound.protocol === Protocols.HTTP\">\n    {{template \"form/http\"}}\n</template>\n\n<!-- wireguard -->\n<template v-if=\"inbound.protocol === Protocols.WIREGUARD\">\n    {{template \"form/wireguard\"}}\n</template>\n\n<!-- tun -->\n<template v-if=\"inbound.protocol === Protocols.TUN\">\n    {{template \"form/tun\"}}\n</template>\n\n<!-- stream settings -->\n<template v-if=\"inbound.canEnableStream()\">\n    {{template \"form/streamSettings\"}}\n    {{template \"form/externalProxy\" }}\n</template>\n\n<!-- tls settings -->\n<template v-if=\"inbound.canEnableTls()\">\n    {{template \"form/tlsSettings\"}}\n</template>\n\n<!-- sniffing -->\n<a-collapse>\n    <a-collapse-panel header='Sniffing'>\n        {{template \"form/sniffing\"}}\n    </a-collapse-panel>\n</a-collapse>\n\n{{end}}"
  },
  {
    "path": "web/html/form/outbound.html",
    "content": "{{define \"form/outbound\"}}\n<!-- base -->\n<a-tabs :active-key=\"outModal.activeKey\"\n  :style=\"{ padding: '0', backgroundColor: 'transparent' }\"\n  @change=\"(activeKey) => {outModal.toggleJson(activeKey == '2'); }\">\n  <a-tab-pane key=\"1\" tab=\"Form\">\n    <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\"\n      :wrapper-col=\"{ md: {span:14} }\">\n      <a-form-item label='{{ i18n \"protocol\" }}'>\n        <a-select v-model=\"outbound.protocol\"\n          :dropdown-class-name=\"themeSwitcher.currentTheme\">\n          <a-select-option v-for=\"x,y in Protocols\" :value=\"x\">[[ y\n            ]]</a-select-option>\n        </a-select>\n      </a-form-item>\n      <a-form-item label='{{ i18n \"pages.xray.outbound.tag\" }}' has-feedback\n        :validate-status=\"outModal.duplicateTag? 'warning' : 'success'\">\n        <a-input v-model.trim=\"outbound.tag\" @change=\"outModal.check()\"\n          placeholder='{{ i18n \"pages.xray.outbound.tagDesc\" }}'></a-input>\n      </a-form-item>\n      <a-form-item label='{{ i18n \"pages.xray.outbound.sendThrough\" }}'>\n        <a-input v-model=\"outbound.sendThrough\"></a-input>\n      </a-form-item>\n\n      <!-- freedom settings-->\n      <template v-if=\"outbound.protocol === Protocols.Freedom\">\n        <a-form-item label='Strategy'>\n          <a-select v-model=\"outbound.settings.domainStrategy\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"s in OutboundDomainStrategies\" :value=\"s\">[[\n              s ]]</a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label='Redirect'>\n          <a-input v-model=\"outbound.settings.redirect\"></a-input>\n        </a-form-item>\n        <a-form-item label='Fragment'>\n          <a-switch :checked=\"Object.keys(outbound.settings.fragment).length >0\"\n            @change=\"checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}\">\n          </a-switch>\n        </a-form-item>\n        <template v-if=\"Object.keys(outbound.settings.fragment).length >0\">\n          <a-form-item label='Packets'>\n            <a-select v-model=\"outbound.settings.fragment.packets\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option v-for=\"s in ['1-3','tlshello']\" :value=\"s\">[[ s\n                ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n          <a-form-item label='Length'>\n            <a-input v-model.trim=\"outbound.settings.fragment.length\"></a-input>\n          </a-form-item>\n          <a-form-item label='Interval'>\n            <a-input\n              v-model.trim=\"outbound.settings.fragment.interval\"></a-input>\n          </a-form-item>\n          <a-form-item label='Max Split'>\n            <a-input\n              v-model.trim=\"outbound.settings.fragment.maxSplit\"></a-input>\n          </a-form-item>\n        </template>\n\n        <!-- Switch for Noises -->\n        <a-form-item label='Noises'>\n          <a-switch :checked=\"outbound.settings.noises.length > 0\"\n            @change=\"checked => outbound.settings.noises = checked ? [new Outbound.FreedomSettings.Noise()] : []\">\n          </a-switch>\n        </a-form-item>\n\n        <!-- Add Noise Button -->\n        <template v-if=\"outbound.settings.noises.length > 0\">\n          <a-form-item label=\"Noises\">\n            <a-button icon=\"plus\" type=\"primary\" size=\"small\"\n              @click=\"outbound.settings.addNoise()\"></a-button>\n          </a-form-item>\n\n          <!-- Noise Configurations -->\n          <a-form v-for=\"(noise, index) in outbound.settings.noises\"\n            :key=\"index\" :colon=\"false\"\n            :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n            <a-divider :style=\"{ margin: '0' }\"> Noise [[ index + 1 ]]\n              <a-icon v-if=\"outbound.settings.noises.length > 1\" type=\"delete\"\n                @click=\"() => outbound.settings.delNoise(index)\"\n                :style=\"{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }\"></a-icon>\n            </a-divider>\n            <a-form-item label='Type'>\n              <a-select v-model=\"noise.type\"\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option v-for=\"s in ['rand','base64','str', 'hex']\"\n                  :value=\"s\">[[ s ]]</a-select-option>\n              </a-select>\n            </a-form-item>\n            <a-form-item label='Packet'>\n              <a-input v-model.trim=\"noise.packet\"></a-input>\n            </a-form-item>\n            <a-form-item label='Delay'>\n              <a-input v-model.trim=\"noise.delay\"></a-input>\n            </a-form-item>\n            <a-form-item label='Apply To'>\n              <a-select v-model=\"noise.applyTo\"\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option v-for=\"s in ['ip','ipv4','ipv6']\" :value=\"s\">[[\n                  s ]]</a-select-option>\n              </a-select>\n            </a-form-item>\n          </a-form>\n        </template>\n      </template>\n\n      <!-- blackhole settings -->\n      <template v-if=\"outbound.protocol === Protocols.Blackhole\">\n        <a-form-item label='Response Type'>\n          <a-select v-model=\"outbound.settings.type\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"s in ['', 'none','http']\" :value=\"s\">[[ s\n              ]]</a-select-option>\n          </a-select>\n        </a-form-item>\n      </template>\n\n      <!-- dns settings -->\n      <template v-if=\"outbound.protocol === Protocols.DNS\">\n        <a-form-item label='{{ i18n \"pages.inbounds.network\" }}'>\n          <a-select v-model=\"outbound.settings.network\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"s in ['udp','tcp']\" :value=\"s\">[[ s\n              ]]</a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label='non-IP queries'>\n          <a-select v-model=\"outbound.settings.nonIPQuery\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"s in ['reject','drop','skip']\" :value=\"s\">[[\n              s ]]</a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item v-if=\"outbound.settings.nonIPQuery === 'skip'\"\n          label='Block Types'>\n          <a-input v-model.number=\"outbound.settings.blockTypes\"></a-input>\n        </a-form-item>\n      </template>\n\n      <!-- wireguard settings -->\n      <template v-if=\"outbound.protocol === Protocols.Wireguard\">\n        <a-form-item>\n          <template slot=\"label\">\n            <a-tooltip>\n              <template slot=\"title\">\n                <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n              </template>\n              {{ i18n \"pages.xray.outbound.address\" }}\n              <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n          </template>\n          <a-input v-model.trim=\"outbound.settings.address\"></a-input>\n        </a-form-item>\n        <a-form-item>\n          <template slot=\"label\">\n            <a-tooltip>\n              <template slot=\"title\">\n                <span>{{ i18n \"reset\" }}</span>\n              </template>\n              {{ i18n \"pages.xray.wireguard.secretKey\" }}\n              <a-icon type=\"sync\"\n                @click=\"[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())\">\n              </a-icon>\n            </a-tooltip>\n          </template>\n          <a-input v-model.trim=\"outbound.settings.secretKey\"></a-input>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.xray.wireguard.publicKey\" }}'>\n          <a-input disabled v-model=\"outbound.settings.pubKey\"></a-input>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.xray.wireguard.domainStrategy\" }}'>\n          <a-select v-model=\"outbound.settings.domainStrategy\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"wds in ['', ...WireguardDomainStrategy]\"\n              :value=\"wds\">[[ wds ]]</a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label='MTU'>\n          <a-input-number v-model.number=\"outbound.settings.mtu\"\n            min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label='Workers'>\n          <a-input-number v-model.number=\"outbound.settings.workers\"\n            min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label='No Kernel Tun'>\n          <a-switch v-model=\"outbound.settings.noKernelTun\"></a-switch>\n        </a-form-item>\n        <a-form-item>\n          <template slot=\"label\">\n            <a-tooltip>\n              <template slot=\"title\">\n                <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n              </template> Reserved <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n          </template>\n          <a-input v-model=\"outbound.settings.reserved\"></a-input>\n        </a-form-item>\n        <a-form-item label=\"Peers\">\n          <a-button icon=\"plus\" type=\"primary\" size=\"small\"\n            @click=\"outbound.settings.addPeer()\"></a-button>\n        </a-form-item>\n        <a-form v-for=\"(peer, index) in outbound.settings.peers\" :colon=\"false\"\n          :label-col=\"{ md: {span:8} }\"\n          :wrapper-col=\"{ md: {span:14} }\">\n          <a-divider :style=\"{ margin: '0' }\"> Peer [[ index + 1 ]] <a-icon\n              v-if=\"outbound.settings.peers.length>1\"\n              type=\"delete\" @click=\"() => outbound.settings.delPeer(index)\"\n              :style=\"{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }\"></a-icon>\n          </a-divider>\n          <a-form-item label='{{ i18n \"pages.xray.wireguard.endpoint\" }}'>\n            <a-input v-model.trim=\"peer.endpoint\"></a-input>\n          </a-form-item>\n          <a-form-item label='{{ i18n \"pages.xray.wireguard.publicKey\" }}'>\n            <a-input v-model.trim=\"peer.publicKey\"></a-input>\n          </a-form-item>\n          <a-form-item label='{{ i18n \"pages.xray.wireguard.psk\" }}'>\n            <a-input v-model.trim=\"peer.psk\"></a-input>\n          </a-form-item>\n          <a-form-item>\n            <template slot=\"label\">\n              {{ i18n \"pages.xray.wireguard.allowedIPs\" }}\n              <a-button icon=\"plus\" type=\"primary\" size=\"small\"\n                @click=\"peer.allowedIPs.push('')\"></a-button>\n            </template>\n            <template v-for=\"(aip, index) in peer.allowedIPs\"\n              :style=\"{ marginBottom: '10px' }\">\n              <a-input v-model.trim=\"peer.allowedIPs[index]\">\n                <a-button icon=\"minus\" v-if=\"peer.allowedIPs.length>1\"\n                  slot=\"addonAfter\" size=\"small\"\n                  @click=\"peer.allowedIPs.splice(index, 1)\"></a-button>\n              </a-input>\n            </template>\n          </a-form-item>\n          <a-form-item label='Keep Alive'>\n            <a-input-number v-model.number=\"peer.keepAlive\"\n              :min=\"0\"></a-input-number>\n          </a-form-item>\n        </a-form>\n      </template>\n\n      <!-- Address + Port -->\n      <template v-if=\"outbound.hasAddressPort()\">\n        <a-form-item label='{{ i18n \"pages.inbounds.address\" }}'>\n          <a-input v-model.trim=\"outbound.settings.address\"></a-input>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.inbounds.port\" }}'>\n          <a-input-number v-model.number=\"outbound.settings.port\" :min=\"1\"\n            :max=\"65532\"></a-input-number>\n        </a-form-item>\n      </template>\n\n      <!-- VLESS/VMess user settings -->\n      <template\n        v-if=\"[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)\">\n        <a-form-item label='ID'>\n          <a-input v-model.trim=\"outbound.settings.id\"></a-input>\n        </a-form-item>\n\n        <!-- vmess settings -->\n        <template v-if=\"outbound.protocol === Protocols.VMess\">\n          <a-form-item label='Security'>\n            <a-select v-model=\"outbound.settings.security\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option v-for=\"key in USERS_SECURITY\" :value=\"key\">[[ key\n                ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n        </template>\n\n        <!-- vless settings -->\n        <template v-if=\"outbound.protocol === Protocols.VLESS\">\n          <a-form-item label='encryption'>\n            <a-input v-model.trim=\"outbound.settings.encryption\"></a-input>\n          </a-form-item>\n        </template>\n        <template v-if=\"outbound.canEnableTlsFlow()\">\n          <a-form-item label='Flow'>\n            <a-select v-model=\"outbound.settings.flow\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option value selected>{{ i18n \"none\"\n                }}</a-select-option>\n              <a-select-option v-for=\"key in TLS_FLOW_CONTROL\" :value=\"key\">[[\n                key ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n        </template>\n        <!-- XTLS Vision Advanced Settings -->\n        <template v-if=\"outbound.canEnableVisionSeed()\">\n          <a-form-item label=\"Vision Pre-Connect\">\n            <a-input-number v-model.number=\"outbound.settings.testpre\" :min=\"0\"\n              :max=\"10\" :style=\"{ width: '100%' }\"\n              placeholder=\"0\"></a-input-number>\n          </a-form-item>\n          <a-form-item label=\"Vision Seed\">\n            <a-row :gutter=\"8\">\n              <a-col :span=\"6\">\n                <a-input-number v-model.number=\"outbound.settings.testseed[0]\"\n                  :min=\"0\" :max=\"9999\"\n                  :style=\"{ width: '100%' }\" placeholder=\"900\"\n                  addon-before=\"[0]\"></a-input-number>\n              </a-col>\n              <a-col :span=\"6\">\n                <a-input-number v-model.number=\"outbound.settings.testseed[1]\"\n                  :min=\"0\" :max=\"9999\"\n                  :style=\"{ width: '100%' }\" placeholder=\"500\"\n                  addon-before=\"[1]\"></a-input-number>\n              </a-col>\n              <a-col :span=\"6\">\n                <a-input-number v-model.number=\"outbound.settings.testseed[2]\"\n                  :min=\"0\" :max=\"9999\"\n                  :style=\"{ width: '100%' }\" placeholder=\"900\"\n                  addon-before=\"[2]\"></a-input-number>\n              </a-col>\n              <a-col :span=\"6\">\n                <a-input-number v-model.number=\"outbound.settings.testseed[3]\"\n                  :min=\"0\" :max=\"9999\"\n                  :style=\"{ width: '100%' }\" placeholder=\"256\"\n                  addon-before=\"[3]\"></a-input-number>\n              </a-col>\n            </a-row>\n          </a-form-item>\n        </template>\n      </template>\n\n      <!-- Servers (trojan/shadowsocks/socks/http) settings -->\n      <template v-if=\"outbound.hasServers()\">\n        <!-- http / socks -->\n        <template v-if=\"outbound.hasUsername()\">\n          <a-form-item label='{{ i18n \"username\" }}'>\n            <a-input v-model.trim=\"outbound.settings.user\"></a-input>\n          </a-form-item>\n          <a-form-item label='{{ i18n \"password\" }}'>\n            <a-input v-model.trim=\"outbound.settings.pass\"></a-input>\n          </a-form-item>\n        </template>\n\n        <!-- trojan/shadowsocks -->\n        <template\n          v-if=\"[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)\">\n          <a-form-item label='{{ i18n \"password\" }}'>\n            <a-input v-model.trim=\"outbound.settings.password\"></a-input>\n          </a-form-item>\n        </template>\n\n        <!-- shadowsocks -->\n        <template v-if=\"outbound.protocol === Protocols.Shadowsocks\">\n          <a-form-item label='{{ i18n \"encryption\" }}'>\n            <a-select v-model=\"outbound.settings.method\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option v-for=\"(method, method_name) in SSMethods\"\n                :value=\"method\">[[ method_name\n                ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n          <a-form-item label='UDP over TCP'>\n            <a-switch v-model=\"outbound.settings.uot\"></a-switch>\n          </a-form-item>\n          <a-form-item label='UoTVersion'>\n            <a-input-number v-model.number=\"outbound.settings.UoTVersion\"\n              :min=\"1\" :max=\"2\"></a-input-number>\n          </a-form-item>\n        </template>\n      </template>\n\n      <!-- hysteria settings -->\n      <template v-if=\"outbound.protocol === Protocols.Hysteria\">\n        <a-form-item label='Version'>\n          <a-input-number v-model.number=\"outbound.settings.version\" :min=\"2\"\n            :max=\"2\" disabled></a-input-number>\n        </a-form-item>\n      </template>\n\n      <!-- stream settings -->\n      <template v-if=\"outbound.canEnableStream()\">\n        <a-form-item label='{{ i18n \"transmission\" }}'>\n          <a-select v-model=\"outbound.stream.network\"\n            @change=\"streamNetworkChange\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"tcp\">TCP (RAW)</a-select-option>\n            <a-select-option value=\"kcp\">mKCP</a-select-option>\n            <a-select-option value=\"ws\">WebSocket</a-select-option>\n            <a-select-option value=\"grpc\">gRPC</a-select-option>\n            <a-select-option value=\"httpupgrade\">HTTPUpgrade</a-select-option>\n            <a-select-option value=\"xhttp\">XHTTP</a-select-option>\n            <a-select-option v-if=\"outbound.protocol === Protocols.Hysteria\"\n              value=\"hysteria\">Hysteria2</a-select-option>\n          </a-select>\n        </a-form-item>\n        <template v-if=\"outbound.stream.network === 'tcp'\">\n          <a-form-item label='HTTP {{ i18n \"camouflage\" }}'>\n            <a-switch :checked=\"outbound.stream.tcp.type === 'http'\"\n              @change=\"checked => outbound.stream.tcp.type = checked ? 'http' : 'none'\"></a-switch>\n          </a-form-item>\n          <template v-if=\"outbound.stream.tcp.type == 'http'\">\n            <a-form-item label='{{ i18n \"host\" }}'>\n              <a-input v-model.trim=\"outbound.stream.tcp.host\"></a-input>\n            </a-form-item>\n            <a-form-item label='{{ i18n \"path\" }}'>\n              <a-input v-model.trim=\"outbound.stream.tcp.path\"></a-input>\n            </a-form-item>\n          </template>\n        </template>\n\n        <!-- kcp -->\n        <template v-if=\"outbound.stream.network === 'kcp'\">\n          <a-form-item label='MTU'>\n            <a-input-number v-model.number=\"outbound.stream.kcp.mtu\"\n              min=\"0\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='TTI (ms)'>\n            <a-input-number v-model.number=\"outbound.stream.kcp.tti\"\n              min=\"0\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Uplink (MB/s)'>\n            <a-input-number v-model.number=\"outbound.stream.kcp.upCap\"\n              min=\"0\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Downlink (MB/s)'>\n            <a-input-number v-model.number=\"outbound.stream.kcp.downCap\"\n              min=\"0\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Congestion'>\n            <a-switch v-model=\"outbound.stream.kcp.congestion\"></a-switch>\n          </a-form-item>\n          <a-form-item label='Read Buffer (MB)'>\n            <a-input-number v-model.number=\"outbound.stream.kcp.readBuffer\"\n              min=\"0\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Write Buffer (MB)'>\n            <a-input-number v-model.number=\"outbound.stream.kcp.writeBuffer\"\n              min=\"0\"></a-input-number>\n          </a-form-item>\n        </template>\n\n        <!-- ws -->\n        <template v-if=\"outbound.stream.network === 'ws'\">\n          <a-form-item label='{{ i18n \"host\" }}'>\n            <a-input v-model=\"outbound.stream.ws.host\"></a-input>\n          </a-form-item>\n          <a-form-item label='{{ i18n \"path\" }}'>\n            <a-input v-model.trim=\"outbound.stream.ws.path\"></a-input>\n          </a-form-item>\n          <a-form-item label='Heartbeat Period'>\n            <a-input-number v-model.number=\"outbound.stream.ws.heartbeatPeriod\"\n              :min=\"0\"></a-input-number>\n          </a-form-item>\n        </template>\n\n        <!-- grpc -->\n        <template v-if=\"outbound.stream.network === 'grpc'\">\n          <a-form-item label='Service Name'>\n            <a-input v-model.trim=\"outbound.stream.grpc.serviceName\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"Authority\">\n            <a-input v-model.trim=\"outbound.stream.grpc.authority\"></a-input>\n          </a-form-item>\n          <a-form-item label='Multi Mode'>\n            <a-switch v-model=\"outbound.stream.grpc.multiMode\"></a-switch>\n          </a-form-item>\n        </template>\n\n        <!-- httpupgrade -->\n        <template v-if=\"outbound.stream.network === 'httpupgrade'\">\n          <a-form-item label='{{ i18n \"host\" }}'>\n            <a-input v-model=\"outbound.stream.httpupgrade.host\"></a-input>\n          </a-form-item>\n          <a-form-item label='{{ i18n \"path\" }}'>\n            <a-input v-model.trim=\"outbound.stream.httpupgrade.path\"></a-input>\n          </a-form-item>\n        </template>\n\n        <!-- xhttp -->\n        <template v-if=\"outbound.stream.network === 'xhttp'\">\n          <a-form-item label='{{ i18n \"host\" }}'>\n            <a-input v-model=\"outbound.stream.xhttp.host\"></a-input>\n          </a-form-item>\n          <a-form-item label='{{ i18n \"path\" }}'>\n            <a-input v-model.trim=\"outbound.stream.xhttp.path\"></a-input>\n          </a-form-item>\n          <a-form-item label='Mode'>\n            <a-select v-model=\"outbound.stream.xhttp.mode\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option v-for=\"key in MODE_OPTION\" :value=\"key\">[[ key\n                ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n          <a-form-item label=\"No gRPC Header\"\n            v-if=\"outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'\">\n            <a-switch v-model=\"outbound.stream.xhttp.noGRPCHeader\"></a-switch>\n          </a-form-item>\n          <a-form-item label=\"Min Upload Interval (Ms)\"\n            v-if=\"outbound.stream.xhttp.mode === 'packet-up'\">\n            <a-input\n              v-model.trim=\"outbound.stream.xhttp.scMinPostsIntervalMs\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"Max Concurrency\"\n            v-if=\"!outbound.stream.xhttp.xmux.maxConnections\">\n            <a-input\n              v-model=\"outbound.stream.xhttp.xmux.maxConcurrency\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"Max Connections\"\n            v-if=\"!outbound.stream.xhttp.xmux.maxConcurrency\">\n            <a-input\n              v-model=\"outbound.stream.xhttp.xmux.maxConnections\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"Max Reuse Times\">\n            <a-input\n              v-model=\"outbound.stream.xhttp.xmux.cMaxReuseTimes\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"Max Request Times\">\n            <a-input\n              v-model=\"outbound.stream.xhttp.xmux.hMaxRequestTimes\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"Max Reusable Secs\">\n            <a-input\n              v-model=\"outbound.stream.xhttp.xmux.hMaxReusableSecs\"></a-input>\n          </a-form-item>\n          <a-form-item label='Keep Alive Period'>\n            <a-input-number\n              v-model.number=\"outbound.stream.xhttp.xmux.hKeepAlivePeriod\"></a-input-number>\n          </a-form-item>\n        </template>\n\n        <!-- hysteria -->\n        <template v-if=\"outbound.stream.network === 'hysteria'\">\n          <a-form-item label='Auth Password'>\n            <a-input v-model.trim=\"outbound.stream.hysteria.auth\"></a-input>\n          </a-form-item>\n          <a-form-item label='Congestion'>\n            <a-select v-model=\"outbound.stream.hysteria.congestion\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option value>BBR (Auto)</a-select-option>\n              <a-select-option value=\"brutal\">Brutal</a-select-option>\n            </a-select>\n          </a-form-item>\n          <a-form-item label='Upload Speed'>\n            <a-input v-model.trim=\"outbound.stream.hysteria.up\"\n              placeholder=\"0 (BBR mode), e.g., 100 mbps\"></a-input>\n          </a-form-item>\n          <a-form-item label='Download Speed'>\n            <a-input v-model.trim=\"outbound.stream.hysteria.down\"\n              placeholder=\"0 (BBR mode), e.g., 100 mbps\"></a-input>\n          </a-form-item>\n          <a-form-item label='UDP Hop Port'>\n            <a-input v-model.trim=\"outbound.stream.hysteria.udphopPort\"\n              placeholder=\"e.g., 1145-1919 or 11,13,15-17\"></a-input>\n          </a-form-item>\n          <a-form-item label='UDP Hop Interval Min (s)'\n            v-if=\"outbound.stream.hysteria.udphopPort\">\n            <a-input-number\n              v-model.number=\"outbound.stream.hysteria.udphopIntervalMin\"\n              :min=\"5\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='UDP Hop Interval Max (s)'\n            v-if=\"outbound.stream.hysteria.udphopPort\">\n            <a-input-number\n              v-model.number=\"outbound.stream.hysteria.udphopIntervalMax\"\n              :min=\"5\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Init Stream Receive'>\n            <a-input-number\n              v-model.number=\"outbound.stream.hysteria.initStreamReceiveWindow\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Max Stream Receive'>\n            <a-input-number\n              v-model.number=\"outbound.stream.hysteria.maxStreamReceiveWindow\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Init Connection Receive'>\n            <a-input-number\n              v-model.number=\"outbound.stream.hysteria.initConnectionReceiveWindow\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Max Connection Receive'>\n            <a-input-number\n              v-model.number=\"outbound.stream.hysteria.maxConnectionReceiveWindow\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Max Idle Timeout (s)'>\n            <a-input-number\n              v-model.number=\"outbound.stream.hysteria.maxIdleTimeout\" :min=\"4\"\n              :max=\"120\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Keep Alive Period (s)'>\n            <a-input-number\n              v-model.number=\"outbound.stream.hysteria.keepAlivePeriod\" :min=\"0\"\n              :max=\"60\"></a-input-number>\n          </a-form-item>\n          <a-form-item label='Disable Path MTU'>\n            <a-switch\n              v-model=\"outbound.stream.hysteria.disablePathMTUDiscovery\"></a-switch>\n          </a-form-item>\n        </template>\n      </template>\n\n      <!-- finalmask settings -->\n      <template v-if=\"outbound.canEnableStream()\">\n        <a-form-item label=\"UDP Masks\">\n          <a-button icon=\"plus\" type=\"primary\" size=\"small\"\n            @click=\"outbound.stream.addUdpMask(outbound.protocol === Protocols.Hysteria ? 'salamander' : (outbound.stream.network === 'kcp' ? 'mkcp-aes128gcm' : 'xdns'))\"></a-button>\n        </a-form-item>\n        <template\n          v-if=\"outbound.stream.finalmask.udp && outbound.stream.finalmask.udp.length > 0\">\n          <a-form v-for=\"(mask, index) in outbound.stream.finalmask.udp\"\n            :key=\"index\" :colon=\"false\"\n            :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n            <a-divider :style=\"{ margin: '0' }\"> UDP Mask [[ index + 1 ]]\n              <a-icon type=\"delete\"\n                @click=\"() => outbound.stream.delUdpMask(index)\"\n                :style=\"{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }\"></a-icon>\n            </a-divider>\n            <a-form-item label='Type'>\n              <a-select v-model=\"mask.type\"\n                @change=\"(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(outbound.stream.network === 'kcp') { outbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }\"\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <!-- Salamander for Hysteria2 only -->\n                <a-select-option v-if=\"outbound.protocol === Protocols.Hysteria\"\n                  value=\"salamander\">\n                  Salamander (Hysteria2)</a-select-option>\n                <!-- mKCP-specific masks -->\n                <a-select-option v-if=\"outbound.stream.network === 'kcp'\"\n                  value=\"mkcp-aes128gcm\">\n                  mKCP AES-128-GCM</a-select-option>\n                <a-select-option v-if=\"outbound.stream.network === 'kcp'\"\n                  value=\"header-dns\">\n                  Header DNS</a-select-option>\n                <a-select-option v-if=\"outbound.stream.network === 'kcp'\"\n                  value=\"header-dtls\">\n                  Header DTLS 1.2</a-select-option>\n                <a-select-option v-if=\"outbound.stream.network === 'kcp'\"\n                  value=\"header-srtp\">\n                  Header SRTP</a-select-option>\n                <a-select-option v-if=\"outbound.stream.network === 'kcp'\"\n                  value=\"header-utp\">\n                  Header uTP</a-select-option>\n                <a-select-option v-if=\"outbound.stream.network === 'kcp'\"\n                  value=\"header-wechat\">\n                  Header WeChat Video</a-select-option>\n                <a-select-option v-if=\"outbound.stream.network === 'kcp'\"\n                  value=\"header-wireguard\">\n                  Header WireGuard</a-select-option>\n                <a-select-option v-if=\"outbound.stream.network === 'kcp'\"\n                  value=\"mkcp-original\">\n                  mKCP Original</a-select-option>\n                <!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->\n                <a-select-option\n                  v-if=\"['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(outbound.stream.network)\"\n                  value=\"xdns\">\n                  xDNS (Experimental)</a-select-option>\n              </a-select>\n            </a-form-item>\n            <!-- Settings for password-based masks -->\n            <a-form-item label='Password'\n              v-if=\"['salamander', 'mkcp-aes128gcm'].includes(mask.type)\">\n              <a-input v-model.trim=\"mask.settings.password\"\n                placeholder=\"Obfuscation password\"></a-input>\n            </a-form-item>\n            <!-- Settings for domain-based masks -->\n            <a-form-item label='Domain'\n              v-if=\"['header-dns', 'xdns'].includes(mask.type)\">\n              <a-input v-model.trim=\"mask.settings.domain\"\n                placeholder=\"e.g., www.example.com\"></a-input>\n            </a-form-item>\n          </a-form>\n        </template>\n      </template>\n\n      <!-- tls settings -->\n      <template v-if=\"outbound.canEnableTls()\">\n        <a-form-item label='{{ i18n \"security\" }}'>\n          <a-radio-group v-model=\"outbound.stream.security\"\n            button-style=\"solid\">\n            <a-radio-button value=\"none\">{{ i18n \"none\" }}</a-radio-button>\n            <a-radio-button value=\"tls\">TLS</a-radio-button>\n            <a-radio-button v-if=\"outbound.canEnableReality()\"\n              value=\"reality\">Reality</a-radio-button>\n          </a-radio-group>\n        </a-form-item>\n        <template v-if=\"outbound.stream.isTls\">\n          <a-form-item label=\"SNI\" placeholder=\"Server Name Indication\">\n            <a-input v-model.trim=\"outbound.stream.tls.serverName\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"uTLS\">\n            <a-select v-model=\"outbound.stream.tls.fingerprint\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option value>None</a-select-option>\n              <a-select-option v-for=\"key in UTLS_FINGERPRINT\" :value=\"key\">[[\n                key ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n          <a-form-item label=\"ALPN\">\n            <a-select mode=\"multiple\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\"\n              v-model=\"outbound.stream.tls.alpn\">\n              <a-select-option v-for=\"alpn in ALPN_OPTION\" :value=\"alpn\">[[ alpn\n                ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n          <a-form-item label=\"ECH Config List\">\n            <a-input v-model.trim=\"outbound.stream.tls.echConfigList\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"verify Peer Cert By Name\">\n            <a-input\n              v-model.trim=\"outbound.stream.tls.verifyPeerCertByName\"\n              placeholder=\"cloudflare-dns.com\"></a-input>\n          </a-form-item>\n          <a-form-item label=\" pinned Peer Cert Sha256\">\n            <a-input v-model.trim=\"outbound.stream.tls.pinnedPeerCertSha256\"\n              placeholder=\"Enter SHA256 fingerprints (base64)\">\n            </a-input>\n          </a-form-item>\n        </template>\n\n        <!-- reality settings -->\n        <template v-if=\"outbound.stream.isReality\">\n          <a-form-item label=\"SNI\">\n            <a-input\n              v-model.trim=\"outbound.stream.reality.serverName\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"uTLS\">\n            <a-select v-model=\"outbound.stream.reality.fingerprint\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option v-for=\"key in UTLS_FINGERPRINT\" :value=\"key\">[[\n                key ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n          <a-form-item label=\"Short ID\">\n            <a-input v-model.trim=\"outbound.stream.reality.shortId\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"SpiderX\">\n            <a-input v-model.trim=\"outbound.stream.reality.spiderX\"></a-input>\n          </a-form-item>\n          <a-form-item label=\"Public Key\">\n            <a-textarea\n              v-model.trim=\"outbound.stream.reality.publicKey\"></a-textarea>\n          </a-form-item>\n          <a-form-item label=\"mldsa65 Verify\">\n            <a-textarea\n              v-model.trim=\"outbound.stream.reality.mldsa65Verify\"></a-textarea>\n          </a-form-item>\n        </template>\n      </template>\n\n      <!-- sockopt settings -->\n      <a-form-item label=\"Sockopts\">\n        <a-switch v-model=\"outbound.stream.sockoptSwitch\"></a-switch>\n      </a-form-item>\n      <template v-if=\"outbound.stream.sockoptSwitch\">\n        <a-form-item label=\"Dialer Proxy\">\n          <a-select v-model=\"outbound.stream.sockopt.dialerProxy\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"tag in ['', ...outModal.tags]\"\n              :value=\"tag\">[[ tag ]]</a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label='Address Port Strategy'>\n          <a-select v-model=\"outbound.stream.sockopt.addressPortStrategy\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"key in Address_Port_Strategy\"\n              :value=\"key\">[[ key ]]</a-select-option>\n          </a-select>\n        </a-form-item>\n        <a-form-item label=\"Keep Alive Interval\">\n          <a-input-number\n            v-model.number=\"outbound.stream.sockopt.tcpKeepAliveInterval\"\n            :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label=\"TCP Fast Open\">\n          <a-switch v-model=\"outbound.stream.sockopt.tcpFastOpen\"></a-switch>\n        </a-form-item>\n        <a-form-item label=\"Multipath TCP\">\n          <a-switch\n            v-model.trim=\"outbound.stream.sockopt.tcpMptcp\"></a-switch>\n        </a-form-item>\n        <a-form-item label=\"Penetrate\">\n          <a-switch v-model=\"outbound.stream.sockopt.penetrate\"></a-switch>\n        </a-form-item>\n        <a-form-item label=\"Trusted X-Forwarded-For\">\n          <a-select mode=\"tags\"\n            v-model=\"outbound.stream.sockopt.trustedXForwardedFor\"\n            :style=\"{ width: '100%' }\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option\n              value=\"CF-Connecting-IP\">CF-Connecting-IP</a-select-option>\n            <a-select-option value=\"X-Real-IP\">X-Real-IP</a-select-option>\n            <a-select-option\n              value=\"True-Client-IP\">True-Client-IP</a-select-option>\n            <a-select-option value=\"X-Client-IP\">X-Client-IP</a-select-option>\n          </a-select>\n        </a-form-item>\n      </template>\n\n      <!-- mux settings -->\n      <template v-if=\"outbound.canEnableMux()\">\n        <a-form-item label=\"Mux\">\n          <a-switch v-model=\"outbound.mux.enabled\"></a-switch>\n        </a-form-item>\n        <template v-if=\"outbound.mux.enabled\">\n          <a-form-item label=\"Concurrency\">\n            <a-input-number v-model.number=\"outbound.mux.concurrency\"\n              :min=\"-1\"\n              :max=\"1024\"></a-input-number>\n          </a-form-item>\n          <a-form-item label=\"xudp Concurrency\">\n            <a-input-number v-model.number=\"outbound.mux.xudpConcurrency\"\n              :min=\"-1\" :max=\"1024\"></a-input-number>\n          </a-form-item>\n          <a-form-item label=\"xudp UDP 443\">\n            <a-select v-model=\"outbound.mux.xudpProxyUDP443\"\n              :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option v-for=\"c in ['reject', 'allow', 'skip']\"\n                :value=\"c\">[[ c ]]</a-select-option>\n            </a-select>\n          </a-form-item>\n        </template>\n      </template>\n    </a-form>\n  </a-tab-pane>\n  <a-tab-pane key=\"2\" tab=\"JSON\" force-render=\"true\">\n    <a-space direction=\"vertical\" :size=\"10\" :style=\"{ marginTop: '10px' }\">\n      <a-input addon-before='{{ i18n \"pages.xray.outbound.link\" }}'\n        v-model.trim=\"outModal.link\"\n        placeholder=\"vmess:// vless:// trojan:// ss:// hysteria2://\">\n        <a-icon slot=\"addonAfter\" type=\"form\" @click=\"convertLink\"></a-icon>\n      </a-input>\n      <textarea :style=\"{ position: 'absolute', left: '-800px' }\"\n        id=\"outboundJson\"></textarea>\n    </a-space>\n  </a-tab-pane>\n</a-tabs>\n{{end}}"
  },
  {
    "path": "web/html/form/protocol/dokodemo.html",
    "content": "{{define \"form/tunnel\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='{{ i18n \"pages.inbounds.targetAddress\"}}'>\n        <a-input v-model.trim=\"inbound.settings.address\"></a-input>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.inbounds.destinationPort\"}}'>\n        <a-input-number v-model.number=\"inbound.settings.port\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.inbounds.portMap\"}}'>\n        <a-button size=\"small\" @click=\"inbound.settings.portMap.push({name: '', value: ''})\">+</a-button>\n    </a-form-item>\n    <a-form-item :wrapper-col=\"{span:24}\">\n        <a-input-group compact v-for=\"(pm, index) in inbound.settings.portMap\">\n            <a-input style=\"width: 50%\" v-model.trim=\"pm.name\" placeholder='{{ i18n \"pages.inbounds.port\"}}'>\n                <template slot=\"addonBefore\" style=\"margin: 0;\">[[ index+1 ]]</template>\n            </a-input>\n            <a-input style=\"width: 50%\" v-model.trim=\"pm.value\" placeholder='{{ i18n \"pages.inbounds.targetAddress\" }}'>\n                <a-button slot=\"addonAfter\" size=\"small\" @click=\"inbound.settings.portMap.splice(index,1)\">-</a-button>\n            </a-input>\n        </a-input-group>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.inbounds.network\"}}'>\n        <a-select v-model=\"inbound.settings.network\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"tcp,udp\">TCP,UDP</a-select-option>\n            <a-select-option value=\"tcp\">TCP</a-select-option>\n            <a-select-option value=\"udp\">UDP</a-select-option>\n        </a-select>\n    </a-form-item>           \n    <a-form-item label='Follow Redirect'>\n        <a-switch v-model=\"inbound.settings.followRedirect\"></a-switch>\n    </a-form-item>\n</a-form>\n<!-- sockopt -->\n<template>\n    {{template \"form/streamSockopt\"}}\n</template>\n{{end}}\n"
  },
  {
    "path": "web/html/form/protocol/http.html",
    "content": "{{define \"form/http\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <table :style=\"{ width: '100%', textAlign: 'center', margin: '1rem 0' }\">\n    <tr>\n      <td width=\"45%\">{{ i18n \"username\" }}</td>\n      <td width=\"45%\">{{ i18n \"password\" }}</td>\n      <td>\n        <a-button icon=\"plus\" size=\"small\" @click=\"inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())\"></a-button>\n      </td>\n    </tr>\n  </table>\n  <a-input-group compact v-for=\"(account, index) in inbound.settings.accounts\" :style=\"{ marginBottom: '10px' }\">\n    <a-input :style=\"{ width: '50%' }\" v-model.trim=\"account.user\" placeholder='{{ i18n \"username\" }}'>\n      <template slot=\"addonBefore\" :style=\"{ margin: '0' }\">[[ index+1 ]]</template>\n    </a-input>\n    <a-input :style=\"{ width: '50%' }\" v-model.trim=\"account.pass\" placeholder='{{ i18n \"password\" }}'>\n      <template slot=\"addonAfter\">\n        <a-button icon=\"minus\" size=\"small\" @click=\"inbound.settings.delAccount(index)\"></a-button>\n      </template>\n    </a-input>\n  </a-input-group>\n  <a-form-item label=\"Allow Transparent\">\n    <a-switch v-model=\"inbound.settings.allowTransparent\" />\n  </a-form-item>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/protocol/shadowsocks.html",
    "content": "{{define \"form/shadowsocks\"}}\n<template v-if=\"inbound.isSSMultiUser\">\n    <a-collapse activeKey=\"0\" v-for=\"(client, index) in inbound.settings.shadowsockses.slice(0,1)\" v-if=\"!isEdit\">  \n        <a-collapse-panel header='{{ i18n \"pages.inbounds.client\" }}'>\n            {{template \"form/client\"}}\n        </a-collapse-panel>\n    </a-collapse>\n    <a-collapse v-else>\n        <a-collapse-panel :header=\"'{{ i18n \"pages.client.clientCount\"}} : ' + inbound.settings.shadowsockses.length\">\n            <table width=\"100%\">\n                <tr class=\"client-table-header\">\n                    <th>{{ i18n \"pages.inbounds.email\" }}</th>\n                    <th>Password</th>\n                </tr>\n                <tr v-for=\"(client, index) in inbound.settings.shadowsockses\" :class=\"index % 2 == 1 ? 'client-table-odd-row' : ''\">\n                    <td>[[ client.email ]]</td>\n                    <td>[[ client.password ]]</td>\n                </tr>\n            </table>\n        </a-collapse-panel>\n    </a-collapse>\n</template>\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='{{ i18n \"encryption\" }}'>\n        <a-select v-model=\"inbound.settings.method\" @change=\"SSMethodChange\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"(method,method_name) in SSMethods\" :value=\"method\">[[ method_name ]]</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item v-if=\"inbound.isSS2022\">\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"reset\" }}</span>\n                </template> Password <a-icon @click=\"inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)\" type=\"sync\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"inbound.settings.password\"></a-input>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.inbounds.network\" }}'>\n        <a-select v-model=\"inbound.settings.network\" :style=\"{ width: '100px' }\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"tcp,udp\">TCP,UDP</a-select-option>\n            <a-select-option value=\"tcp\">TCP</a-select-option>\n            <a-select-option value=\"udp\">UDP</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item label='ivCheck'>\n        <a-switch v-model=\"inbound.settings.ivCheck\"></a-switch>\n    </a-form-item>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/protocol/socks.html",
    "content": "{{define \"form/mixed\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <a-form-item label='{{ i18n \"pages.inbounds.enable\" }} UDP'>\n    <a-switch v-model=\"inbound.settings.udp\"></a-switch>\n  </a-form-item>\n  <a-form-item label=\"IP\" v-if=\"inbound.settings.udp\">\n    <a-input v-model.trim=\"inbound.settings.ip\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"password\" }}'>\n    <a-switch :checked=\"inbound.settings.auth === 'password'\" @change=\"checked => inbound.settings.auth = checked ? 'password' : 'noauth'\"></a-switch>\n  </a-form-item>\n  <template v-if=\"inbound.settings.auth === 'password'\">\n    <table :style=\"{ width: '100%', textAlign: 'center', margin: '1rem 0' }\">\n      <tr>\n        <td width=\"45%\">{{ i18n \"username\" }}</td>\n        <td width=\"45%\">{{ i18n \"password\" }}</td>\n        <td>\n          <a-button icon=\"plus\" size=\"small\" @click=\"inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())\"></a-button>\n        </td>\n      </tr>\n    </table>\n    <a-input-group compact v-for=\"(account, index) in inbound.settings.accounts\" :style=\"{ marginBottom: '10px' }\">\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"account.user\" placeholder='{{ i18n \"username\" }}'>\n        <template slot=\"addonBefore\" :style=\"{ margin: '0' }\">[[ index+1 ]]</template>\n      </a-input>\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"account.pass\" placeholder='{{ i18n \"password\" }}'>\n        <template slot=\"addonAfter\">\n          <a-button icon=\"minus\" size=\"small\" @click=\"inbound.settings.delAccount(index)\"></a-button>\n        </template>\n      </a-input>\n    </a-input-group>\n  </template>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/protocol/trojan.html",
    "content": "{{define \"form/trojan\"}}\n<a-collapse activeKey=\"0\" v-for=\"(client, index) in inbound.settings.trojans.slice(0,1)\" v-if=\"!isEdit\">\n  <a-collapse-panel header='{{ i18n \"pages.inbounds.client\" }}'>\n    {{template \"form/client\"}}\n  </a-collapse-panel>\n</a-collapse>\n<a-collapse v-else>\n  <a-collapse-panel :header=\"'{{ i18n \"pages.client.clientCount\"}} : ' + inbound.settings.trojans.length\">\n    <table width=\"100%\">\n      <tr class=\"client-table-header\">\n        <th>{{ i18n \"pages.inbounds.email\" }}</th>\n        <th>Password</th>\n      </tr>\n      <tr v-for=\"(client, index) in inbound.settings.trojans\" :class=\"index % 2 == 1 ? 'client-table-odd-row' : ''\">\n        <td>[[ client.email ]]</td>\n        <td>[[ client.password ]]</td>\n      </tr>\n    </table>\n  </a-collapse-panel>\n</a-collapse>\n<template v-if=\"inbound.isTcp\">\n  <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label=\"Fallbacks\">\n      <a-button icon=\"plus\" type=\"primary\" size=\"small\" @click=\"inbound.settings.addFallback()\"></a-button>\n    </a-form-item>\n  </a-form>\n\n  <!-- trojan fallbacks -->\n  <a-form v-for=\"(fallback, index) in inbound.settings.fallbacks\" :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-divider :style=\"{ margin: '0' }\"> Fallback [[ index + 1 ]] <a-icon type=\"delete\" @click=\"() => inbound.settings.delFallback(index)\" :style=\"{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }\"></a-icon>\n    </a-divider>\n    <a-form-item label='SNI'>\n      <a-input v-model=\"fallback.name\"></a-input>\n    </a-form-item>\n    <a-form-item label='ALPN'>\n      <a-input v-model=\"fallback.alpn\"></a-input>\n    </a-form-item>\n    <a-form-item label='Path'>\n      <a-input v-model=\"fallback.path\"></a-input>\n    </a-form-item>\n    <a-form-item label='Dest'>\n      <a-input v-model=\"fallback.dest\"></a-input>\n    </a-form-item>\n    <a-form-item label='xVer'>\n      <a-input-number v-model.number=\"fallback.xver\" :min=\"0\" :max=\"2\"></a-input-number>\n    </a-form-item>\n  </a-form>\n  <a-divider style=\"margin:5px 0;\"></a-divider>\n</template>\n{{end}}\n"
  },
  {
    "path": "web/html/form/protocol/tun.html",
    "content": "{{define \"form/tun\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\"\n    :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.xray.tun.nameDesc\" }}</span>\n                </template>\n                Interface Name\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"inbound.settings.name\"\n            placeholder=\"xray0\"></a-input>\n    </a-form-item>\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.xray.tun.mtuDesc\" }}</span>\n                </template>\n                MTU\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input-number v-model.number=\"inbound.settings.mtu\" :min=\"1\"\n            :max=\"9000\" placeholder=\"1500\"></a-input-number>\n    </a-form-item>\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"pages.xray.tun.userLevelDesc\" }}</span>\n                </template>\n                {{ i18n \"pages.xray.tun.userLevel\" }}\n                <a-icon type=\"question-circle\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input-number v-model.number=\"inbound.settings.userLevel\" :min=\"0\"\n            placeholder=\"0\"></a-input-number>\n    </a-form-item>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/protocol/vless.html",
    "content": "{{define \"form/vless\"}}\n<a-collapse activeKey=\"0\" v-for=\"(client, index) in inbound.settings.vlesses.slice(0,1)\" v-if=\"!isEdit\">\n  <a-collapse-panel header='{{ i18n \"pages.inbounds.client\" }}'>\n    {{template \"form/client\"}}\n  </a-collapse-panel>\n</a-collapse>\n<a-collapse v-else>\n  <a-collapse-panel :header=\"'{{ i18n \"pages.client.clientCount\"}} : ' +\n    inbound.settings.vlesses.length\">\n    <table width=\"100%\">\n      <tr class=\"client-table-header\">\n        <th>{{ i18n \"pages.inbounds.email\" }}</th>\n        <th>ID</th>\n      </tr>\n      <tr v-for=\"(client, index) in inbound.settings.vlesses\"\n        :class=\"index % 2 == 1 ? ' client-table-odd-row' : ''\">\n        <td>[[ client.email ]]</td>\n        <td>[[ client.id ]]</td>\n      </tr>\n    </table>\n  </a-collapse-panel>\n</a-collapse>\n<template v-if=\" !inbound.stream.isTLS || !inbound.stream.isReality\">\n    <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n      <a-form-item label=\"Authentication\">\n        <a-select v-model=\"inbound.settings.selectedAuth\" @change=\"getNewVlessEnc\"\n          :dropdown-class-name=\"themeSwitcher.currentTheme\">\n          <a-select-option value=\"X25519, not Post-Quantum\">X25519 (not\n            Post-Quantum)</a-select-option>\n          <a-select-option value=\"ML-KEM-768, Post-Quantum\">ML-KEM-768\n            (Post-Quantum)</a-select-option>\n        </a-select>\n      </a-form-item>\n      <a-form-item label=\"decryption\">\n        <a-input v-model.trim=\"inbound.settings.decryption\"></a-input>\n      </a-form-item>\n      <a-form-item label=\"encryption\">\n        <a-input v-model=\"inbound.settings.encryption\"></a-input>\n      </a-form-item>\n      <a-form-item label=\" \">\n        <a-space>\n          <a-button type=\"primary\" icon=\"import\" @click=\"getNewVlessEnc\">Get New\n            keys</a-button>\n          <a-button danger @click=\"clearVlessEnc\">Clear</a-button>\n        </a-space>\n      </a-form-item>\n    </a-form>\n    <a-divider :style=\"{ margin: '5px 0' }\"></a-divider>\n    </template>\n    <template v-if=\"inbound.isTcp && !inbound.settings.selectedAuth\">\n      <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n        <a-form-item label=\"Fallbacks\">\n          <a-button icon=\"plus\" type=\"primary\" size=\"small\" @click=\"inbound.settings.addFallback()\"></a-button>\n        </a-form-item>\n      </a-form>\n\n      <!-- vless fallbacks -->\n      <a-form v-for=\"(fallback, index) in inbound.settings.fallbacks\" :colon=\"false\" :label-col=\"{ md: {span:8} }\"\n        :wrapper-col=\"{ md: {span:14} }\">\n        <a-divider :style=\"{ margin: '0' }\"> Fallback [[ index + 1 ]] <a-icon type=\"delete\"\n            @click=\"() => inbound.settings.delFallback(index)\"\n            :style=\"{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }\"></a-icon>\n        </a-divider>\n        <a-form-item label='SNI'>\n          <a-input v-model=\"fallback.name\"></a-input>\n        </a-form-item>\n        <a-form-item label='ALPN'>\n          <a-input v-model=\"fallback.alpn\"></a-input>\n        </a-form-item>\n        <a-form-item label='Path'>\n          <a-input v-model=\"fallback.path\"></a-input>\n        </a-form-item>\n        <a-form-item label='Dest'>\n          <a-input v-model=\"fallback.dest\"></a-input>\n        </a-form-item>\n        <a-form-item label='xVer'>\n          <a-input-number v-model.number=\"fallback.xver\" :min=\"0\" :max=\"2\"></a-input-number>\n        </a-form-item>\n      </a-form>\n      <a-divider :style=\"{ margin: '5px 0' }\"></a-divider>\n    </template>\n    <template v-if=\"inbound.canEnableVisionSeed()\">\n      <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n        <a-form-item label=\"Vision Seed\">\n          <a-row :gutter=\"8\">\n            <a-col :span=\"6\">\n              <a-input-number\n                :value=\"(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900\"\n                @change=\"(val) => updateTestseed(0, val)\" :min=\"0\" :max=\"9999\" :style=\"{ width: '100%' }\"\n                placeholder=\"900\" addon-before=\"[0]\"></a-input-number>\n            </a-col>\n            <a-col :span=\"6\">\n              <a-input-number\n                :value=\"(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500\"\n                @change=\"(val) => updateTestseed(1, val)\" :min=\"0\" :max=\"9999\" :style=\"{ width: '100%' }\"\n                placeholder=\"500\" addon-before=\"[1]\"></a-input-number>\n            </a-col>\n            <a-col :span=\"6\">\n              <a-input-number\n                :value=\"(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900\"\n                @change=\"(val) => updateTestseed(2, val)\" :min=\"0\" :max=\"9999\" :style=\"{ width: '100%' }\"\n                placeholder=\"900\" addon-before=\"[2]\"></a-input-number>\n            </a-col>\n            <a-col :span=\"6\">\n              <a-input-number\n                :value=\"(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256\"\n                @change=\"(val) => updateTestseed(3, val)\" :min=\"0\" :max=\"9999\" :style=\"{ width: '100%' }\"\n                placeholder=\"256\" addon-before=\"[3]\"></a-input-number>\n            </a-col>\n          </a-row>\n          <a-space :size=\"8\" :style=\"{ marginTop: '8px' }\">\n            <a-button type=\"primary\" @click=\"setRandomTestseed\">\n              Rand\n            </a-button>\n            <a-button @click=\"resetTestseed\">\n              Reset\n            </a-button>\n          </a-space>\n        </a-form-item>\n      </a-form>\n      <a-divider :style=\"{ margin: '5px 0' }\"></a-divider>\n    </template>\n{{end}}"
  },
  {
    "path": "web/html/form/protocol/vmess.html",
    "content": "{{define \"form/vmess\"}}\n<a-collapse activeKey=\"0\" v-for=\"(client, index) in inbound.settings.vmesses.slice(0,1)\" v-if=\"!isEdit\">    \n    <a-collapse-panel header='{{ i18n \"pages.inbounds.client\" }}'>\n        {{template \"form/client\"}}\n    </a-collapse-panel>\n</a-collapse>\n<a-collapse v-else>\n    <a-collapse-panel :header=\"'{{ i18n \"pages.client.clientCount\"}} : ' + inbound.settings.vmesses.length\">\n        <table width=\"100%\">\n            <tr class=\"client-table-header\">\n                <th>{{ i18n \"pages.inbounds.email\" }}</th>\n                <th>ID</th>\n                <th>{{ i18n \"security\" }}</th>\n            </tr>\n            <tr v-for=\"(client, index) in inbound.settings.vmesses\" :class=\"index % 2 == 1 ? 'client-table-odd-row' : ''\">\n                <td>[[ client.email ]]</td>\n                <td>[[ client.id ]]</td>\n                <td>[[ client.security ]]</td>\n            </tr>\n        </table>\n    </a-collapse-panel>\n</a-collapse>\n{{end}}\n"
  },
  {
    "path": "web/html/form/protocol/wireguard.html",
    "content": "{{define \"form/wireguard\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <a-form-item>\n    <template slot=\"label\">\n      <a-tooltip>\n        <template slot=\"title\">\n          <span>{{ i18n \"reset\" }}</span>\n        </template>\n        {{ i18n \"pages.xray.wireguard.secretKey\" }}\n        <a-icon type=\"sync\" @click=\"[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())\"></a-icon>\n      </a-tooltip>\n    </template>\n    <a-input v-model.trim=\"inbound.settings.secretKey\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"pages.xray.wireguard.publicKey\" }}'>\n    <a-input disabled v-model=\"inbound.settings.pubKey\"></a-input>\n  </a-form-item>\n  <a-form-item label='MTU'>\n    <a-input-number v-model.number=\"inbound.settings.mtu\"></a-input-number>\n  </a-form-item>\n  <a-form-item label='No Kernel Tun'>\n    <a-switch v-model=\"inbound.settings.noKernelTun\"></a-switch>\n  </a-form-item>\n  <a-form-item label=\"Peers\">\n    <a-button icon=\"plus\" type=\"primary\" size=\"small\" @click=\"inbound.settings.addPeer()\"></a-button>\n  </a-form-item>\n  <a-form v-for=\"(peer, index) in inbound.settings.peers\" :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-divider :style=\"{ margin: '0' }\"> Peer [[ index + 1 ]] <a-icon v-if=\"inbound.settings.peers.length>1\" type=\"delete\" @click=\"() => inbound.settings.delPeer(index)\" :style=\"{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }\"></a-icon>\n    </a-divider>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"reset\" }}</span>\n          </template>\n          {{ i18n \"pages.xray.wireguard.secretKey\" }}\n          <a-icon @click=\"[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())\" type=\"sync\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"peer.privateKey\"></a-input>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        {{ i18n \"pages.xray.wireguard.publicKey\" }}\n      </template>\n      <a-input v-model.trim=\"peer.publicKey\"></a-input>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"reset\" }}</span>\n          </template>\n          {{ i18n \"pages.xray.wireguard.psk\" }}\n          <a-icon @click=\"peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())\" type=\"sync\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"peer.psk\"></a-input>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        {{ i18n \"pages.xray.wireguard.allowedIPs\" }}\n        <a-button icon=\"plus\" type=\"primary\" size=\"small\" @click=\"peer.allowedIPs.push('')\"></a-button>\n      </template>\n      <template v-for=\"(aip, index) in peer.allowedIPs\" :style=\"{ marginBottom: '10px' }\">\n        <a-input v-model.trim=\"peer.allowedIPs[index]\">\n          <a-button icon=\"minus\" v-if=\"peer.allowedIPs.length>1\" slot=\"addonAfter\" size=\"small\" @click=\"peer.allowedIPs.splice(index, 1)\"></a-button>\n        </a-input>\n      </template>\n    </a-form-item>\n    <a-form-item label='Keep Alive'>\n      <a-input-number v-model.number=\"peer.keepAlive\" :min=\"0\"></a-input-number>\n    </a-form-item>\n  </a-form>\n</a-form>\n{{end}}"
  },
  {
    "path": "web/html/form/reality_settings.html",
    "content": "{{define \"form/realitySettings\"}}\n<template>\n    <a-form-item label='Show'>\n        <a-switch v-model=\"inbound.stream.reality.show\"></a-switch>\n    </a-form-item>\n    <a-form-item label='Xver'>\n        <a-input-number v-model.number=\"inbound.stream.reality.xver\" :min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='uTLS'>\n        <a-select v-model=\"inbound.stream.reality.settings.fingerprint\" :style=\"{ width: '100%' }\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"key in UTLS_FINGERPRINT\" :value=\"key\">[[ key ]]</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"reset\" }}</span>\n                </template> Target <a-icon @click=\"randomizeRealityTarget()\"\n                    type=\"sync\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"inbound.stream.reality.target\"></a-input>\n    </a-form-item>\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"reset\" }}</span>\n                </template> SNI <a-icon @click=\"randomizeRealityTarget()\"\n                    type=\"sync\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-input v-model.trim=\"inbound.stream.reality.serverNames\"></a-input>\n    </a-form-item>\n    <a-form-item label='Max Time Diff (ms)'>\n        <a-input-number v-model.number=\"inbound.stream.reality.maxTimediff\" :min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='Min Client Ver'>\n        <a-input v-model.trim=\"inbound.stream.reality.minClientVer\" placeholder='25.9.11'></a-input>\n    </a-form-item>\n    <a-form-item label='Max Client Ver'>\n        <a-input v-model.trim=\"inbound.stream.reality.maxClientVer\" placeholder='25.9.11'></a-input>\n    </a-form-item>\n    <a-form-item>\n        <template slot=\"label\">\n            <a-tooltip>\n                <template slot=\"title\">\n                    <span>{{ i18n \"reset\" }}</span>\n                </template> Short IDs <a-icon @click=\"inbound.stream.reality.shortIds = RandomUtil.randomShortIds()\"\n                    type=\"sync\"></a-icon>\n            </a-tooltip>\n        </template>\n        <a-textarea v-model.trim=\"inbound.stream.reality.shortIds\"></a-textarea>\n    </a-form-item>\n    <a-form-item label='SpiderX'>\n        <a-input v-model.trim=\"inbound.stream.reality.settings.spiderX\"></a-input>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.inbounds.publicKey\" }}'>\n        <a-textarea v-model=\"inbound.stream.reality.settings.publicKey\"></a-textarea>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.inbounds.privatekey\" }}'>\n        <a-textarea v-model=\"inbound.stream.reality.privateKey\"></a-textarea>\n    </a-form-item>\n    <a-form-item label=\" \">\n        <a-space>\n            <a-button type=\"primary\" icon=\"import\" @click=\"getNewX25519Cert\">Get New Cert</a-button>\n            <a-button danger @click=\"clearX25519Cert\">Clear</a-button>\n        </a-space>\n    </a-form-item>\n    <a-form-item label=\"mldsa65 Seed\">\n        <a-textarea v-model=\"inbound.stream.reality.mldsa65Seed\"></a-textarea>\n    </a-form-item>\n    <a-form-item label=\"mldsa65 Verify\">\n        <a-textarea v-model=\"inbound.stream.reality.settings.mldsa65Verify\"></a-textarea>\n    </a-form-item>\n    <a-form-item label=\" \">\n        <a-space>\n            <a-button type=\"primary\" icon=\"import\" @click=\"getNewmldsa65\">Get New Seed</a-button>\n            <a-button danger @click=\"clearMldsa65\">Clear</a-button>\n        </a-space>\n    </a-form-item>\n</template>\n{{end}}"
  },
  {
    "path": "web/html/form/sniffing.html",
    "content": "{{define \"form/sniffing\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <a-form-item>\n    <span slot=\"label\">\n      {{ i18n \"enabled\" }}\n        <a-tooltip>\n            <template slot=\"title\">\n                <span>{{ i18n \"pages.inbounds.noRecommendKeepDefault\" }}</span>\n            </template>\n            <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n    </span>\n    <a-switch v-model=\"inbound.sniffing.enabled\"></a-switch>\n  </a-form-item>\n  <template v-if=\"inbound.sniffing.enabled\">\n    <a-form-item :wrapper-col=\"{span:24}\">\n      <a-checkbox-group v-model=\"inbound.sniffing.destOverride\">\n        <a-checkbox v-for=\"key,value in SNIFFING_OPTION\" :value=\"key\">[[ value ]]</a-checkbox>\n      </a-checkbox-group>\n    </a-form-item>\n    <a-form-item label='Metadata Only'>\n      <a-switch v-model=\"inbound.sniffing.metadataOnly\"></a-switch>\n    </a-form-item>\n    <a-form-item label='Route Only'>\n      <a-switch v-model=\"inbound.sniffing.routeOnly\"></a-switch>\n    </a-form-item>\n  </template>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/external_proxy.html",
    "content": "{{define \"form/externalProxy\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <a-divider :style=\"{ margin: '5px 0 0' }\"></a-divider>\n  <a-form-item label=\"External Proxy\">\n    <a-switch v-model=\"externalProxy\"></a-switch>\n    <a-button icon=\"plus\" v-if=\"externalProxy\" type=\"primary\" :style=\"{ marginLeft: '10px' }\" size=\"small\"\n      @click=\"inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})\"></a-button>\n  </a-form-item>\n  <a-input-group :style=\"{ margin: '8px 0' }\" compact v-for=\"(row, index) in inbound.stream.externalProxy\">\n    <template>\n      <a-tooltip title=\"Force TLS\">\n        <a-select v-model=\"row.forceTls\" :style=\"{ width: '20%', margin: '0px' }\"\n          :dropdown-class-name=\"themeSwitcher.currentTheme\">\n          <a-select-option value=\"same\">{{ i18n \"pages.inbounds.same\" }}</a-select-option>\n          <a-select-option value=\"none\">{{ i18n \"none\" }}</a-select-option>\n          <a-select-option value=\"tls\">TLS</a-select-option>\n        </a-select>\n      </a-tooltip>\n    </template>\n    <a-input :style=\"{ width: '30%' }\" v-model.trim=\"row.dest\" placeholder='{{ i18n \"host\" }}'></a-input>\n    <a-tooltip title='{{ i18n \"pages.inbounds.port\" }}'>\n      <a-input-number :style=\"{ width: '15%' }\" v-model.number=\"row.port\" min=\"1\" max=\"65535\"></a-input-number>\n    </a-tooltip>\n    <a-input :style=\"{ width: '30%', top: '0' }\" v-model.trim=\"row.remark\" placeholder='{{ i18n \"remark\" }}'>\n      <template slot=\"addonAfter\">\n        <a-button icon=\"minus\" size=\"small\" @click=\"inbound.stream.externalProxy.splice(index, 1)\"></a-button>\n      </template>\n    </a-input>\n  </a-input-group>\n</a-form>\n{{end}}"
  },
  {
    "path": "web/html/form/stream/stream_finalmask.html",
    "content": "{{define \"form/streamFinalMask\"}}\n<a-divider :style=\"{ margin: '5px 0 0' }\"></a-divider>\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\"\n    :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label=\"UDP Masks\">\n        <a-button icon=\"plus\" type=\"primary\" size=\"small\"\n            @click=\"inbound.stream.addUdpMask(inbound.stream.network === 'kcp' ? 'mkcp-aes128gcm' : 'xdns')\"></a-button>\n    </a-form-item>\n    <template\n        v-if=\"inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0\">\n        <a-form v-for=\"(mask, index) in inbound.stream.finalmask.udp\"\n            :key=\"index\" :colon=\"false\"\n            :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n            <a-divider :style=\"{ margin: '0' }\"> UDP Mask [[ index + 1 ]]\n                <a-icon type=\"delete\"\n                    @click=\"() => inbound.stream.delUdpMask(index)\"\n                    :style=\"{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }\"></a-icon>\n            </a-divider>\n            <a-form-item label='Type'>\n                <a-select v-model=\"mask.type\"\n                    @change=\"(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                    <!-- mKCP-specific masks -->\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"mkcp-aes128gcm\">\n                        mKCP AES-128-GCM</a-select-option>\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"header-dns\">\n                        Header DNS</a-select-option>\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"header-dtls\">\n                        Header DTLS 1.2</a-select-option>\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"header-srtp\">\n                        Header SRTP</a-select-option>\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"header-utp\">\n                        Header uTP</a-select-option>\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"header-wechat\">\n                        Header WeChat Video</a-select-option>\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"header-wireguard\">\n                        Header WireGuard</a-select-option>\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"mkcp-original\">\n                        mKCP Original</a-select-option>\n                    <a-select-option v-if=\"inbound.stream.network === 'kcp'\"\n                        value=\"xicmp\">\n                        xICMP (Experimental)</a-select-option>\n                    <!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->\n                    <a-select-option\n                        v-if=\"['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)\"\n                        value=\"xdns\">\n                        xDNS (Experimental)</a-select-option>\n                </a-select>\n            </a-form-item>\n            <!-- Settings for password-based masks -->\n            <a-form-item label='Password'\n                v-if=\"['mkcp-aes128gcm'].includes(mask.type)\">\n                <a-input v-model.trim=\"mask.settings.password\"\n                    placeholder=\"Obfuscation password\"></a-input>\n            </a-form-item>\n            <!-- Settings for domain-based masks -->\n            <a-form-item label='Domain'\n                v-if=\"['header-dns', 'xdns'].includes(mask.type)\">\n                <a-input v-model.trim=\"mask.settings.domain\"\n                    placeholder=\"e.g., www.example.com\"></a-input>\n            </a-form-item>\n            <!-- Settings for xICMP -->\n            <a-form-item label='IP'\n                v-if=\"mask.type === 'xicmp'\">\n                <a-input v-model.trim=\"mask.settings.ip\"\n                    placeholder=\"e.g., 1.1.1.1\"></a-input>\n            </a-form-item>\n            <a-form-item label='ID'\n                v-if=\"mask.type === 'xicmp'\">\n                <a-input-number v-model.number=\"mask.settings.id\"\n                    :min=\"0\" :max=\"65535\"></a-input-number>\n            </a-form-item>\n        </a-form>\n    </template>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/stream_grpc.html",
    "content": "{{define \"form/streamGRPC\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label=\"Service Name\">\n        <a-input v-model.trim=\"inbound.stream.grpc.serviceName\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Authority\">\n        <a-input v-model.trim=\"inbound.stream.grpc.authority\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Multi Mode\">\n        <a-switch v-model=\"inbound.stream.grpc.multiMode\"></a-switch>\n    </a-form-item>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/stream_httpupgrade.html",
    "content": "{{define \"form/streamHTTPUpgrade\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <a-form-item label=\"Proxy Protocol\">\n    <a-switch v-model=\"inbound.stream.httpupgrade.acceptProxyProtocol\"></a-switch>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"host\" }}'>\n    <a-input v-model.trim=\"inbound.stream.httpupgrade.host\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"path\" }}'>\n    <a-input v-model.trim=\"inbound.stream.httpupgrade.path\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.requestHeader\" }}'>\n    <a-button icon=\"plus\" size=\"small\" @click=\"inbound.stream.httpupgrade.addHeader('', '')\"></a-button>\n  </a-form-item>\n  <a-form-item :wrapper-col=\"{span:24}\">\n    <a-input-group compact v-for=\"(header, index) in inbound.stream.httpupgrade.headers\">\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.name\" placeholder='{{ i18n \"pages.inbounds.stream.general.name\"}}'>\n        <template slot=\"addonBefore\" :style=\"{ margin: '0' }\">[[ index+1 ]]</template>\n      </a-input>\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.value\" placeholder='{{ i18n \"pages.inbounds.stream.general.value\" }}'>\n        <a-button icon=\"minus\" slot=\"addonAfter\" size=\"small\" @click=\"inbound.stream.httpupgrade.removeHeader(index)\"></a-button>\n      </a-input>\n    </a-input-group>\n  </a-form-item>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/stream_kcp.html",
    "content": "{{define \"form/streamKCP\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\"\n    :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='MTU'>\n        <a-input-number v-model.number=\"inbound.stream.kcp.mtu\" :min=\"576\"\n            :max=\"1460\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='TTI (ms)'>\n        <a-input-number v-model.number=\"inbound.stream.kcp.tti\" :min=\"10\"\n            :max=\"100\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='Uplink (MB/s)'>\n        <a-input-number v-model.number=\"inbound.stream.kcp.upCap\"\n            :min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='Downlink (MB/s)'>\n        <a-input-number v-model.number=\"inbound.stream.kcp.downCap\"\n            :min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='Congestion'>\n        <a-switch v-model=\"inbound.stream.kcp.congestion\"></a-switch>\n    </a-form-item>\n    <a-form-item label='Read Buffer (MB)'>\n        <a-input-number v-model.number=\"inbound.stream.kcp.readBuffer\"\n            :min=\"0\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='Write Buffer (MB)'>\n        <a-input-number v-model.number=\"inbound.stream.kcp.writeBuffer\"\n            :min=\"0\"></a-input-number>\n    </a-form-item>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/stream_settings.html",
    "content": "{{define \"form/streamSettings\"}}\n<!-- select stream network -->\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\"\n    :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='{{ i18n \"transmission\" }}'>\n        <a-select v-model=\"inbound.stream.network\" :style=\"{ width: '75%' }\"\n            @change=\"streamNetworkChange\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"tcp\">TCP (RAW)</a-select-option>\n            <a-select-option value=\"kcp\">mKCP</a-select-option>\n            <a-select-option value=\"ws\">WebSocket</a-select-option>\n            <a-select-option value=\"grpc\">gRPC</a-select-option>\n            <a-select-option value=\"httpupgrade\">HTTPUpgrade</a-select-option>\n            <a-select-option value=\"xhttp\">XHTTP</a-select-option>\n        </a-select>\n    </a-form-item>\n</a-form>\n\n<!-- tcp -->\n<template v-if=\"inbound.stream.network === 'tcp'\">\n    {{template \"form/streamTCP\"}}\n</template>\n\n<!-- kcp -->\n<template v-if=\"inbound.stream.network === 'kcp'\">\n    {{template \"form/streamKCP\"}}\n</template>\n\n<!-- ws -->\n<template v-if=\"inbound.stream.network === 'ws'\">\n    {{template \"form/streamWS\"}}\n</template>\n\n<!-- grpc -->\n<template v-if=\"inbound.stream.network === 'grpc'\">\n    {{template \"form/streamGRPC\"}}\n</template>\n\n<!-- httpupgrade -->\n<template v-if=\"inbound.stream.network === 'httpupgrade'\">\n    {{template \"form/streamHTTPUpgrade\"}}\n</template>\n\n<!-- xhttp -->\n<template v-if=\"inbound.stream.network === 'xhttp'\">\n    {{template \"form/streamXHTTP\"}}\n</template>\n\n<!-- sockopt -->\n<template>\n    {{template \"form/streamSockopt\"}}\n</template>\n\n<!-- finalmask - only for TCP, WS, HTTPUpgrade, XHTTP, mKCP -->\n<template\n    v-if=\"['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)\">\n    {{template \"form/streamFinalMask\"}}\n</template>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/stream_sockopt.html",
    "content": "{{define \"form/streamSockopt\"}}\n<a-divider :style=\"{ margin: '5px 0 0' }\"></a-divider>\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label=\"Sockopt\">\n        <a-switch v-model=\"inbound.stream.sockoptSwitch\"></a-switch>\n    </a-form-item>\n    <template v-if=\"inbound.stream.sockoptSwitch\">\n        <a-form-item label=\"Route Mark\">\n            <a-input-number v-model.number=\"inbound.stream.sockopt.mark\" :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label=\"TCP Keep Alive Interval\">\n            <a-input-number v-model.number=\"inbound.stream.sockopt.tcpKeepAliveInterval\" :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label=\"TCP Keep Alive Idle\">\n            <a-input-number v-model.number=\"inbound.stream.sockopt.tcpKeepAliveIdle\" :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label=\"TCP Max Seg\">\n            <a-input-number v-model.number=\"inbound.stream.sockopt.tcpMaxSeg\" :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label=\"TCP User Timeout\">\n            <a-input-number v-model.number=\"inbound.stream.sockopt.tcpUserTimeout\" :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label=\"TCP Window Clamp\">\n            <a-input-number v-model.number=\"inbound.stream.sockopt.tcpWindowClamp\" :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label=\"Proxy Protocol\">\n            <a-switch v-model=\"inbound.stream.sockopt.acceptProxyProtocol\"></a-switch>\n        </a-form-item>\n        <a-form-item label=\"TCP Fast Open\">\n            <a-switch v-model.trim=\"inbound.stream.sockopt.tcpFastOpen\"></a-switch>\n        </a-form-item>\n        <a-form-item label=\"Multipath TCP\">\n            <a-switch v-model.trim=\"inbound.stream.sockopt.tcpMptcp\"></a-switch>\n        </a-form-item>\n        <a-form-item label=\"Penetrate\">\n            <a-switch v-model.trim=\"inbound.stream.sockopt.penetrate\"></a-switch>\n        </a-form-item>\n        <a-form-item label=\"V6 Only\">\n            <a-switch v-model.trim=\"inbound.stream.sockopt.V6Only\"></a-switch>\n        </a-form-item>\n        <a-form-item label='Domain Strategy'>\n            <a-select v-model=\"inbound.stream.sockopt.domainStrategy\" :style=\"{ width: '50%' }\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option v-for=\"key in DOMAIN_STRATEGY_OPTION\" :value=\"key\">[[ key ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label='TCP Congestion'>\n            <a-select v-model=\"inbound.stream.sockopt.tcpcongestion\" :style=\"{ width: '50%' }\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n              <a-select-option v-for=\"key in TCP_CONGESTION_OPTION\" :value=\"key\">[[ key ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label=\"TProxy\">\n            <a-select v-model=\"inbound.stream.sockopt.tproxy\" :style=\"{ width: '50%' }\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option value=\"off\">Off</a-select-option>\n                <a-select-option value=\"redirect\">Redirect</a-select-option>\n                <a-select-option value=\"tproxy\">TProxy</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label=\"Dialer Proxy\">\n            <a-input v-model=\"inbound.stream.sockopt.dialerProxy\"></a-input>\n        </a-form-item>\n        <a-form-item label=\"Interface Name\">\n            <a-input v-model=\"inbound.stream.sockopt.interfaceName\"></a-input>\n        </a-form-item>\n        <a-form-item label=\"Trusted X-Forwarded-For\">\n            <a-select mode=\"tags\" v-model=\"inbound.stream.sockopt.trustedXForwardedFor\" :style=\"{ width: '100%' }\"\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option value=\"CF-Connecting-IP\">CF-Connecting-IP</a-select-option>\n                <a-select-option value=\"X-Real-IP\">X-Real-IP</a-select-option>\n                <a-select-option value=\"True-Client-IP\">True-Client-IP</a-select-option>\n                <a-select-option value=\"X-Client-IP\">X-Client-IP</a-select-option>\n            </a-select>\n        </a-form-item>\n    </template>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/stream_tcp.html",
    "content": "{{define \"form/streamTCP\"}}\n<!-- tcp type -->\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <a-form-item label=\"Proxy Protocol\" v-if=\"inbound.canEnableTls()\">\n    <a-switch v-model=\"inbound.stream.tcp.acceptProxyProtocol\"></a-switch>\n  </a-form-item>\n  <a-form-item label='HTTP {{ i18n \"camouflage\" }}'>\n    <a-switch :checked=\"inbound.stream.tcp.type === 'http'\" @change=\"checked => inbound.stream.tcp.type = checked ? 'http' : 'none'\"></a-switch>\n  </a-form-item>\n</a-form>\n\n<a-form v-if=\"inbound.stream.tcp.type === 'http'\" :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <!-- tcp request -->\n  <a-divider :style=\"{ margin: '0' }\">{{ i18n \"pages.inbounds.stream.general.request\" }}</a-divider>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.version\" }}'>\n    <a-input v-model.trim=\"inbound.stream.tcp.request.version\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.method\" }}'>\n    <a-input v-model.trim=\"inbound.stream.tcp.request.method\"></a-input>\n  </a-form-item>\n  <a-form-item>\n    <template slot=\"label\">{{ i18n \"pages.inbounds.stream.tcp.path\" }}\n      <a-button icon=\"plus\" size=\"small\" @click=\"inbound.stream.tcp.request.addPath('/')\"></a-button>\n    </template>\n    <template v-for=\"(path, index) in inbound.stream.tcp.request.path\">\n      <a-input v-model.trim=\"inbound.stream.tcp.request.path[index]\">\n        <a-button icon=\"minus\" size=\"small\" slot=\"addonAfter\" @click=\"inbound.stream.tcp.request.removePath(index)\" v-if=\"inbound.stream.tcp.request.path.length>1\"></a-button>\n      </a-input>\n    </template>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.requestHeader\" }}'>\n    <a-button icon=\"plus\" size=\"small\" @click=\"inbound.stream.tcp.request.addHeader('Host', '')\"></a-button>\n  </a-form-item>\n  <a-form-item :wrapper-col=\"{span:24}\">\n    <a-input-group compact v-for=\"(header, index) in inbound.stream.tcp.request.headers\">\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.name\" placeholder='{{ i18n \"pages.inbounds.stream.general.name\" }}'>\n        <template slot=\"addonBefore\" :style=\"{ margin: '0' }\">[[ index+1 ]]</template>\n      </a-input>\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.value\" placeholder='{{ i18n \"pages.inbounds.stream.general.value\" }}'>\n        <a-button icon=\"minus\" slot=\"addonAfter\" size=\"small\" @click=\"inbound.stream.tcp.request.removeHeader(index)\"></a-button>\n      </a-input>\n    </a-input-group>\n  </a-form-item>\n\n  <!-- tcp response -->\n  <a-divider :style=\"{ margin: '0' }\">{{ i18n \"pages.inbounds.stream.general.response\" }}</a-divider>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.version\" }}'>\n    <a-input v-model.trim=\"inbound.stream.tcp.response.version\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.status\" }}'>\n    <a-input v-model.trim=\"inbound.stream.tcp.response.status\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.statusDescription\" }}'>\n    <a-input v-model.trim=\"inbound.stream.tcp.response.reason\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.responseHeader\" }}'>\n    <a-button icon=\"plus\" size=\"small\" @click=\"inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')\"></a-button>\n  </a-form-item>\n  <a-form-item :wrapper-col=\"{span:24}\">\n    <a-input-group compact v-for=\"(header, index) in inbound.stream.tcp.response.headers\">\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.name\" placeholder='{{ i18n \"pages.inbounds.stream.general.name\" }}'>\n        <template slot=\"addonBefore\" :style=\"{ margin: '0' }\">[[ index+1 ]]</template>\n      </a-input>\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.value\" placeholder='{{ i18n \"pages.inbounds.stream.general.value\" }}'>\n        <template slot=\"addonAfter\">\n          <a-button icon=\"minus\" size=\"small\" @click=\"inbound.stream.tcp.response.removeHeader(index)\"></a-button>\n        </template>\n      </a-input>\n    </a-input-group>\n  </a-form-item>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/stream_ws.html",
    "content": "{{define \"form/streamWS\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <a-form-item label=\"Proxy Protocol\">\n    <a-switch v-model=\"inbound.stream.ws.acceptProxyProtocol\"></a-switch>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"host\" }}'>\n    <a-input v-model.trim=\"inbound.stream.ws.host\"></a-input>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"path\" }}'>\n    <a-input v-model.trim=\"inbound.stream.ws.path\"></a-input>\n  </a-form-item>\n  <a-form-item label='Heartbeat Period'>\n    <a-input-number v-model.number=\"inbound.stream.ws.heartbeatPeriod\" :min=\"0\"></a-input-number>\n  </a-form-item>\n  <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.requestHeader\" }}'>\n    <a-button icon=\"plus\" size=\"small\" @click=\"inbound.stream.ws.addHeader('', '')\"></a-button>\n  </a-form-item>\n  <a-form-item :wrapper-col=\"{span:24}\">\n    <a-input-group compact v-for=\"(header, index) in inbound.stream.ws.headers\">\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.name\" placeholder='{{ i18n \"pages.inbounds.stream.general.name\"}}'>\n        <template slot=\"addonBefore\" :style=\"{ margin: '0' }\">[[ index+1 ]]</template>\n      </a-input>\n      <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.value\" placeholder='{{ i18n \"pages.inbounds.stream.general.value\" }}'>\n        <a-button icon=\"minus\" slot=\"addonAfter\" size=\"small\" @click=\"inbound.stream.ws.removeHeader(index)\"></a-button>\n      </a-input>\n    </a-input-group>\n  </a-form-item>\n</a-form>\n{{end}}\n"
  },
  {
    "path": "web/html/form/stream/stream_xhttp.html",
    "content": "{{define \"form/streamXHTTP\"}}\n<a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\"\n    :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='{{ i18n \"host\" }}'>\n        <a-input v-model.trim=\"inbound.stream.xhttp.host\"></a-input>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"path\" }}'>\n        <a-input v-model.trim=\"inbound.stream.xhttp.path\"></a-input>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.inbounds.stream.tcp.requestHeader\" }}'>\n        <a-button icon=\"plus\" size=\"small\"\n            @click=\"inbound.stream.xhttp.addHeader('', '')\"></a-button>\n    </a-form-item>\n    <a-form-item :wrapper-col=\"{span:24}\">\n        <a-input-group compact\n            v-for=\"(header, index) in inbound.stream.xhttp.headers\">\n            <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.name\"\n                placeholder='{{ i18n \"pages.inbounds.stream.general.name\"}}'>\n                <template slot=\"addonBefore\" :style=\"{ margin: '0' }\">[[ index+1\n                    ]]</template>\n            </a-input>\n            <a-input :style=\"{ width: '50%' }\" v-model.trim=\"header.value\"\n                placeholder='{{ i18n \"pages.inbounds.stream.general.value\" }}'>\n                <a-button icon=\"minus\" slot=\"addonAfter\" size=\"small\"\n                    @click=\"inbound.stream.xhttp.removeHeader(index)\"></a-button>\n            </a-input>\n        </a-input-group>\n    </a-form-item>\n    <a-form-item label='Mode'>\n        <a-select v-model=\"inbound.stream.xhttp.mode\" :style=\"{ width: '50%' }\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option v-for=\"key in MODE_OPTION\" :value=\"key\">[[ key\n                ]]</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item label=\"Max Buffered Upload\"\n        v-if=\"inbound.stream.xhttp.mode === 'packet-up'\">\n        <a-input-number\n            v-model.number=\"inbound.stream.xhttp.scMaxBufferedPosts\"></a-input-number>\n    </a-form-item>\n    <a-form-item label=\"Max Upload Size (Byte)\"\n        v-if=\"inbound.stream.xhttp.mode === 'packet-up'\">\n        <a-input\n            v-model.trim=\"inbound.stream.xhttp.scMaxEachPostBytes\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Stream-Up Server\"\n        v-if=\"inbound.stream.xhttp.mode === 'stream-up'\">\n        <a-input\n            v-model.trim=\"inbound.stream.xhttp.scStreamUpServerSecs\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Padding Bytes\">\n        <a-input v-model.trim=\"inbound.stream.xhttp.xPaddingBytes\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Padding Obfs Mode\">\n        <a-switch v-model=\"inbound.stream.xhttp.xPaddingObfsMode\"></a-switch>\n    </a-form-item>\n    <template v-if=\"inbound.stream.xhttp.xPaddingObfsMode\">\n        <a-form-item label=\"Padding Key\">\n            <a-input v-model.trim=\"inbound.stream.xhttp.xPaddingKey\"\n                placeholder=\"x_padding\"></a-input>\n        </a-form-item>\n        <a-form-item label=\"Padding Header\">\n            <a-input v-model.trim=\"inbound.stream.xhttp.xPaddingHeader\"\n                placeholder=\"X-Padding\"></a-input>\n        </a-form-item>\n        <a-form-item label=\"Padding Placement\">\n            <a-select v-model=\"inbound.stream.xhttp.xPaddingPlacement\"\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option value>Default (queryInHeader)</a-select-option>\n                <a-select-option\n                    value=\"queryInHeader\">queryInHeader</a-select-option>\n                <a-select-option value=\"header\">header</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label=\"Padding Method\">\n            <a-select v-model=\"inbound.stream.xhttp.xPaddingMethod\"\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option value>Default (repeat-x)</a-select-option>\n                <a-select-option value=\"repeat-x\">repeat-x</a-select-option>\n                <a-select-option value=\"tokenish\">tokenish</a-select-option>\n            </a-select>\n        </a-form-item>\n    </template>\n    <a-form-item label=\"Uplink HTTP Method\">\n        <a-select v-model=\"inbound.stream.xhttp.uplinkHTTPMethod\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value>Default (POST)</a-select-option>\n            <a-select-option value=\"POST\">POST</a-select-option>\n            <a-select-option value=\"PUT\">PUT</a-select-option>\n            <a-select-option value=\"GET\">GET (packet-up only)</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item label=\"Session Placement\">\n        <a-select v-model=\"inbound.stream.xhttp.sessionPlacement\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value>Default (path)</a-select-option>\n            <a-select-option value=\"path\">path</a-select-option>\n            <a-select-option value=\"header\">header</a-select-option>\n            <a-select-option value=\"cookie\">cookie</a-select-option>\n            <a-select-option value=\"query\">query</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item label=\"Session Key\"\n        v-if=\"inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'\">\n        <a-input v-model.trim=\"inbound.stream.xhttp.sessionKey\"\n            placeholder=\"x_session\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Sequence Placement\">\n        <a-select v-model=\"inbound.stream.xhttp.seqPlacement\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value>Default (path)</a-select-option>\n            <a-select-option value=\"path\">path</a-select-option>\n            <a-select-option value=\"header\">header</a-select-option>\n            <a-select-option value=\"cookie\">cookie</a-select-option>\n            <a-select-option value=\"query\">query</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item label=\"Sequence Key\"\n        v-if=\"inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'\">\n        <a-input v-model.trim=\"inbound.stream.xhttp.seqKey\"\n            placeholder=\"x_seq\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Uplink Data Placement\"\n        v-if=\"inbound.stream.xhttp.mode === 'packet-up'\">\n        <a-select v-model=\"inbound.stream.xhttp.uplinkDataPlacement\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value>Default (body)</a-select-option>\n            <a-select-option value=\"body\">body</a-select-option>\n            <a-select-option value=\"header\">header</a-select-option>\n            <a-select-option value=\"query\">query</a-select-option>\n        </a-select>\n    </a-form-item>\n    <a-form-item label=\"Uplink Data Key\"\n        v-if=\"inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'\">\n        <a-input v-model.trim=\"inbound.stream.xhttp.uplinkDataKey\"\n            placeholder=\"x_data\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Uplink Chunk Size\"\n        v-if=\"inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'\">\n        <a-input-number v-model.number=\"inbound.stream.xhttp.uplinkChunkSize\"\n            :min=\"0\" placeholder=\"0 (unlimited)\"></a-input-number>\n    </a-form-item>\n    <a-form-item label=\"No SSE Header\">\n        <a-switch v-model=\"inbound.stream.xhttp.noSSEHeader\"></a-switch>\n    </a-form-item>\n</a-form>\n{{end}}"
  },
  {
    "path": "web/html/form/tls_settings.html",
    "content": "{{define \"form/tlsSettings\"}}\n<!-- tls enable -->\n<a-form v-if=\"inbound.canEnableTls()\" :colon=\"false\"\n  :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n  <a-divider :style=\"{ margin: '3px 0' }\"></a-divider>\n  <a-form-item label='{{ i18n \"security\" }}'>\n    <a-radio-group v-model=\"inbound.stream.security\" button-style=\"solid\">\n      <a-radio-button value=\"none\">{{ i18n \"none\" }}</a-radio-button>\n      <a-radio-button v-if=\"inbound.canEnableReality()\"\n        value=\"reality\">Reality</a-radio-button>\n      <a-radio-button value=\"tls\">TLS</a-radio-button>\n    </a-radio-group>\n  </a-form-item>\n\n  <!-- tls settings -->\n  <template v-if=\"inbound.stream.isTls\">\n    <a-form-item label=\"SNI\" placeholder=\"Server Name Indication\">\n      <a-input v-model.trim=\"inbound.stream.tls.sni\"></a-input>\n    </a-form-item>\n    <a-form-item label=\"Cipher Suites\">\n      <a-select v-model=\"inbound.stream.tls.cipherSuites\"\n        :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option value>Auto</a-select-option>\n        <a-select-option v-for=\"key,value in TLS_CIPHER_OPTION\" :value=\"key\">[[\n          value ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-form-item label=\"Min/Max Version\">\n      <a-input-group compact>\n        <a-select v-model=\"inbound.stream.tls.minVersion\"\n          :style=\"{ width: '50%' }\"\n          :dropdown-class-name=\"themeSwitcher.currentTheme\">\n          <a-select-option v-for=\"key in TLS_VERSION_OPTION\" :value=\"key\">[[ key\n            ]]</a-select-option>\n        </a-select>\n        <a-select v-model=\"inbound.stream.tls.maxVersion\"\n          :style=\"{ width: '50%' }\"\n          :dropdown-class-name=\"themeSwitcher.currentTheme\">\n          <a-select-option v-for=\"key in TLS_VERSION_OPTION\" :value=\"key\">[[ key\n            ]]</a-select-option>\n        </a-select>\n      </a-input-group>\n    </a-form-item>\n    <a-form-item label=\"uTLS\">\n      <a-select v-model=\"inbound.stream.tls.settings.fingerprint\"\n        :style=\"{ width: '100%' }\"\n        :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option value>None</a-select-option>\n        <a-select-option v-for=\"key in UTLS_FINGERPRINT\" :value=\"key\">[[ key\n          ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-form-item label=\"ALPN\">\n      <a-select mode=\"multiple\"\n        :dropdown-class-name=\"themeSwitcher.currentTheme\"\n        v-model=\"inbound.stream.tls.alpn\">\n        <a-select-option v-for=\"alpn in ALPN_OPTION\" :value=\"alpn\">[[ alpn\n          ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-form-item label=\"Reject Unknown SNI\">\n      <a-switch v-model=\"inbound.stream.tls.rejectUnknownSni\"></a-switch>\n    </a-form-item>\n    <a-form-item label=\"Disable System Root\">\n      <a-switch v-model=\"inbound.stream.tls.disableSystemRoot\"></a-switch>\n    </a-form-item>\n    <a-form-item label=\"Session Resumption\">\n      <a-switch v-model=\"inbound.stream.tls.enableSessionResumption\"></a-switch>\n    </a-form-item>\n    <a-divider :style=\"{ margin: '3px 0' }\"></a-divider>\n    <template v-for=\"cert,index in inbound.stream.tls.certs\">\n      <a-form-item label='{{ i18n \"certificate\" }}'>\n        <a-radio-group v-model=\"cert.useFile\" button-style=\"solid\"\n          :style=\"{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }\">\n          <a-radio-button :value=\"true\"\n            :style=\"{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }\">{{\n            i18n \"pages.inbounds.certificatePath\" }}</a-radio-button>\n          <a-radio-button :value=\"false\"\n            :style=\"{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }\">{{\n            i18n \"pages.inbounds.certificateContent\" }}</a-radio-button>\n        </a-radio-group>\n      </a-form-item>\n      <a-form-item label=\" \">\n        <a-space>\n          <a-button icon=\"plus\" v-if=\"index === 0\" type=\"primary\" size=\"small\"\n            @click=\"inbound.stream.tls.addCert()\"></a-button>\n          <a-button icon=\"minus\" v-if=\"inbound.stream.tls.certs.length>1\"\n            type=\"primary\" size=\"small\"\n            @click=\"inbound.stream.tls.removeCert(index)\"></a-button>\n        </a-space>\n      </a-form-item>\n      <template v-if=\"cert.useFile\">\n        <a-form-item label='{{ i18n \"pages.inbounds.publicKey\" }}'>\n          <a-input v-model.trim=\"cert.certFile\"></a-input>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.inbounds.privatekey\" }}'>\n          <a-input v-model.trim=\"cert.keyFile\"></a-input>\n        </a-form-item>\n        <a-form-item label=\" \">\n          <a-button type=\"primary\" icon=\"import\"\n            @click=\"setDefaultCertData(index)\">\n            {{ i18n \"pages.inbounds.setDefaultCert\" }}</a-button>\n        </a-form-item>\n      </template>\n      <template v-else>\n        <a-form-item label='{{ i18n \"pages.inbounds.publicKey\" }}'>\n          <a-textarea v-model=\"cert.cert\"></a-textarea>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.inbounds.privatekey\" }}'>\n          <a-textarea v-model=\"cert.key\"></a-textarea>\n        </a-form-item>\n      </template>\n      <a-form-item label=\"One Time Loading\">\n        <a-switch v-model=\"cert.oneTimeLoading\"></a-switch>\n      </a-form-item>\n      <a-form-item label='Usage Option'>\n        <a-select v-model=\"cert.usage\" :style=\"{ width: '50%' }\"\n          :dropdown-class-name=\"themeSwitcher.currentTheme\">\n          <a-select-option v-for=\"key in USAGE_OPTION\" :value=\"key\">[[ key\n            ]]</a-select-option>\n        </a-select>\n      </a-form-item>\n      <a-form-item label=\"Build Chain\" v-if=\"cert.usage === 'issue'\">\n        <a-switch v-model=\"cert.buildChain\"></a-switch>\n      </a-form-item>\n    </template>\n    <a-form-item label='ECH key'>\n      <a-input v-model=\"inbound.stream.tls.echServerKeys\"></a-input>\n    </a-form-item>\n    <a-form-item label='ECH config'>\n      <a-input v-model=\"inbound.stream.tls.settings.echConfigList\"></a-input>\n    </a-form-item>\n    <a-form-item label='ECH force query'>\n      <a-select v-model=\"inbound.stream.tls.echForceQuery\"\n        :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option v-for=\"key in ['none', 'half', 'full']\" :value=\"key\">[[\n          key ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-form-item label=\" \">\n      <a-space>\n        <a-button type=\"primary\" icon=\"import\" @click=\"getNewEchCert\">Get New\n          ECH Cert</a-button>\n        <a-button danger @click=\"clearEchCert\">Clear</a-button>\n      </a-space>\n    </a-form-item>\n  </template>\n\n  <!-- reality settings -->\n  <template v-if=\"inbound.stream.isReality\">\n    {{template \"form/realitySettings\"}}\n  </template>\n</a-form>\n{{end}}"
  },
  {
    "path": "web/html/inbounds.html",
    "content": "{{ template \"page/head_start\" .}}\n{{ template \"page/head_end\" .}}\n\n{{ template \"page/body_start\" .}}\n<a-layout id=\"app\" v-cloak :class=\"themeSwitcher.currentTheme + ' inbounds-page'\">\n  <a-sidebar></a-sidebar>\n  <a-layout id=\"content-layout\">\n    <a-layout-content>\n      <a-spin :spinning=\"loadingStates.spinning\" :delay=\"500\" tip='{{ i18n \"loading\"}}'>\n        <transition name=\"list\" appear>\n          <a-alert type=\"error\" v-if=\"showAlert && loadingStates.fetched\" :style=\"{ marginBottom: '10px' }\"\n            message='{{ i18n \"secAlertTitle\" }}' color=\"red\" description='{{ i18n \"secAlertSsl\" }}' show-icon closable>\n          </a-alert>\n        </transition>\n        <transition name=\"list\" appear>\n          <a-row v-if=\"!loadingStates.fetched\">\n            <a-card\n              :style=\"{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }\">\n              <a-spin tip='{{ i18n \"loading\" }}'></a-spin>\n            </a-card>\n          </a-row>\n          <a-row :gutter=\"[isMobile ? 8 : 16, isMobile ? 0 : 12]\" v-else>\n            <a-col>\n              <a-card size=\"small\" :style=\"{ padding: '16px' }\" hoverable>\n                <a-row>\n                  <a-col :sm=\"12\" :md=\"5\">\n                    <a-custom-statistic title='{{ i18n \"pages.inbounds.totalDownUp\" }}'\n                      :value=\"`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`\">\n                      <template #prefix>\n                        <a-icon type=\"swap\"></a-icon>\n                      </template>\n                    </a-custom-statistic>\n                  </a-col>\n                  <a-col :sm=\"12\" :md=\"5\">\n                    <a-custom-statistic title='{{ i18n \"pages.inbounds.totalUsage\" }}'\n                      :value=\"SizeFormatter.sizeFormat(total.up + total.down)\"\n                      :style=\"{ marginTop: isMobile ? '10px' : 0 }\">\n                      <template #prefix>\n                        <a-icon type=\"pie-chart\"></a-icon>\n                      </template>\n                    </a-custom-statistic>\n                  </a-col>\n                  <a-col :sm=\"12\" :md=\"5\">\n                    <a-custom-statistic title='{{ i18n \"pages.inbounds.allTimeTrafficUsage\" }}'\n                      :value=\"SizeFormatter.sizeFormat(total.allTime)\" :style=\"{ marginTop: isMobile ? '10px' : 0 }\">\n                      <template #prefix>\n                        <a-icon type=\"history\"></a-icon>\n                      </template>\n                    </a-custom-statistic>\n                  </a-col>\n                  <a-col :sm=\"12\" :md=\"5\">\n                    <a-custom-statistic title='{{ i18n \"pages.inbounds.inboundCount\" }}' :value=\"dbInbounds.length\"\n                      :style=\"{ marginTop: isMobile ? '10px' : 0 }\">\n                      <template #prefix>\n                        <a-icon type=\"bars\"></a-icon>\n                      </template>\n                    </a-custom-statistic>\n                  </a-col>\n                  <a-col :sm=\"12\" :md=\"4\">\n                    <a-custom-statistic title='{{ i18n \"clients\" }}' value=\" \"\n                      :style=\"{ marginTop: isMobile ? '10px' : 0 }\">\n                      <template #prefix>\n                        <a-space direction=\"horizontal\">\n                          <a-icon type=\"team\"></a-icon>\n                          <div>\n                            <a-back-top :target=\"() => document.getElementById('content-layout')\"\n                              visibility-height=\"200\"></a-back-top>\n                            <a-tag color=\"green\">[[ total.clients ]]</a-tag>\n                            <a-popover title='{{ i18n \"disabled\" }}' :overlay-class-name=\"themeSwitcher.currentTheme\">\n                              <template slot=\"content\">\n                                <div v-for=\"clientEmail in total.deactive\"><span>[[ clientEmail ]]</span></div>\n                              </template>\n                              <a-tag v-if=\"total.deactive.length\">[[ total.deactive.length ]]</a-tag>\n                            </a-popover>\n                            <a-popover title='{{ i18n \"depleted\" }}' :overlay-class-name=\"themeSwitcher.currentTheme\">\n                              <template slot=\"content\">\n                                <div v-for=\"clientEmail in total.depleted\"><span>[[ clientEmail ]]</span></div>\n                              </template>\n                              <a-tag color=\"red\" v-if=\"total.depleted.length\">[[ total.depleted.length ]]</a-tag>\n                            </a-popover>\n                            <a-popover title='{{ i18n \"depletingSoon\" }}'\n                              :overlay-class-name=\"themeSwitcher.currentTheme\">\n                              <template slot=\"content\">\n                                <div v-for=\"clientEmail in total.expiring\"><span>[[ clientEmail ]]</span></div>\n                              </template>\n                              <a-tag color=\"orange\" v-if=\"total.expiring.length\">[[ total.expiring.length ]]</a-tag>\n                            </a-popover>\n                            <a-popover title='{{ i18n \"online\" }}' :overlay-class-name=\"themeSwitcher.currentTheme\">\n                              <template slot=\"content\">\n                                <div v-for=\"clientEmail in onlineClients\"><span>[[ clientEmail ]]</span></div>\n                              </template>\n                              <a-tag color=\"blue\" v-if=\"onlineClients.length\">[[ onlineClients.length ]]</a-tag>\n                            </a-popover>\n                          </div>\n                        </a-space>\n                      </template>\n                    </a-custom-statistic>\n                  </a-col>\n                </a-row>\n              </a-card>\n            </a-col>\n            <a-col>\n              <a-card hoverable>\n                <template #title>\n                  <a-space direction=\"horizontal\">\n                    <a-button type=\"primary\" icon=\"plus\" @click=\"openAddInbound\">\n                      <template v-if=\"!isMobile\">{{ i18n \"pages.inbounds.addInbound\" }}</template>\n                    </a-button>\n                    <a-dropdown :trigger=\"['click']\">\n                      <a-button type=\"primary\" icon=\"menu\">\n                        <template v-if=\"!isMobile\">{{ i18n \"pages.inbounds.generalActions\" }}</template>\n                      </a-button>\n                      <a-menu slot=\"overlay\" @click=\"a => generalActions(a)\" :theme=\"themeSwitcher.currentTheme\">\n                        <a-menu-item key=\"import\">\n                          <a-icon type=\"import\"></a-icon>\n                          {{ i18n \"pages.inbounds.importInbound\" }}\n                        </a-menu-item>\n                        <a-menu-item key=\"export\">\n                          <a-icon type=\"export\"></a-icon>\n                          {{ i18n \"pages.inbounds.export\" }}\n                        </a-menu-item>\n                        <a-menu-item key=\"subs\" v-if=\"subSettings.enable\">\n                          <a-icon type=\"export\"></a-icon>\n                          {{ i18n \"pages.inbounds.export\" }} - {{ i18n \"pages.settings.subSettings\" }}\n                        </a-menu-item>\n                        <a-menu-item key=\"resetInbounds\">\n                          <a-icon type=\"reload\"></a-icon>\n                          {{ i18n \"pages.inbounds.resetAllTraffic\" }}\n                        </a-menu-item>\n                        <a-menu-item key=\"resetClients\">\n                          <a-icon type=\"file-done\"></a-icon>\n                          {{ i18n \"pages.inbounds.resetAllClientTraffics\" }}\n                        </a-menu-item>\n                        <a-menu-item key=\"delDepletedClients\" :style=\"{ color: '#FF4D4F' }\">\n                          <a-icon type=\"rest\"></a-icon>\n                          {{ i18n \"pages.inbounds.delDepletedClients\" }}\n                        </a-menu-item>\n                      </a-menu>\n                    </a-dropdown>\n                  </a-space>\n                </template>\n                <template #extra>\n                  <a-button-group>\n                    <a-button icon=\"sync\" @click=\"manualRefresh\" :loading=\"refreshing\"></a-button>\n                    <a-popover placement=\"bottomRight\" trigger=\"click\" :overlay-class-name=\"themeSwitcher.currentTheme\">\n                      <template #title>\n                        <div class=\"ant-custom-popover-title\">\n                          <a-switch v-model=\"isRefreshEnabled\" @change=\"toggleRefresh\" size=\"small\"></a-switch>\n                          <span>{{ i18n \"pages.inbounds.autoRefresh\" }}</span>\n                        </div>\n                      </template>\n                      <template #content>\n                        <a-space direction=\"vertical\">\n                          <span>{{ i18n \"pages.inbounds.autoRefreshInterval\" }}</span>\n                          <a-select v-model=\"refreshInterval\" :disabled=\"!isRefreshEnabled\" :style=\"{ width: '100%' }\"\n                            @change=\"changeRefreshInterval\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                            <a-select-option v-for=\"key in [5,10,30,60]\" :value=\"key*1000\">[[ key ]]s</a-select-option>\n                          </a-select>\n                        </a-space>\n                      </template>\n                      <a-button icon=\"down\"></a-button>\n                    </a-popover>\n                  </a-button-group>\n                </template>\n                <a-space direction=\"vertical\">\n                  <div :style=\"isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }\">\n                    <a-switch v-model=\"enableFilter\"\n                      :style=\"isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }\"\n                      @change=\"toggleFilter\">\n                      <a-icon slot=\"checkedChildren\" type=\"search\"></a-icon>\n                      <a-icon slot=\"unCheckedChildren\" type=\"filter\"></a-icon>\n                    </a-switch>\n                    <a-input v-if=\"!enableFilter\" v-model.lazy=\"searchKey\" placeholder='{{ i18n \"search\" }}' autofocus\n                      :style=\"{ maxWidth: '300px' }\" :size=\"isMobile ? 'small' : ''\"></a-input>\n                    <a-radio-group v-if=\"enableFilter\" v-model=\"filterBy\" @change=\"filterInbounds\" button-style=\"solid\"\n                      :size=\"isMobile ? 'small' : ''\">\n                      <a-radio-button value=\"\">{{ i18n \"none\" }}</a-radio-button>\n                      <a-radio-button value=\"deactive\">{{ i18n \"disabled\" }}</a-radio-button>\n                      <a-radio-button value=\"depleted\">{{ i18n \"depleted\" }}</a-radio-button>\n                      <a-radio-button value=\"expiring\">{{ i18n \"depletingSoon\" }}</a-radio-button>\n                      <a-radio-button value=\"online\">{{ i18n \"online\" }}</a-radio-button>\n                    </a-radio-group>\n                  </div>\n                  <a-table :columns=\"isMobile ? mobileColumns : columns\" :row-key=\"dbInbound => dbInbound.id\"\n                    :data-source=\"searchedInbounds\" :scroll=\"isMobile ? {} : { x: 1000 }\"\n                    :pagination=pagination(searchedInbounds) :expand-icon-as-cell=\"false\" :expand-row-by-click=\"false\"\n                    :expand-icon-column-index=\"0\" :indent-size=\"0\"\n                    :row-class-name=\"dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')\"\n                    :style=\"{ marginTop: '10px' }\"\n                    :locale='{ filterConfirm: `{{ i18n \"confirm\" }}`, filterReset: `{{ i18n \"reset\" }}`, emptyText: `{{ i18n \"noData\" }}` }'>\n                    <template slot=\"action\" slot-scope=\"text, dbInbound\">\n                      <a-dropdown :trigger=\"['click']\">\n                        <a-icon @click=\"e => e.preventDefault()\" type=\"more\"\n                          :style=\"{ fontSize: '20px', textDecoration: 'solid' }\"></a-icon>\n                        <a-menu slot=\"overlay\" @click=\"a => clickAction(a, dbInbound)\"\n                          :theme=\"themeSwitcher.currentTheme\">\n                          <a-menu-item key=\"edit\">\n                            <a-icon type=\"edit\"></a-icon>\n                            {{ i18n \"edit\" }}\n                          </a-menu-item>\n                          <a-menu-item key=\"qrcode\"\n                            v-if=\"(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard\">\n                            <a-icon type=\"qrcode\"></a-icon>\n                            {{ i18n \"qrCode\" }}\n                          </a-menu-item>\n                          <template v-if=\"dbInbound.isMultiUser()\">\n                            <a-menu-item key=\"addClient\">\n                              <a-icon type=\"user-add\"></a-icon>\n                              {{ i18n \"pages.client.add\"}}\n                            </a-menu-item>\n                            <a-menu-item key=\"addBulkClient\">\n                              <a-icon type=\"usergroup-add\"></a-icon>\n                              {{ i18n \"pages.client.bulk\"}}\n                            </a-menu-item>\n                            <a-menu-item key=\"resetClients\">\n                              <a-icon type=\"file-done\"></a-icon>\n                              {{ i18n \"pages.inbounds.resetInboundClientTraffics\"}}\n                            </a-menu-item>\n                            <a-menu-item key=\"export\">\n                              <a-icon type=\"export\"></a-icon>\n                              {{ i18n \"pages.inbounds.export\"}}\n                            </a-menu-item>\n                            <a-menu-item key=\"subs\" v-if=\"subSettings.enable\">\n                              <a-icon type=\"export\"></a-icon>\n                              {{ i18n \"pages.inbounds.export\"}} - {{ i18n \"pages.settings.subSettings\" }}\n                            </a-menu-item>\n                            <a-menu-item key=\"delDepletedClients\" :style=\"{ color: '#FF4D4F' }\">\n                              <a-icon type=\"rest\"></a-icon>\n                              {{ i18n \"pages.inbounds.delDepletedClients\" }}\n                            </a-menu-item>\n                          </template>\n                          <template v-else>\n                            <a-menu-item key=\"showInfo\">\n                              <a-icon type=\"info-circle\"></a-icon>\n                              {{ i18n \"info\"}}\n                            </a-menu-item>\n                          </template>\n                          <a-menu-item key=\"clipboard\">\n                            <a-icon type=\"copy\"></a-icon>\n                            {{ i18n \"pages.inbounds.exportInbound\" }}\n                          </a-menu-item>\n                          <a-menu-item key=\"resetTraffic\">\n                            <a-icon type=\"retweet\"></a-icon> {{ i18n \"pages.inbounds.resetTraffic\" }}\n                          </a-menu-item>\n                          <a-menu-item key=\"clone\">\n                            <a-icon type=\"block\"></a-icon> {{ i18n \"pages.inbounds.clone\"}}\n                          </a-menu-item>\n                          <a-menu-item key=\"delete\">\n                            <span :style=\"{ color: '#FF4D4F' }\">\n                              <a-icon type=\"delete\"></a-icon> {{ i18n \"delete\"}}\n                            </span>\n                          </a-menu-item>\n                          <a-menu-item v-if=\"isMobile\">\n                            <a-switch size=\"small\" v-model=\"dbInbound.enable\"\n                              @change=\"switchEnable(dbInbound.id,dbInbound.enable)\"></a-switch>\n                            {{ i18n \"pages.inbounds.enable\" }}\n                          </a-menu-item>\n                        </a-menu>\n                      </a-dropdown>\n                    </template>\n                    <template slot=\"protocol\" slot-scope=\"text, dbInbound\">\n                      <a-tag :style=\"{ margin: '0' }\" color=\"purple\">[[ dbInbound.protocol ]]</a-tag>\n                      <template v-if=\"dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS\">\n                        <a-tag :style=\"{ margin: '0' }\" color=\"green\">[[ dbInbound.toInbound().stream.network ]]</a-tag>\n                        <a-tag :style=\"{ margin: '0' }\" v-if=\"dbInbound.toInbound().stream.isTls\"\n                          color=\"blue\">TLS</a-tag>\n                        <a-tag :style=\"{ margin: '0' }\" v-if=\"dbInbound.toInbound().stream.isReality\"\n                          color=\"blue\">Reality</a-tag>\n                      </template>\n                    </template>\n                    <template slot=\"clients\" slot-scope=\"text, dbInbound\">\n                      <template v-if=\"clientCount[dbInbound.id]\">\n                        <a-tag :style=\"{ margin: '0' }\" color=\"green\">[[ clientCount[dbInbound.id].clients ]]</a-tag>\n                        <a-popover title='{{ i18n \"disabled\" }}' :overlay-class-name=\"themeSwitcher.currentTheme\">\n                          <template slot=\"content\">\n                            <div v-for=\"clientEmail in clientCount[dbInbound.id].deactive\" :key=\"clientEmail\"\n                              class=\"client-popup-item\">\n                              <span>[[ clientEmail ]]</span>\n                              <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                <template #title>\n                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]\n                                </template>\n                                <a-icon type=\"message\"\n                                  v-if=\"clientCount[dbInbound.id].comments.get(clientEmail)\"></a-icon>\n                              </a-tooltip>\n                            </div>\n                          </template>\n                          <a-tag :style=\"{ margin: '0', padding: '0 2px' }\"\n                            v-if=\"clientCount[dbInbound.id].deactive.length\">[[\n                            clientCount[dbInbound.id].deactive.length ]]</a-tag>\n                        </a-popover>\n                        <a-popover title='{{ i18n \"depleted\" }}' :overlay-class-name=\"themeSwitcher.currentTheme\">\n                          <template slot=\"content\">\n                            <div v-for=\"clientEmail in clientCount[dbInbound.id].depleted\" :key=\"clientEmail\"\n                              class=\"client-popup-item\">\n                              <span>[[ clientEmail ]]</span>\n                              <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                <template #title>\n                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]\n                                </template>\n                                <a-icon type=\"message\"\n                                  v-if=\"clientCount[dbInbound.id].comments.get(clientEmail)\"></a-icon>\n                              </a-tooltip>\n                            </div>\n                          </template>\n                          <a-tag :style=\"{ margin: '0', padding: '0 2px' }\" color=\"red\"\n                            v-if=\"clientCount[dbInbound.id].depleted.length\">[[\n                            clientCount[dbInbound.id].depleted.length ]]</a-tag>\n                        </a-popover>\n                        <a-popover title='{{ i18n \"depletingSoon\" }}' :overlay-class-name=\"themeSwitcher.currentTheme\">\n                          <template slot=\"content\">\n                            <div v-for=\"clientEmail in clientCount[dbInbound.id].expiring\" :key=\"clientEmail\"\n                              class=\"client-popup-item\">\n                              <span>[[ clientEmail ]]</span>\n                              <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                <template #title>\n                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]\n                                </template>\n                                <a-icon type=\"message\"\n                                  v-if=\"clientCount[dbInbound.id].comments.get(clientEmail)\"></a-icon>\n                              </a-tooltip>\n                            </div>\n                          </template>\n                          <a-tag :style=\"{ margin: '0', padding: '0 2px' }\" color=\"orange\"\n                            v-if=\"clientCount[dbInbound.id].expiring.length\">[[\n                            clientCount[dbInbound.id].expiring.length ]]</a-tag>\n                        </a-popover>\n                        <a-popover title='{{ i18n \"online\" }}' :overlay-class-name=\"themeSwitcher.currentTheme\">\n                          <template slot=\"content\">\n                            <div v-for=\"clientEmail in clientCount[dbInbound.id].online\" :key=\"clientEmail\"\n                              class=\"client-popup-item\">\n                              <span>[[ clientEmail ]]</span>\n                              <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                <template #title>\n                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]\n                                </template>\n                                <a-icon type=\"message\"\n                                  v-if=\"clientCount[dbInbound.id].comments.get(clientEmail)\"></a-icon>\n                              </a-tooltip>\n                            </div>\n                          </template>\n                          <a-tag :style=\"{ margin: '0', padding: '0 2px' }\" color=\"blue\"\n                            v-if=\"clientCount[dbInbound.id].online.length\">[[ clientCount[dbInbound.id].online.length\n                            ]]</a-tag>\n                        </a-popover>\n                      </template>\n                    </template>\n                    <template slot=\"traffic\" slot-scope=\"text, dbInbound\">\n                      <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n                        <template slot=\"content\">\n                          <table cellpadding=\"2\" width=\"100%\">\n                            <tr>\n                              <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>\n                              <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>\n                            </tr>\n                            <tr v-if=\"dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total\">\n                              <td>{{ i18n \"remained\" }}</td>\n                              <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>\n                            </tr>\n                          </table>\n                        </template>\n                        <a-tag\n                          :color=\"ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)\">\n                          [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /\n                          <template v-if=\"dbInbound.total > 0\">\n                            [[ SizeFormatter.sizeFormat(dbInbound.total) ]]\n                          </template>\n                          <template v-else>\n                            <svg height=\"10px\" width=\"14px\" viewBox=\"0 0 640 512\" fill=\"currentColor\">\n                              <path\n                                d=\"M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z\"\n                                fill=\"currentColor\"></path>\n                            </svg>\n                          </template>\n                        </a-tag>\n                      </a-popover>\n                    </template>\n                    <template slot=\"allTimeInbound\" slot-scope=\"text, dbInbound\">\n                      <a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag>\n                    </template>\n                    <template slot=\"enable\" slot-scope=\"text, dbInbound\">\n                      <a-switch v-model=\"dbInbound.enable\"\n                        @change=\"switchEnable(dbInbound.id,dbInbound.enable)\"></a-switch>\n                    </template>\n                    <template slot=\"expiryTime\" slot-scope=\"text, dbInbound\">\n                      <a-popover v-if=\"dbInbound.expiryTime > 0\" :overlay-class-name=\"themeSwitcher.currentTheme\">\n                        <template slot=\"content\">\n                          [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]\n                        </template>\n                        <a-tag :style=\"{ minWidth: '50px' }\"\n                          :color=\"ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)\">\n                          [[ IntlUtil.formatRelativeTime(dbInbound.expiryTime) ]]\n                        </a-tag>\n                      </a-popover>\n                      <a-tag v-else color=\"purple\" class=\"infinite-tag\">\n                        <svg height=\"10px\" width=\"14px\" viewBox=\"0 0 640 512\" fill=\"currentColor\">\n                          <path\n                            d=\"M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z\"\n                            fill=\"currentColor\"></path>\n                        </svg>\n                      </a-tag>\n                    </template>\n                    <template slot=\"info\" slot-scope=\"text, dbInbound\">\n                      <a-popover placement=\"bottomRight\" :overlay-class-name=\"themeSwitcher.currentTheme\"\n                        trigger=\"click\">\n                        <template slot=\"content\">\n                          <table cellpadding=\"2\">\n                            <tr>\n                              <td>{{ i18n \"pages.inbounds.protocol\" }}</td>\n                              <td>\n                                <a-tag :style=\"{ margin: '0' }\" color=\"purple\">[[ dbInbound.protocol ]]</a-tag>\n                                <template\n                                  v-if=\"dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS\">\n                                  <a-tag :style=\"{ margin: '0' }\" color=\"blue\">[[ dbInbound.toInbound().stream.network\n                                    ]]</a-tag>\n                                  <a-tag :style=\"{ margin: '0' }\" v-if=\"dbInbound.toInbound().stream.isTls\"\n                                    color=\"green\">tls</a-tag>\n                                  <a-tag :style=\"{ margin: '0' }\" v-if=\"dbInbound.toInbound().stream.isReality\"\n                                    color=\"green\">reality</a-tag>\n                                </template>\n                              </td>\n                            </tr>\n                            <tr>\n                              <td>{{ i18n \"pages.inbounds.port\" }}</td>\n                              <td><a-tag>[[ dbInbound.port ]]</a-tag></td>\n                            </tr>\n                            <tr v-if=\"clientCount[dbInbound.id]\">\n                              <td>{{ i18n \"clients\" }}</td>\n                              <td>\n                                <a-tag :style=\"{ margin: '0' }\" color=\"blue\">[[ clientCount[dbInbound.id].clients\n                                  ]]</a-tag>\n                                <a-popover title='{{ i18n \"disabled\" }}'\n                                  :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                  <template slot=\"content\">\n                                    <div v-for=\"clientEmail in clientCount[dbInbound.id].deactive\" :key=\"clientEmail\"\n                                      class=\"client-popup-item\">\n                                      <span>[[ clientEmail ]]</span>\n                                      <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                        <template #title>\n                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]\n                                        </template>\n                                        <a-icon type=\"message\"\n                                          v-if=\"clientCount[dbInbound.id].comments.get(clientEmail)\"></a-icon>\n                                      </a-tooltip>\n                                    </div>\n                                  </template>\n                                  <a-tag :style=\"{ margin: '0', padding: '0 2px' }\"\n                                    v-if=\"clientCount[dbInbound.id].deactive.length\">[[\n                                    clientCount[dbInbound.id].deactive.length ]]</a-tag>\n                                </a-popover>\n                                <a-popover title='{{ i18n \"depleted\" }}'\n                                  :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                  <template slot=\"content\">\n                                    <div v-for=\"clientEmail in clientCount[dbInbound.id].depleted\" :key=\"clientEmail\"\n                                      class=\"client-popup-item\">\n                                      <span>[[ clientEmail ]]</span>\n                                      <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                        <template #title>\n                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]\n                                        </template>\n                                        <a-icon type=\"message\"\n                                          v-if=\"clientCount[dbInbound.id].comments.get(clientEmail)\"></a-icon>\n                                      </a-tooltip>\n                                    </div>\n                                  </template>\n                                  <a-tag :style=\"{ margin: '0', padding: '0 2px' }\" color=\"red\"\n                                    v-if=\"clientCount[dbInbound.id].depleted.length\">[[\n                                    clientCount[dbInbound.id].depleted.length ]]</a-tag>\n                                </a-popover>\n                                <a-popover title='{{ i18n \"depletingSoon\" }}'\n                                  :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                  <template slot=\"content\">\n                                    <div v-for=\"clientEmail in clientCount[dbInbound.id].expiring\" :key=\"clientEmail\"\n                                      class=\"client-popup-item\">\n                                      <span>[[ clientEmail ]]</span>\n                                      <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                        <template #title>\n                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]\n                                        </template>\n                                        <a-icon type=\"message\"\n                                          v-if=\"clientCount[dbInbound.id].comments.get(clientEmail)\"></a-icon>\n                                      </a-tooltip>\n                                    </div>\n                                  </template>\n                                  <a-tag :style=\"{ margin: '0', padding: '0 2px' }\" color=\"orange\"\n                                    v-if=\"clientCount[dbInbound.id].expiring.length\">[[\n                                    clientCount[dbInbound.id].expiring.length ]]</a-tag>\n                                </a-popover>\n                                <a-popover title='{{ i18n \"online\" }}' :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                  <template slot=\"content\">\n                                    <div v-for=\"clientEmail in clientCount[dbInbound.id].online\" :key=\"clientEmail\"\n                                      class=\"client-popup-item\">\n                                      <span>[[ clientEmail ]]</span>\n                                      <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                        <template #title>\n                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]\n                                        </template>\n                                        <a-icon type=\"message\"\n                                          v-if=\"clientCount[dbInbound.id].comments.get(clientEmail)\"></a-icon>\n                                      </a-tooltip>\n                                    </div>\n                                  </template>\n                                  <a-tag :style=\"{ margin: '0', padding: '0 2px' }\" color=\"green\"\n                                    v-if=\"clientCount[dbInbound.id].online.length\">[[\n                                    clientCount[dbInbound.id].online.length ]]</a-tag>\n                                </a-popover>\n                              </td>\n                            </tr>\n                            <tr>\n                              <td>{{ i18n \"pages.inbounds.traffic\" }}</td>\n                              <td>\n                                <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n                                  <template slot=\"content\">\n                                    <table cellpadding=\"2\" width=\"100%\">\n                                      <tr>\n                                        <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>\n                                        <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>\n                                      </tr>\n                                      <tr\n                                        v-if=\"dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total\">\n                                        <td>{{ i18n \"remained\" }}</td>\n                                        <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down)\n                                          ]]</td>\n                                      </tr>\n                                    </table>\n                                  </template>\n                                  <a-tag\n                                    :color=\"ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)\">\n                                    [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /\n                                    <template v-if=\"dbInbound.total > 0\">\n                                      [[ SizeFormatter.sizeFormat(dbInbound.total) ]]\n                                    </template>\n                                    <template v-else>\n                                      <svg height=\"10px\" width=\"14px\" viewBox=\"0 0 640 512\" fill=\"currentColor\">\n                                        <path\n                                          d=\"M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z\"\n                                          fill=\"currentColor\"></path>\n                                      </svg>\n                                    </template>\n                                  </a-tag>\n                                </a-popover>\n                              </td>\n                            </tr>\n                            <tr>\n                              <td>{{ i18n \"pages.inbounds.expireDate\" }}</td>\n                              <td>\n                                <a-tag :style=\"{ minWidth: '50px', textAlign: 'center' }\"\n                                  v-if=\"dbInbound.expiryTime > 0\" :color=\"dbInbound.isExpiry? 'red': 'blue'\">\n                                  [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]\n                                </a-tag>\n                                <a-tag v-else :style=\"{ textAlign: 'center' }\" color=\"purple\" class=\"infinite-tag\">\n                                  <svg height=\"10px\" width=\"14px\" viewBox=\"0 0 640 512\" fill=\"currentColor\">\n                                    <path\n                                      d=\"M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z\"\n                                      fill=\"currentColor\"></path>\n                                  </svg>\n                                </a-tag>\n                              </td>\n                            </tr>\n                            <tr>\n                              <td>{{ i18n \"pages.inbounds.periodicTrafficResetTitle\" }}</td>\n                              <td>\n                                <a-tag color=\"blue\">[[ dbInbound.trafficReset ]]</a-tag>\n                              </td>\n                            </tr>\n                          </table>\n                        </template>\n                        <a-badge>\n                          <a-icon v-if=\"!dbInbound.enable\" slot=\"count\" type=\"pause-circle\"\n                            :style=\"{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }\"></a-icon>\n                          <a-button shape=\"round\" size=\"small\" :style=\"{ fontSize: '14px', padding: '0 10px' }\">\n                            <a-icon type=\"info\"></a-icon>\n                          </a-button>\n                        </a-badge>\n                      </a-popover>\n                    </template>\n                    <template slot=\"expandedRowRender\" slot-scope=\"record\">\n                      <a-table :row-key=\"client => client.id\" :columns=\"isMobile ? innerMobileColumns : innerColumns\"\n                        :data-source=\"getInboundClients(record)\" :pagination=pagination(getInboundClients(record))\n                        :style=\"{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }\">\n                        {{template \"component/aClientTable\"}}\n                      </a-table>\n                    </template>\n                  </a-table>\n                </a-space>\n              </a-card>\n            </a-col>\n          </a-row>\n        </transition>\n      </a-spin>\n    </a-layout-content>\n  </a-layout>\n</a-layout>\n{{template \"page/body_scripts\" .}}\n<script src=\"{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}\"></script>\n{{template \"component/aSidebar\" .}}\n{{template \"component/aThemeSwitch\" .}}\n{{template \"component/aCustomStatistic\" .}}\n{{template \"component/aPersianDatepicker\" .}}\n{{template \"modals/inboundModal\"}}\n{{template \"modals/promptModal\"}}\n{{template \"modals/qrcodeModal\"}}\n{{template \"modals/textModal\"}}\n{{template \"modals/inboundInfoModal\"}}\n{{template \"modals/clientsModal\"}}\n{{template \"modals/clientsBulkModal\"}}\n<script>\n  const columns = [{\n    title: \"ID\",\n    align: 'right',\n    dataIndex: \"id\",\n    width: 30,\n    responsive: [\"xs\"],\n  }, {\n    title: '{{ i18n \"pages.inbounds.operate\" }}',\n    align: 'center',\n    width: 30,\n    scopedSlots: { customRender: 'action' },\n  }, {\n    title: '{{ i18n \"pages.inbounds.enable\" }}',\n    align: 'center',\n    width: 35,\n    scopedSlots: { customRender: 'enable' },\n  }, {\n    title: '{{ i18n \"pages.inbounds.remark\" }}',\n    align: 'center',\n    width: 60,\n    dataIndex: \"remark\",\n  }, {\n    title: '{{ i18n \"pages.inbounds.port\" }}',\n    align: 'center',\n    dataIndex: \"port\",\n    width: 40,\n  }, {\n    title: '{{ i18n \"pages.inbounds.protocol\" }}',\n    align: 'left',\n    width: 70,\n    scopedSlots: { customRender: 'protocol' },\n  }, {\n    title: '{{ i18n \"clients\" }}',\n    align: 'left',\n    width: 50,\n    scopedSlots: { customRender: 'clients' },\n  }, {\n    title: '{{ i18n \"pages.inbounds.traffic\" }}',\n    align: 'center',\n    width: 90,\n    scopedSlots: { customRender: 'traffic' },\n  }, {\n    title: '{{ i18n \"pages.inbounds.allTimeTraffic\" }}',\n    align: 'center',\n    width: 60,\n    scopedSlots: { customRender: 'allTimeInbound' },\n  }, {\n    title: '{{ i18n \"pages.inbounds.expireDate\" }}',\n    align: 'center',\n    width: 40,\n    scopedSlots: { customRender: 'expiryTime' },\n  }];\n\n  const mobileColumns = [{\n    title: \"ID\",\n    align: 'right',\n    dataIndex: \"id\",\n    width: 10,\n    responsive: [\"s\"],\n  }, {\n    title: '{{ i18n \"pages.inbounds.operate\" }}',\n    align: 'center',\n    width: 25,\n    scopedSlots: { customRender: 'action' },\n  }, {\n    title: '{{ i18n \"pages.inbounds.remark\" }}',\n    align: 'left',\n    width: 70,\n    dataIndex: \"remark\",\n  }, {\n    title: '{{ i18n \"pages.inbounds.info\" }}',\n    align: 'center',\n    width: 10,\n    scopedSlots: { customRender: 'info' },\n  }];\n\n  const innerColumns = [\n    { title: '{{ i18n \"pages.inbounds.operate\" }}', width: 70, scopedSlots: { customRender: 'actions' } },\n    { title: '{{ i18n \"pages.inbounds.enable\" }}', width: 30, scopedSlots: { customRender: 'enable' } },\n    { title: '{{ i18n \"online\" }}', width: 32, scopedSlots: { customRender: 'online' } },\n    { title: '{{ i18n \"pages.inbounds.client\" }}', width: 80, scopedSlots: { customRender: 'client' } },\n    { title: '{{ i18n \"pages.inbounds.traffic\" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },\n    { title: '{{ i18n \"pages.inbounds.allTimeTraffic\" }}', width: 60, align: 'center', scopedSlots: { customRender: 'allTime' } },\n    { title: '{{ i18n \"pages.inbounds.expireDate\" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },\n  ];\n\n  const innerMobileColumns = [\n    { title: '{{ i18n \"pages.inbounds.operate\" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } },\n    { title: '{{ i18n \"pages.inbounds.client\" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },\n    { title: '{{ i18n \"pages.inbounds.info\" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },\n  ];\n\n  const app = new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#app',\n    mixins: [MediaQueryMixin],\n    data: {\n      themeSwitcher,\n      persianDatepicker,\n      loadingStates: {\n        fetched: false,\n        spinning: false\n      },\n      inbounds: [],\n      dbInbounds: [],\n      searchKey: '',\n      enableFilter: false,\n      filterBy: '',\n      searchedInbounds: [],\n      expireDiff: 0,\n      trafficDiff: 0,\n      defaultCert: '',\n      defaultKey: '',\n      clientCount: [],\n      onlineClients: [],\n      lastOnlineMap: {},\n      isRefreshEnabled: localStorage.getItem(\"isRefreshEnabled\") === \"true\" ? true : false,\n      refreshing: false,\n      refreshInterval: Number(localStorage.getItem(\"refreshInterval\")) || 5000,\n      subSettings: {\n        enable: false,\n        subTitle: '',\n        subURI: '',\n        subJsonURI: '',\n        subJsonEnable: false,\n      },\n      remarkModel: '-ieo',\n      datepicker: 'gregorian',\n      tgBotEnable: false,\n      showAlert: false,\n      ipLimitEnable: false,\n      pageSize: 0,\n    },\n    methods: {\n      loading(spinning = true) {\n        this.loadingStates.spinning = spinning;\n      },\n      async getDBInbounds() {\n        this.refreshing = true;\n        const msg = await HttpUtil.get('/panel/api/inbounds/list');\n        if (!msg.success) {\n          this.refreshing = false;\n          return;\n        }\n\n        await this.getLastOnlineMap();\n        await this.getOnlineUsers();\n\n        this.setInbounds(msg.obj);\n        setTimeout(() => {\n          this.refreshing = false;\n        }, 500);\n      },\n      async getOnlineUsers() {\n        const msg = await HttpUtil.post('/panel/api/inbounds/onlines');\n        if (!msg.success) {\n          return;\n        }\n        this.onlineClients = msg.obj != null ? msg.obj : [];\n      },\n      async getLastOnlineMap() {\n        const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');\n        if (!msg.success || !msg.obj) return;\n        this.lastOnlineMap = msg.obj || {}\n      },\n      async getDefaultSettings() {\n        const msg = await HttpUtil.post('/panel/setting/defaultSettings');\n        if (!msg.success) {\n          return;\n        }\n        with (msg.obj) {\n          this.expireDiff = expireDiff * 86400000;\n          this.trafficDiff = trafficDiff * 1073741824;\n          this.defaultCert = defaultCert;\n          this.defaultKey = defaultKey;\n          this.tgBotEnable = tgBotEnable;\n          this.subSettings = {\n            enable: subEnable,\n            subTitle: subTitle,\n            subURI: subURI,\n            subJsonURI: subJsonURI,\n            subJsonEnable: subJsonEnable,\n          };\n          this.pageSize = pageSize;\n          this.remarkModel = remarkModel;\n          this.datepicker = datepicker;\n          this.ipLimitEnable = ipLimitEnable;\n        }\n      },\n      setInbounds(dbInbounds) {\n        this.inbounds.splice(0);\n        this.dbInbounds.splice(0);\n        this.clientCount.splice(0);\n        for (const inbound of dbInbounds) {\n          const dbInbound = new DBInbound(inbound);\n          to_inbound = dbInbound.toInbound()\n          this.inbounds.push(to_inbound);\n          this.dbInbounds.push(dbInbound);\n          if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {\n            if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {\n              continue;\n            }\n            this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);\n          }\n        }\n        if (!this.loadingStates.fetched) {\n          this.loadingStates.fetched = true\n        }\n        if (this.enableFilter) {\n          this.filterInbounds();\n        } else {\n          this.searchInbounds(this.searchKey);\n        }\n      },\n      getClientCounts(dbInbound, inbound) {\n        let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [], comments = new Map();\n        clients = inbound.clients;\n        clientStats = dbInbound.clientStats\n        now = new Date().getTime()\n        if (clients) {\n          clientCount = clients.length;\n          if (dbInbound.enable) {\n            clients.forEach(client => {\n              if (client.comment) {\n                comments.set(client.email, client.comment)\n              }\n              if (client.enable) {\n                active.push(client.email);\n                if (this.isClientOnline(client.email)) online.push(client.email);\n              } else {\n                deactive.push(client.email);\n              }\n            });\n            clientStats.forEach(stats => {\n              const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;\n              const expired = stats.expiryTime > 0 && stats.expiryTime <= now;\n              if (expired || exhausted) {\n                depleted.push(stats.email);\n              } else {\n                const expiringSoon = (stats.expiryTime > 0 && (stats.expiryTime - now < this.expireDiff)) ||\n                  (stats.total > 0 && (stats.total - (stats.up + stats.down) < this.trafficDiff));\n                if (expiringSoon) expiring.push(stats.email);\n              }\n            });\n          } else {\n            clients.forEach(client => {\n              deactive.push(client.email);\n            });\n          }\n        }\n        return {\n          clients: clientCount,\n          active: active,\n          deactive: deactive,\n          depleted: depleted,\n          expiring: expiring,\n          online: online,\n          comments: comments,\n        };\n      },\n\n      searchInbounds(key) {\n        if (ObjectUtil.isEmpty(key)) {\n          this.searchedInbounds = this.dbInbounds.slice();\n        } else {\n          this.searchedInbounds.splice(0, this.searchedInbounds.length);\n          this.dbInbounds.forEach(inbound => {\n            if (ObjectUtil.deepSearch(inbound, key)) {\n              const newInbound = new DBInbound(inbound);\n              const inboundSettings = JSON.parse(inbound.settings);\n              if (inboundSettings.hasOwnProperty('clients')) {\n                const searchedSettings = { \"clients\": [] };\n                inboundSettings.clients.forEach(client => {\n                  if (ObjectUtil.deepSearch(client, key)) {\n                    searchedSettings.clients.push(client);\n                  }\n                });\n                newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);\n              }\n              this.searchedInbounds.push(newInbound);\n            }\n          });\n        }\n      },\n      filterInbounds() {\n        if (ObjectUtil.isEmpty(this.filterBy)) {\n          this.searchedInbounds = this.dbInbounds.slice();\n        } else {\n          this.searchedInbounds.splice(0, this.searchedInbounds.length);\n          this.dbInbounds.forEach(inbound => {\n            const newInbound = new DBInbound(inbound);\n            const inboundSettings = JSON.parse(inbound.settings);\n            if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)) {\n              const list = this.clientCount[inbound.id][this.filterBy];\n              if (list.length > 0) {\n                const filteredSettings = { \"clients\": [] };\n                if (inboundSettings.clients) {\n                  inboundSettings.clients.forEach(client => {\n                    if (list.includes(client.email)) {\n                      filteredSettings.clients.push(client);\n                    }\n                  });\n                }\n                newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);\n                this.searchedInbounds.push(newInbound);\n              }\n            }\n          });\n        }\n      },\n      toggleFilter() {\n        if (this.enableFilter) {\n          this.searchKey = '';\n        } else {\n          this.filterBy = '';\n          this.searchedInbounds = this.dbInbounds.slice();\n        }\n      },\n      generalActions(action) {\n        switch (action.key) {\n          case \"import\":\n            this.importInbound();\n            break;\n          case \"export\":\n            this.exportAllLinks();\n            break;\n          case \"subs\":\n            this.exportAllSubs();\n            break;\n          case \"resetInbounds\":\n            this.resetAllTraffic();\n            break;\n          case \"resetClients\":\n            this.resetAllClientTraffics(-1);\n            break;\n          case \"delDepletedClients\":\n            this.delDepletedClients(-1)\n            break;\n        }\n      },\n      clickAction(action, dbInbound) {\n        switch (action.key) {\n          case \"qrcode\":\n            this.showQrcode(dbInbound.id);\n            break;\n          case \"showInfo\":\n            this.showInfo(dbInbound.id);\n            break;\n          case \"edit\":\n            this.openEditInbound(dbInbound.id);\n            break;\n          case \"addClient\":\n            this.openAddClient(dbInbound.id)\n            break;\n          case \"addBulkClient\":\n            this.openAddBulkClient(dbInbound.id)\n            break;\n          case \"export\":\n            this.inboundLinks(dbInbound.id);\n            break;\n          case \"subs\":\n            this.exportSubs(dbInbound.id);\n            break;\n          case \"clipboard\":\n            this.copy(dbInbound.id);\n            break;\n          case \"resetTraffic\":\n            this.resetTraffic(dbInbound.id);\n            break;\n          case \"resetClients\":\n            this.resetAllClientTraffics(dbInbound.id);\n            break;\n          case \"clone\":\n            this.openCloneInbound(dbInbound);\n            break;\n          case \"delete\":\n            this.delInbound(dbInbound.id);\n            break;\n          case \"delDepletedClients\":\n            this.delDepletedClients(dbInbound.id)\n            break;\n        }\n      },\n      openCloneInbound(dbInbound) {\n        this.$confirm({\n          title: '{{ i18n \"pages.inbounds.cloneInbound\"}} \\\"' + dbInbound.remark + '\\\"',\n          content: '{{ i18n \"pages.inbounds.cloneInboundContent\"}}',\n          okText: '{{ i18n \"pages.inbounds.cloneInboundOk\"}}',\n          class: themeSwitcher.currentTheme,\n          cancelText: '{{ i18n \"cancel\" }}',\n          onOk: () => {\n            const baseInbound = dbInbound.toInbound();\n            dbInbound.up = 0;\n            dbInbound.down = 0;\n            this.cloneInbound(baseInbound, dbInbound);\n          },\n        });\n      },\n      async cloneInbound(baseInbound, dbInbound) {\n        const data = {\n          up: dbInbound.up,\n          down: dbInbound.down,\n          total: dbInbound.total,\n          remark: dbInbound.remark + \" - Cloned\",\n          enable: dbInbound.enable,\n          expiryTime: dbInbound.expiryTime,\n          trafficReset: dbInbound.trafficReset,\n          lastTrafficResetTime: dbInbound.lastTrafficResetTime,\n\n          listen: '',\n          port: RandomUtil.randomInteger(10000, 60000),\n          protocol: baseInbound.protocol,\n          settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),\n          streamSettings: baseInbound.stream.toString(),\n          sniffing: baseInbound.sniffing.toString(),\n        };\n        await this.submit('/panel/api/inbounds/add', data, inModal);\n      },\n      openAddInbound() {\n        inModal.show({\n          title: '{{ i18n \"pages.inbounds.addInbound\"}}',\n          okText: '{{ i18n \"create\"}}',\n          cancelText: '{{ i18n \"close\" }}',\n          confirm: async (inbound, dbInbound) => {\n            await this.addInbound(inbound, dbInbound, inModal);\n          },\n          isEdit: false\n        });\n      },\n      openEditInbound(dbInboundId) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        const inbound = dbInbound.toInbound();\n        inModal.show({\n          title: '{{ i18n \"pages.inbounds.modifyInbound\"}}',\n          okText: '{{ i18n \"update\"}}',\n          cancelText: '{{ i18n \"close\" }}',\n          inbound: inbound,\n          dbInbound: dbInbound,\n          confirm: async (inbound, dbInbound) => {\n            await this.updateInbound(inbound, dbInbound);\n          },\n          isEdit: true\n        });\n      },\n      async addInbound(inbound, dbInbound) {\n        const data = {\n          up: dbInbound.up,\n          down: dbInbound.down,\n          total: dbInbound.total,\n          remark: dbInbound.remark,\n          enable: dbInbound.enable,\n          expiryTime: dbInbound.expiryTime,\n          trafficReset: dbInbound.trafficReset,\n          lastTrafficResetTime: dbInbound.lastTrafficResetTime,\n\n          listen: inbound.listen,\n          port: inbound.port,\n          protocol: inbound.protocol,\n          settings: inbound.settings.toString(),\n        };\n        if (inbound.canEnableStream()) {\n          data.streamSettings = inbound.stream.toString();\n        } else if (inbound.stream?.sockopt) {\n          data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);\n        }\n        data.sniffing = inbound.sniffing.toString();\n\n        await this.submit('/panel/api/inbounds/add', data, inModal);\n      },\n      async updateInbound(inbound, dbInbound) {\n        const data = {\n          up: dbInbound.up,\n          down: dbInbound.down,\n          total: dbInbound.total,\n          remark: dbInbound.remark,\n          enable: dbInbound.enable,\n          expiryTime: dbInbound.expiryTime,\n          trafficReset: dbInbound.trafficReset,\n          lastTrafficResetTime: dbInbound.lastTrafficResetTime,\n\n          listen: inbound.listen,\n          port: inbound.port,\n          protocol: inbound.protocol,\n          settings: inbound.settings.toString(),\n        };\n        if (inbound.canEnableStream()) {\n          data.streamSettings = inbound.stream.toString();\n        } else if (inbound.stream?.sockopt) {\n          data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);\n        }\n        data.sniffing = inbound.sniffing.toString();\n\n        await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal);\n      },\n      openAddClient(dbInboundId) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        clientModal.show({\n          title: '{{ i18n \"pages.client.add\"}}',\n          okText: '{{ i18n \"pages.client.submitAdd\"}}',\n          dbInbound: dbInbound,\n          confirm: async (clients, dbInboundId) => {\n            await this.addClient(clients, dbInboundId, clientModal);\n          },\n          isEdit: false\n        });\n      },\n      openAddBulkClient(dbInboundId) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        clientsBulkModal.show({\n          title: '{{ i18n \"pages.client.bulk\"}} ' + dbInbound.remark,\n          okText: '{{ i18n \"pages.client.bulk\"}}',\n          dbInbound: dbInbound,\n          confirm: async (clients, dbInboundId) => {\n            await this.addClient(clients, dbInboundId, clientsBulkModal);\n          },\n        });\n      },\n      openEditClient(dbInboundId, client) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        if (!dbInbound) return;\n        clients = this.getInboundClients(dbInbound);\n        if (!clients || !Array.isArray(clients)) return;\n        index = this.findIndexOfClient(dbInbound.protocol, clients, client);\n        if (index < 0) return;\n        clientModal.show({\n          title: '{{ i18n \"pages.client.edit\"}}',\n          okText: '{{ i18n \"pages.client.submitEdit\"}}',\n          dbInbound: dbInbound,\n          index: index,\n          confirm: async (client, dbInboundId, clientId) => {\n            clientModal.loading();\n            await this.updateClient(client, dbInboundId, clientId);\n            clientModal.close();\n          },\n          isEdit: true\n        });\n      },\n      findIndexOfClient(protocol, clients, client) {\n        if (!clients || !Array.isArray(clients) || !client) {\n          return -1;\n        }\n        switch (protocol) {\n          case Protocols.TROJAN:\n          case Protocols.SHADOWSOCKS:\n            return clients.findIndex(item => item && item.password === client.password && item.email === client.email);\n          default: return clients.findIndex(item => item && item.id === client.id && item.email === client.email);\n        }\n      },\n      async addClient(clients, dbInboundId, modal) {\n        const data = {\n          id: dbInboundId,\n          settings: '{\"clients\": [' + clients.toString() + ']}',\n        };\n        await this.submit(`/panel/api/inbounds/addClient`, data, modal);\n      },\n      async updateClient(client, dbInboundId, clientId) {\n        const data = {\n          id: dbInboundId,\n          settings: '{\"clients\": [' + client.toString() + ']}',\n        };\n        await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);\n      },\n      resetTraffic(dbInboundId) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        this.$confirm({\n          title: '{{ i18n \"pages.inbounds.resetTraffic\"}}' + ' #' + dbInboundId,\n          content: '{{ i18n \"pages.inbounds.resetTrafficContent\"}}',\n          class: themeSwitcher.currentTheme,\n          okText: '{{ i18n \"reset\"}}',\n          cancelText: '{{ i18n \"cancel\"}}',\n          onOk: () => {\n            const inbound = dbInbound.toInbound();\n            dbInbound.up = 0;\n            dbInbound.down = 0;\n            this.updateInbound(inbound, dbInbound);\n          },\n        });\n      },\n      delInbound(dbInboundId) {\n        this.$confirm({\n          title: '{{ i18n \"pages.inbounds.deleteInbound\"}}' + ' #' + dbInboundId,\n          content: '{{ i18n \"pages.inbounds.deleteInboundContent\"}}',\n          class: themeSwitcher.currentTheme,\n          okText: '{{ i18n \"delete\"}}',\n          cancelText: '{{ i18n \"cancel\"}}',\n          onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),\n        });\n      },\n      delClient(dbInboundId, client, confirmation = true) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        clientId = this.getClientId(dbInbound.protocol, client);\n        if (confirmation) {\n          this.$confirm({\n            title: '{{ i18n \"pages.inbounds.deleteClient\"}}' + ' ' + client.email,\n            content: '{{ i18n \"pages.inbounds.deleteClientContent\"}}',\n            class: themeSwitcher.currentTheme,\n            okText: '{{ i18n \"delete\"}}',\n            cancelText: '{{ i18n \"cancel\"}}',\n            onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),\n          });\n        } else {\n          this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);\n        }\n      },\n      getSubGroupClients(dbInbounds, currentClient) {\n        const response = {\n          inbounds: [],\n          clients: [],\n          editIds: []\n        }\n        if (dbInbounds && dbInbounds.length > 0 && currentClient) {\n          dbInbounds.forEach((dbInboundItem) => {\n            const dbInbound = new DBInbound(dbInboundItem);\n            if (dbInbound) {\n              const inbound = dbInbound.toInbound();\n              if (inbound) {\n                const clients = inbound.clients;\n                if (clients.length > 0) {\n                  clients.forEach((client) => {\n                    if (client['subId'] === currentClient['subId']) {\n                      client['inboundId'] = dbInboundItem.id\n                      client['clientId'] = this.getClientId(dbInbound.protocol, client)\n                      response.inbounds.push(dbInboundItem.id)\n                      response.clients.push(client)\n                      response.editIds.push(client['clientId'])\n                    }\n                  })\n                }\n              }\n            }\n          })\n        }\n        return response;\n      },\n      getClientId(protocol, client) {\n        switch (protocol) {\n          case Protocols.TROJAN: return client.password;\n          case Protocols.SHADOWSOCKS: return client.email;\n          default: return client.id;\n        }\n      },\n      checkFallback(dbInbound) {\n        newDbInbound = new DBInbound(dbInbound);\n        if (dbInbound.listen.startsWith(\"@\")) {\n          rootInbound = this.inbounds.find((i) =>\n            i.isTcp &&\n            ['trojan', 'vless'].includes(i.protocol) &&\n            i.settings.fallbacks.find(f => f.dest === dbInbound.listen)\n          );\n          if (rootInbound) {\n            newDbInbound.listen = rootInbound.listen;\n            newDbInbound.port = rootInbound.port;\n            newInbound = newDbInbound.toInbound();\n            newInbound.stream.security = rootInbound.stream.security;\n            newInbound.stream.tls = rootInbound.stream.tls;\n            newInbound.stream.externalProxy = rootInbound.stream.externalProxy;\n            newDbInbound.streamSettings = newInbound.stream.toString();\n          }\n        }\n        return newDbInbound;\n      },\n      showQrcode(dbInboundId, client) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        newDbInbound = this.checkFallback(dbInbound);\n        qrModal.show('{{ i18n \"qrCode\"}}', newDbInbound, client);\n      },\n      showInfo(dbInboundId, client) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        if (!dbInbound) return;\n        index = 0;\n        if (dbInbound.isMultiUser()) {\n          inbound = dbInbound.toInbound();\n          clients = inbound && inbound.clients ? inbound.clients : null;\n          if (clients && Array.isArray(clients)) {\n            index = this.findIndexOfClient(dbInbound.protocol, clients, client);\n            if (index < 0) index = 0;\n          }\n        }\n        newDbInbound = this.checkFallback(dbInbound);\n        infoModal.show(newDbInbound, index);\n      },\n      switchEnable(dbInboundId, state) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        dbInbound.enable = state;\n        this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound);\n      },\n      async switchEnableClient(dbInboundId, client) {\n        this.loading()\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        if (!dbInbound) return;\n        inbound = dbInbound.toInbound();\n        clients = inbound && inbound.clients ? inbound.clients : null;\n        if (!clients || !Array.isArray(clients)) return;\n        index = this.findIndexOfClient(dbInbound.protocol, clients, client);\n        if (index < 0 || !clients[index]) return;\n        clients[index].enable = !clients[index].enable;\n        clientId = this.getClientId(dbInbound.protocol, clients[index]);\n        await this.updateClient(clients[index], dbInboundId, clientId);\n        this.loading(false);\n      },\n      async submit(url, data, modal) {\n        const msg = await HttpUtil.postWithModal(url, data, modal);\n        if (msg.success) {\n          await this.getDBInbounds();\n        }\n      },\n      getInboundClients(dbInbound) {\n        if (!dbInbound) return null;\n        const inbound = dbInbound.toInbound();\n        return inbound && inbound.clients ? inbound.clients : null;\n      },\n      resetClientTraffic(client, dbInboundId, confirmation = true) {\n        if (confirmation) {\n          this.$confirm({\n            title: '{{ i18n \"pages.inbounds.resetTraffic\"}}' + ' ' + client.email,\n            content: '{{ i18n \"pages.inbounds.resetTrafficContent\"}}',\n            class: themeSwitcher.currentTheme,\n            okText: '{{ i18n \"reset\"}}',\n            cancelText: '{{ i18n \"cancel\"}}',\n            onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email),\n          })\n        } else {\n          this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);\n        }\n      },\n      resetAllTraffic() {\n        this.$confirm({\n          title: '{{ i18n \"pages.inbounds.resetAllTrafficTitle\"}}',\n          content: '{{ i18n \"pages.inbounds.resetAllTrafficContent\"}}',\n          class: themeSwitcher.currentTheme,\n          okText: '{{ i18n \"reset\"}}',\n          cancelText: '{{ i18n \"cancel\"}}',\n          onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),\n        });\n      },\n      resetAllClientTraffics(dbInboundId) {\n        this.$confirm({\n          title: dbInboundId > 0 ? '{{ i18n \"pages.inbounds.resetInboundClientTrafficTitle\"}}' : '{{ i18n \"pages.inbounds.resetAllClientTrafficTitle\"}}',\n          content: dbInboundId > 0 ? '{{ i18n \"pages.inbounds.resetInboundClientTrafficContent\"}}' : '{{ i18n \"pages.inbounds.resetAllClientTrafficContent\"}}',\n          class: themeSwitcher.currentTheme,\n          okText: '{{ i18n \"reset\"}}',\n          cancelText: '{{ i18n \"cancel\"}}',\n          onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),\n        })\n      },\n      delDepletedClients(dbInboundId) {\n        this.$confirm({\n          title: '{{ i18n \"pages.inbounds.delDepletedClientsTitle\"}}',\n          content: '{{ i18n \"pages.inbounds.delDepletedClientsContent\"}}',\n          class: themeSwitcher.currentTheme,\n          okText: '{{ i18n \"delete\"}}',\n          cancelText: '{{ i18n \"cancel\"}}',\n          onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),\n        })\n      },\n      isExpiry(dbInbound, index) {\n        return dbInbound.toInbound().isExpiry(index);\n      },\n      getUpStats(dbInbound, email) {\n        if (email.length == 0) return 0;\n        clientStats = dbInbound.clientStats.find(stats => stats.email === email);\n        return clientStats ? clientStats.up : 0;\n      },\n      getDownStats(dbInbound, email) {\n        if (email.length == 0) return 0;\n        clientStats = dbInbound.clientStats.find(stats => stats.email === email);\n        return clientStats ? clientStats.down : 0;\n      },\n      getSumStats(dbInbound, email) {\n        if (email.length == 0) return 0;\n        clientStats = dbInbound.clientStats.find(stats => stats.email === email);\n        return clientStats ? clientStats.up + clientStats.down : 0;\n      },\n      getAllTimeClient(dbInbound, email) {\n        if (email.length == 0) return 0;\n        clientStats = dbInbound.clientStats.find(stats => stats.email === email);\n        if (!clientStats) return 0;\n        return clientStats.allTime || (clientStats.up + clientStats.down);\n      },\n      getRemStats(dbInbound, email) {\n        if (email.length == 0) return 0;\n        clientStats = dbInbound.clientStats.find(stats => stats.email === email);\n        if (!clientStats) return 0;\n        remained = clientStats.total - (clientStats.up + clientStats.down);\n        return remained > 0 ? remained : 0;\n      },\n      clientStatsColor(dbInbound, email) {\n        if (email.length == 0) return ColorUtils.clientUsageColor();\n        clientStats = dbInbound.clientStats.find(stats => stats.email === email);\n        return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)\n      },\n      statsProgress(dbInbound, email) {\n        if (email.length == 0) return 100;\n        clientStats = dbInbound.clientStats.find(stats => stats.email === email);\n        if (!clientStats) return 0;\n        if (clientStats.total == 0) return 100;\n        return 100 * (clientStats.down + clientStats.up) / clientStats.total;\n      },\n      expireProgress(expTime, reset) {\n        now = new Date().getTime();\n        remainedSeconds = expTime < 0 ? -expTime / 1000 : (expTime - now) / 1000;\n        resetSeconds = reset * 86400;\n        if (remainedSeconds >= resetSeconds) return 0;\n        return 100 * (1 - (remainedSeconds / resetSeconds));\n      },\n      statsExpColor(dbInbound, email) {\n        if (email.length == 0) return '#7a316f';\n        clientStats = dbInbound.clientStats.find(stats => stats.email === email);\n        if (!clientStats) return '#7a316f';\n        statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);\n        expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);\n        switch (true) {\n          case statsColor == \"red\" || expColor == \"red\":\n            return \"#cf3c3c\"; // Red\n          case statsColor == \"orange\" || expColor == \"orange\":\n            return \"#f37b24\"; // Orange\n          case statsColor == \"green\" || expColor == \"green\":\n            return \"#008771\"; // Green\n          default:\n            return \"#7a316f\"; // purple\n        }\n      },\n      isClientEnabled(dbInbound, email) {\n        clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;\n        return clientStats ? clientStats['enable'] : true;\n      },\n      isClientDepleted(dbInbound, email) {\n        if (!email || !dbInbound || !dbInbound.clientStats) return false;\n        const stats = dbInbound.clientStats.find(s => s.email === email);\n        if (!stats) return false;\n        const total = stats.total ?? 0;\n        const used = (stats.up ?? 0) + (stats.down ?? 0);\n        const hasTotal = total > 0;\n        const exhausted = hasTotal && used >= total;\n        const expiryTime = stats.expiryTime ?? 0;\n        const hasExpiry = expiryTime > 0;\n        const now = Date.now();\n        const expired = hasExpiry && expiryTime <= now;\n        return expired || exhausted;\n      },\n      isClientOnline(email) {\n        return this.onlineClients.includes(email);\n      },\n      getLastOnline(email) {\n        return this.lastOnlineMap[email] || null\n      },\n      formatLastOnline(email) {\n        const ts = this.getLastOnline(email)\n        if (!ts) return '-'\n        // Check if IntlUtil is available (may not be loaded yet)\n        if (typeof IntlUtil !== 'undefined' && IntlUtil.formatDate) {\n          return IntlUtil.formatDate(ts)\n        }\n        // Fallback to simple date formatting if IntlUtil is not available\n        return new Date(ts).toLocaleString()\n      },\n      isRemovable(dbInboundId) {\n        return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;\n      },\n      inboundLinks(dbInboundId) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        newDbInbound = this.checkFallback(dbInbound);\n        txtModal.show('{{ i18n \"pages.inbounds.export\"}}', newDbInbound.genInboundLinks(this.remarkModel), newDbInbound.remark);\n      },\n      exportSubs(dbInboundId) {\n        const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        const clients = this.getInboundClients(dbInbound);\n        let subLinks = []\n        if (clients != null) {\n          clients.forEach(c => {\n            if (c.subId && c.subId.length > 0) {\n              subLinks.push(this.subSettings.subURI + c.subId)\n            }\n          })\n        }\n        txtModal.show(\n          '{{ i18n \"pages.inbounds.export\"}} - {{ i18n \"pages.settings.subSettings\" }}',\n          [...new Set(subLinks)].join('\\n'),\n          dbInbound.remark + \"-Subs\");\n      },\n      importInbound() {\n        promptModal.open({\n          title: '{{ i18n \"pages.inbounds.importInbound\" }}',\n          type: 'textarea',\n          value: '',\n          okText: '{{ i18n \"pages.inbounds.import\" }}',\n          confirm: async (dbInboundText) => {\n            await this.submit('/panel/api/inbounds/import', { data: dbInboundText }, promptModal);\n          },\n        });\n      },\n      exportAllSubs() {\n        let subLinks = []\n        for (const dbInbound of this.dbInbounds) {\n          const clients = this.getInboundClients(dbInbound);\n          if (clients != null) {\n            clients.forEach(c => {\n              if (c.subId && c.subId.length > 0) {\n                subLinks.push(this.subSettings.subURI + c.subId)\n              }\n            })\n          }\n        }\n        txtModal.show(\n          '{{ i18n \"pages.inbounds.export\"}} - {{ i18n \"pages.settings.subSettings\" }}',\n          [...new Set(subLinks)].join('\\r\\n'),\n          'All-Inbounds-Subs');\n      },\n      exportAllLinks() {\n        let copyText = [];\n        for (const dbInbound of this.dbInbounds) {\n          copyText.push(dbInbound.genInboundLinks(this.remarkModel));\n        }\n        txtModal.show('{{ i18n \"pages.inbounds.export\"}}', copyText.join('\\r\\n'), 'All-Inbounds');\n      },\n      copy(dbInboundId) {\n        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);\n        txtModal.show('{{ i18n \"pages.inbounds.inboundData\" }}', JSON.stringify(dbInbound, null, 2));\n      },\n      async startDataRefreshLoop() {\n        while (this.isRefreshEnabled) {\n          try {\n            await this.getDBInbounds();\n          } catch (e) {\n            console.error(e);\n          }\n          await PromiseUtil.sleep(this.refreshInterval);\n        }\n      },\n      toggleRefresh() {\n        localStorage.setItem(\"isRefreshEnabled\", this.isRefreshEnabled);\n        if (this.isRefreshEnabled) {\n          this.startDataRefreshLoop();\n        }\n      },\n      changeRefreshInterval() {\n        localStorage.setItem(\"refreshInterval\", this.refreshInterval);\n      },\n      async manualRefresh() {\n        if (!this.refreshing) {\n          this.loadingStates.spinning = true;\n          await this.getDBInbounds();\n          this.loadingStates.spinning = false;\n        }\n      },\n      pagination(obj) {\n        if (this.pageSize > 0 && obj.length > this.pageSize) {\n          // Set page options based on object size\n          sizeOptions = [];\n          for (i = this.pageSize; i <= obj.length; i = i + this.pageSize) {\n            sizeOptions.push(i.toString());\n          }\n          // Add option to see all in one page\n          sizeOptions.push(i.toString());\n\n          p = {\n            showSizeChanger: true,\n            size: 'small',\n            position: 'bottom',\n            pageSize: this.pageSize,\n            pageSizeOptions: sizeOptions\n          };\n          return p;\n        }\n        return false\n      }\n    },\n    watch: {\n      searchKey: Utils.debounce(function (newVal) {\n        this.searchInbounds(newVal);\n      }, 500)\n    },\n    mounted() {\n      if (window.location.protocol !== \"https:\") {\n        this.showAlert = true;\n      }\n      this.loading();\n      this.getDefaultSettings();\n      \n      // Initial data fetch\n      this.getDBInbounds().then(() => {\n        this.loading(false);\n      });\n\n      // Setup WebSocket for real-time updates\n      if (window.wsClient) {\n        window.wsClient.connect();\n        \n        // Listen for inbounds updates\n        window.wsClient.on('inbounds', (payload) => {\n          if (payload && Array.isArray(payload)) {\n            // Use setInbounds to properly convert to DBInbound objects with methods\n            this.setInbounds(payload);\n          }\n        });\n\n        // Listen for traffic updates\n        window.wsClient.on('traffic', (payload) => {\n          // Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event\n          // because clientTraffics contains delta/incremental values, not total accumulated values.\n          // Total traffic is updated via the 'inbounds' event which contains accumulated values from database.\n          \n          // Update online clients list in real-time\n          if (payload && Array.isArray(payload.onlineClients)) {\n            const nextOnlineClients = payload.onlineClients;\n            let onlineChanged = this.onlineClients.length !== nextOnlineClients.length;\n            if (!onlineChanged) {\n              const prevSet = new Set(this.onlineClients);\n              for (const email of nextOnlineClients) {\n                if (!prevSet.has(email)) {\n                  onlineChanged = true;\n                  break;\n                }\n              }\n            }\n            this.onlineClients = nextOnlineClients;\n            if (onlineChanged) {\n              // Recalculate client counts to update online status\n              this.dbInbounds.forEach(dbInbound => {\n                const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);\n                if (inbound && this.clientCount[dbInbound.id]) {\n                  this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound);\n                }\n              });\n\n              if (this.enableFilter) {\n                this.filterInbounds();\n              }\n            }\n          }\n          \n          // Update last online map in real-time\n          if (payload && payload.lastOnlineMap && typeof payload.lastOnlineMap === 'object') {\n            this.lastOnlineMap = { ...this.lastOnlineMap, ...payload.lastOnlineMap };\n          }\n        });\n\n        // Fallback to polling if WebSocket fails\n        window.wsClient.on('error', () => {\n          console.warn('WebSocket connection failed, falling back to polling');\n          if (this.isRefreshEnabled) {\n            this.startDataRefreshLoop();\n          }\n        });\n\n        window.wsClient.on('disconnected', () => {\n          if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {\n            console.warn('WebSocket reconnection failed, falling back to polling');\n            if (this.isRefreshEnabled) {\n              this.startDataRefreshLoop();\n            }\n          }\n        });\n      } else {\n        // Fallback to polling if WebSocket is not available\n        if (this.isRefreshEnabled) {\n          this.startDataRefreshLoop();\n        }\n      }\n    },\n    computed: {\n      total() {\n        let down = 0, up = 0, allTime = 0;\n        let clients = 0, deactive = [], depleted = [], expiring = [];\n        this.dbInbounds.forEach(dbInbound => {\n          down += dbInbound.down;\n          up += dbInbound.up;\n          allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));\n          if (this.clientCount[dbInbound.id]) {\n            clients += this.clientCount[dbInbound.id].clients;\n            deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);\n            depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);\n            expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);\n          }\n        });\n        return {\n          down: down,\n          up: up,\n          allTime: allTime,\n          clients: clients,\n          deactive: deactive,\n          depleted: depleted,\n          expiring: expiring,\n        };\n      }\n    },\n  });\n</script>\n{{ template \"page/body_end\" .}}"
  },
  {
    "path": "web/html/index.html",
    "content": "{{ template \"page/head_start\" .}}\n{{ template \"page/head_end\" .}}\n\n{{ template \"page/body_start\" .}}\n<a-layout id=\"app\" v-cloak :class=\"themeSwitcher.currentTheme + ' index-page'\">\n  <a-sidebar></a-sidebar>\n  <a-layout id=\"content-layout\">\n    <a-layout-content>\n      <a-spin :spinning=\"loadingStates.spinning\" :delay=\"200\" :tip=\"loadingTip\">\n        <transition name=\"list\" appear>\n          <a-alert type=\"error\" v-if=\"showAlert && loadingStates.fetched\" class=\"mb-10\"\n            message='{{ i18n \"secAlertTitle\" }}' color=\"red\" description='{{ i18n \"secAlertSsl\" }}' show-icon closable>\n          </a-alert>\n        </transition>\n        <transition name=\"list\" appear>\n          <template>\n            <a-row v-if=\"!loadingStates.fetched\">\n              <a-card class=\"card-placeholder text-center\">\n                <a-spin tip='{{ i18n \"loading\" }}'></a-spin>\n              </a-card>\n            </a-row>\n            <a-row :gutter=\"[isMobile ? 8 : 16, isMobile ? 0 : 12]\" v-else>\n              <a-col>\n                <a-card hoverable>\n                  <a-row :gutter=\"[0, isMobile ? 16 : 0]\">\n                    <a-col :sm=\"24\" :md=\"12\">\n                      <a-row>\n                        <a-col :span=\"12\" class=\"text-center\">\n                          <a-progress type=\"dashboard\" status=\"normal\" :stroke-color=\"status.cpu.color\"\n                            :percent=\"status.cpu.percent\"></a-progress>\n                          <div>\n                            <b>{{ i18n \"pages.index.cpu\" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]]\n                            <a-tooltip>\n                              <a-icon type=\"area-chart\"></a-icon>\n                              <template slot=\"title\">\n                                <div><b>{{ i18n \"pages.index.logicalProcessors\" }}:</b> [[ (status.logicalPro) ]]</div>\n                                <div><b>{{ i18n \"pages.index.frequency\" }}:</b> [[\n                                  CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>\n                              </template>\n                            </a-tooltip>\n                            <a-tooltip :overlay-class-name=\"themeSwitcher.currentTheme\">\n                              <a-button size=\"small\" shape=\"circle\" class=\"ml-8\" @click=\"openCpuHistory()\">\n                                <a-icon type=\"history\" />\n                              </a-button>\n                            </a-tooltip>\n                          </div>\n                        </a-col>\n                        <a-col :span=\"12\" class=\"text-center\">\n                          <a-progress type=\"dashboard\" status=\"normal\" :stroke-color=\"status.mem.color\"\n                            :percent=\"status.mem.percent\"></a-progress>\n                          <div>\n                            <b>{{ i18n \"pages.index.memory\"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] /\n                            [[ SizeFormatter.sizeFormat(status.mem.total) ]]\n                          </div>\n                        </a-col>\n                      </a-row>\n                    </a-col>\n                    <a-col :sm=\"24\" :md=\"12\">\n                      <a-row>\n                        <a-col :span=\"12\" class=\"text-center\">\n                          <a-progress type=\"dashboard\" status=\"normal\" :stroke-color=\"status.swap.color\"\n                            :percent=\"status.swap.percent\"></a-progress>\n                          <div>\n                            <b>{{ i18n \"pages.index.swap\" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] /\n                            [[ SizeFormatter.sizeFormat(status.swap.total) ]]\n                          </div>\n                        </a-col>\n                        <a-col :span=\"12\" class=\"text-center\">\n                          <a-progress type=\"dashboard\" status=\"normal\" :stroke-color=\"status.disk.color\"\n                            :percent=\"status.disk.percent\"></a-progress>\n                          <div>\n                            <b>{{ i18n \"pages.index.storage\"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]]\n                            / [[ SizeFormatter.sizeFormat(status.disk.total) ]]\n                          </div>\n                        </a-col>\n                      </a-row>\n                    </a-col>\n                  </a-row>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card hoverable>\n                  <template #title>\n                    <a-space direction=\"horizontal\">\n                      <span>{{ i18n \"pages.index.xrayStatus\" }}</span>\n                      <a-tag v-if=\"isMobile && status.xray.version != 'Unknown'\" color=\"green\">\n                        v[[ status.xray.version ]]\n                      </a-tag>\n                    </a-space>\n                  </template>\n                  <template #extra>\n                    <template v-if=\"status.xray.state != 'error'\">\n                      <a-badge status=\"processing\"\n                        :class=\"({ green: 'xray-running-animation', orange: 'xray-stop-animation' }[status.xray.color]) || 'xray-processing-animation'\"\n                        :text=\"status.xray.stateMsg\" :color=\"status.xray.color\" />\n                    </template>\n                    <template v-else>\n                      <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n                        <span slot=\"title\">\n                          <a-row type=\"flex\" align=\"middle\" justify=\"space-between\">\n                            <a-col>\n                              <span>{{ i18n \"pages.index.xrayErrorPopoverTitle\" }}</span>\n                            </a-col>\n                            <a-col>\n                              <a-icon type=\"bars\" class=\"cursor-pointer float-right\" @click=\"openLogs()\"></a-icon>\n                            </a-col>\n                          </a-row>\n                        </span>\n                        <template slot=\"content\">\n                          <span class=\"max-w-400\" v-for=\"line in status.xray.errorMsg.split('\\n')\">[[ line ]]</span>\n                        </template>\n                        <a-badge :text=\"status.xray.stateMsg\" :color=\"status.xray.color\"\n                          :class=\"status.xray.color === 'red' ? 'xray-error-animation' : ''\" />\n                      </a-popover>\n                    </template>\n                  </template>\n                  <template #actions>\n                    <a-space v-if=\"app.ipLimitEnable\" direction=\"horizontal\" @click=\"openXrayLogs()\" class=\"jc-center\">\n                      <a-icon type=\"bars\"></a-icon>\n                      <span v-if=\"!isMobile\">{{ i18n \"pages.index.logs\" }}</span>\n                    </a-space>\n                    <a-space direction=\"horizontal\" @click=\"stopXrayService\" class=\"jc-center\">\n                      <a-icon type=\"poweroff\"></a-icon>\n                      <span v-if=\"!isMobile\">{{ i18n \"pages.index.stopXray\" }}</span>\n                    </a-space>\n                    <a-space direction=\"horizontal\" @click=\"restartXrayService\" class=\"jc-center\">\n                      <a-icon type=\"reload\"></a-icon>\n                      <span v-if=\"!isMobile\">{{ i18n \"pages.index.restartXray\" }}</span>\n                    </a-space>\n                    <a-space direction=\"horizontal\" @click=\"openSelectV2rayVersion\" class=\"jc-center\">\n                      <a-icon type=\"tool\"></a-icon>\n                      <span v-if=\"!isMobile\">\n                        [[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n\n                        \"pages.index.xraySwitch\" }}' ]]\n                      </span>\n                    </a-space>\n                  </template>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='{{ i18n \"menu.link\" }}' hoverable>\n                  <template #actions>\n                    <a-space direction=\"horizontal\" @click=\"openLogs()\" class=\"jc-center\">\n                      <a-icon type=\"bars\"></a-icon>\n                      <span v-if=\"!isMobile\">{{ i18n \"pages.index.logs\" }}</span>\n                    </a-space>\n                    <a-space direction=\"horizontal\" @click=\"openConfig\" class=\"jc-center\">\n                      <a-icon type=\"control\"></a-icon>\n                      <span v-if=\"!isMobile\">{{ i18n \"pages.index.config\" }}</span>\n                    </a-space>\n                    <a-space direction=\"horizontal\" @click=\"openBackup\" class=\"jc-center\">\n                      <a-icon type=\"cloud-server\"></a-icon>\n                      <span v-if=\"!isMobile\">{{ i18n \"pages.index.backup\" }}</span>\n                    </a-space>\n                  </template>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='3X-UI' hoverable>\n                  <a rel=\"noopener\" href=\"https://github.com/MHSanaei/3x-ui/releases\" target=\"_blank\">\n                    <a-tag color=\"green\">\n                      <span>v{{ .cur_ver }}</span>\n                    </a-tag>\n                  </a>\n                  <a rel=\"noopener\" href=\"https://t.me/XrayUI\" target=\"_blank\">\n                    <a-tag color=\"green\">\n                      <span>@XrayUI</span>\n                    </a-tag>\n                  </a>\n                  <a rel=\"noopener\" href=\"https://github.com/MHSanaei/3x-ui/wiki\" target=\"_blank\">\n                    <a-tag color=\"purple\">\n                      <span>{{ i18n \"pages.index.documentation\" }}</span>\n                    </a-tag>\n                  </a>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='{{ i18n \"pages.index.operationHours\" }}' hoverable>\n                  <a-tag :color=\"status.xray.color\">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime)\n                    ]]</a-tag>\n                  <a-tag color=\"green\">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='{{ i18n \"pages.index.systemLoad\" }}' hoverable>\n                  <a-tag color=\"green\">\n                    <a-tooltip>\n                      [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]\n                      <template slot=\"title\">\n                        {{ i18n \"pages.index.systemLoadDesc\" }}\n                      </template>\n                    </a-tooltip>\n                  </a-tag>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='{{ i18n \"usage\"}}' hoverable>\n                  <a-tag color=\"green\"> {{ i18n \"pages.index.memory\" }}: [[\n                    SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag>\n                  <a-tag color=\"green\"> {{ i18n \"pages.index.threads\" }}: [[ status.appStats.threads ]] </a-tag>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='{{ i18n \"pages.index.overallSpeed\" }}' hoverable>\n                  <a-row :gutter=\"isMobile ? [8,8] : 0\">\n                    <a-col :span=\"12\">\n                      <a-custom-statistic title='{{ i18n \"pages.index.upload\" }}'\n                        :value=\"SizeFormatter.sizeFormat(status.netIO.up)\">\n                        <template #prefix>\n                          <a-icon type=\"arrow-up\" />\n                        </template>\n                        <template #suffix>\n                          /s\n                        </template>\n                      </a-custom-statistic>\n                    </a-col>\n                    <a-col :span=\"12\">\n                      <a-custom-statistic title='{{ i18n \"pages.index.download\" }}'\n                        :value=\"SizeFormatter.sizeFormat(status.netIO.down)\">\n                        <template #prefix>\n                          <a-icon type=\"arrow-down\" />\n                        </template>\n                        <template #suffix>\n                          /s\n                        </template>\n                      </a-custom-statistic>\n                    </a-col>\n                  </a-row>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='{{ i18n \"pages.index.totalData\" }}' hoverable>\n                  <a-row :gutter=\"isMobile ? [8,8] : 0\">\n                    <a-col :span=\"12\">\n                      <a-custom-statistic title='{{ i18n \"pages.index.sent\" }}'\n                        :value=\"SizeFormatter.sizeFormat(status.netTraffic.sent)\">\n                        <template #prefix>\n                          <a-icon type=\"cloud-upload\" />\n                        </template>\n                      </a-custom-statistic>\n                    </a-col>\n                    <a-col :span=\"12\">\n                      <a-custom-statistic title='{{ i18n \"pages.index.received\" }}'\n                        :value=\"SizeFormatter.sizeFormat(status.netTraffic.recv)\">\n                        <template #prefix>\n                          <a-icon type=\"cloud-download\" />\n                        </template>\n                      </a-custom-statistic>\n                    </a-col>\n                  </a-row>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='{{ i18n \"pages.index.ipAddresses\" }}' hoverable>\n                  <template #extra>\n                    <a-tooltip :placement=\"isMobile ? 'topRight' : 'top'\">\n                      <template #title>\n                        {{ i18n \"pages.index.toggleIpVisibility\" }}\n                      </template>\n                      <a-icon :type=\"showIp ? 'eye' : 'eye-invisible'\" class=\"fs-1rem\"\n                        @click=\"showIp = !showIp\"></a-icon>\n                    </a-tooltip>\n                  </template>\n                  <a-row :class=\"showIp ? 'ip-visible' : 'ip-hidden'\" :gutter=\"isMobile ? [8,8] : 0\">\n                    <a-col :span=\"isMobile ? 24 : 12\">\n                      <a-custom-statistic title=\"IPv4\" :value=\"status.publicIP.ipv4\">\n                        <template #prefix>\n                          <a-icon type=\"global\" />\n                        </template>\n                      </a-custom-statistic>\n                    </a-col>\n                    <a-col :span=\"isMobile ? 24 : 12\">\n                      <a-custom-statistic title=\"IPv6\" :value=\"status.publicIP.ipv6\">\n                        <template #prefix>\n                          <a-icon type=\"global\" />\n                        </template>\n                      </a-custom-statistic>\n                    </a-col>\n                  </a-row>\n                </a-card>\n              </a-col>\n              <a-col :sm=\"24\" :lg=\"12\">\n                <a-card title='{{ i18n \"pages.index.connectionCount\" }}' hoverable>\n                  <a-row :gutter=\"isMobile ? [8,8] : 0\">\n                    <a-col :span=\"12\">\n                      <a-custom-statistic title=\"TCP\" :value=\"status.tcpCount\">\n                        <template #prefix>\n                          <a-icon type=\"swap\" />\n                        </template>\n                      </a-custom-statistic>\n                    </a-col>\n                    <a-col :span=\"12\">\n                      <a-custom-statistic title=\"UDP\" :value=\"status.udpCount\">\n                        <template #prefix>\n                          <a-icon type=\"swap\" />\n                        </template>\n                      </a-custom-statistic>\n                    </a-col>\n                  </a-row>\n                </a-card>\n              </a-col>\n            </a-row>\n          </template>\n        </transition>\n      </a-spin>\n    </a-layout-content>\n  </a-layout>\n  <a-modal id=\"version-modal\" v-model=\"versionModal.visible\" title='{{ i18n \"pages.index.xraySwitch\" }}'\n    :closable=\"true\" @ok=\"() => versionModal.visible = false\" :class=\"themeSwitcher.currentTheme\" footer=\"\">\n    <a-collapse default-active-key=\"1\">\n      <a-collapse-panel key=\"1\" header='Xray'>\n        <a-alert type=\"warning\" class=\"mb-12 w-100\" message='{{ i18n \"pages.index.xraySwitchClickDesk\" }}'\n          show-icon></a-alert>\n        <a-list class=\"ant-version-list w-100\" bordered>\n          <a-list-item class=\"ant-version-list-item\" v-for=\"version, index in versionModal.versions\">\n            <a-tag :color=\"index % 2 == 0 ? 'purple' : 'green'\">[[ version ]]</a-tag>\n            <a-radio :class=\"themeSwitcher.currentTheme\" :checked=\"version === `v${status.xray.version}`\"\n              @click=\"switchV2rayVersion(version)\"></a-radio>\n          </a-list-item>\n        </a-list>\n      </a-collapse-panel>\n      <a-collapse-panel key=\"2\" header='Geofiles'>\n        <a-list class=\"ant-version-list w-100\" bordered>\n          <a-list-item class=\"ant-version-list-item\"\n            v-for=\"file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']\">\n            <a-tag :color=\"index % 2 == 0 ? 'purple' : 'green'\">[[ file ]]</a-tag>\n            <a-icon type=\"reload\" @click=\"updateGeofile(file)\" class=\"mr-8\" />\n          </a-list-item>\n        </a-list>\n        <div class=\"mt-5 d-flex justify-end\"><a-button @click=\"updateGeofile('')\">{{ i18n\n            \"pages.index.geofilesUpdateAll\" }}</a-button></div>\n      </a-collapse-panel>\n    </a-collapse>\n  </a-modal>\n  <a-modal id=\"log-modal\" v-model=\"logModal.visible\" :closable=\"true\" @cancel=\"() => logModal.visible = false\"\n    :class=\"themeSwitcher.currentTheme\" width=\"800px\" footer=\"\">\n    <template slot=\"title\">\n      {{ i18n \"pages.index.logs\" }}\n      <a-icon :spin=\"logModal.loading\" type=\"sync\" class=\"va-middle ml-10\" :disabled=\"logModal.loading\"\n        @click=\"openLogs()\">\n      </a-icon>\n    </template>\n    <a-form layout=\"inline\">\n      <a-form-item class=\"mr-05\">\n        <a-input-group compact>\n          <a-select size=\"small\" v-model=\"logModal.rows\" :style=\"{ width: '70px' }\" @change=\"openLogs()\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"10\">10</a-select-option>\n            <a-select-option value=\"20\">20</a-select-option>\n            <a-select-option value=\"50\">50</a-select-option>\n            <a-select-option value=\"100\">100</a-select-option>\n            <a-select-option value=\"500\">500</a-select-option>\n          </a-select>\n          <a-select size=\"small\" v-model=\"logModal.level\" :style=\"{ width: '95px' }\" @change=\"openLogs()\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"debug\">Debug</a-select-option>\n            <a-select-option value=\"info\">Info</a-select-option>\n            <a-select-option value=\"notice\">Notice</a-select-option>\n            <a-select-option value=\"warning\">Warning</a-select-option>\n            <a-select-option value=\"err\">Error</a-select-option>\n          </a-select>\n        </a-input-group>\n      </a-form-item>\n      <a-form-item>\n        <a-checkbox v-model=\"logModal.syslog\" @change=\"openLogs()\">SysLog</a-checkbox>\n      </a-form-item>\n      <a-form-item style=\"float: right;\">\n        <a-button type=\"primary\" icon=\"download\" @click=\"FileManager.downloadTextFile(logModal.logs?.join('\\n'), 'x-ui.log')\"></a-button>\n      </a-form-item>\n    </a-form>\n    <div class=\"ant-input log-container\" v-html=\"logModal.formattedLogs\"></div>\n  </a-modal>\n  <a-modal id=\"xraylog-modal\" v-model=\"xraylogModal.visible\" :closable=\"true\"\n    @cancel=\"() => xraylogModal.visible = false\" :class=\"themeSwitcher.currentTheme\" width=\"80vw\" footer=\"\">\n    <template slot=\"title\">\n      {{ i18n \"pages.index.logs\" }}\n      <a-icon :spin=\"xraylogModal.loading\" type=\"sync\" class=\"va-middle ml-10\" :disabled=\"xraylogModal.loading\"\n        @click=\"openXrayLogs()\">\n      </a-icon>\n    </template>\n    <a-form layout=\"inline\">\n      <a-form-item class=\"mr-05\">\n        <a-input-group compact>\n          <a-select size=\"small\" v-model=\"xraylogModal.rows\" :style=\"{ width: '70px' }\" @change=\"openXrayLogs()\"\n            :dropdown-class-name=\"themeSwitcher.currentTheme\">\n            <a-select-option value=\"10\">10</a-select-option>\n            <a-select-option value=\"20\">20</a-select-option>\n            <a-select-option value=\"50\">50</a-select-option>\n            <a-select-option value=\"100\">100</a-select-option>\n            <a-select-option value=\"500\">500</a-select-option>\n          </a-select>\n        </a-input-group>\n      </a-form-item>\n      <a-form-item label=\"Filter:\">\n        <a-input size=\"small\" v-model=\"xraylogModal.filter\" @keyup.enter=\"openXrayLogs()\"></a-input>\n      </a-form-item>\n      <a-form-item>\n        <a-checkbox v-model=\"xraylogModal.showDirect\" @change=\"openXrayLogs()\">Direct</a-checkbox>\n        <a-checkbox v-model=\"xraylogModal.showBlocked\" @change=\"openXrayLogs()\">Blocked</a-checkbox>\n        <a-checkbox v-model=\"xraylogModal.showProxy\" @change=\"openXrayLogs()\">Proxy</a-checkbox>\n      </a-form-item>\n      <a-form-item style=\"float: right;\">\n        <a-button type=\"primary\" icon=\"download\" @click=\"downloadXrayLogs\"></a-button>\n      </a-form-item>\n    </a-form>\n    <div class=\"ant-input log-container\" v-html=\"xraylogModal.formattedLogs\"></div>\n  </a-modal>\n  <a-modal id=\"backup-modal\" v-model=\"backupModal.visible\" title='{{ i18n \"pages.index.backupTitle\" }}' :closable=\"true\"\n    footer=\"\" :class=\"themeSwitcher.currentTheme\">\n    <a-list class=\"ant-backup-list w-100\" bordered>\n      <a-list-item class=\"ant-backup-list-item\">\n        <a-list-item-meta>\n          <template #title>{{ i18n \"pages.index.exportDatabase\" }}</template>\n          <template #description>{{ i18n \"pages.index.exportDatabaseDesc\" }}</template>\n        </a-list-item-meta>\n        <a-button @click=\"exportDatabase()\" type=\"primary\" icon=\"download\" />\n      </a-list-item>\n      <a-list-item class=\"ant-backup-list-item\">\n        <a-list-item-meta>\n          <template #title>{{ i18n \"pages.index.importDatabase\" }}</template>\n          <template #description>{{ i18n \"pages.index.importDatabaseDesc\" }}</template>\n        </a-list-item-meta>\n        <a-button @click=\"importDatabase()\" type=\"primary\" icon=\"upload\" />\n      </a-list-item>\n    </a-list>\n  </a-modal>\n  <!-- CPU History Modal -->\n  <a-modal id=\"cpu-history-modal\" v-model=\"cpuHistoryModal.visible\" :closable=\"true\"\n    @cancel=\"() => cpuHistoryModal.visible = false\" :class=\"themeSwitcher.currentTheme\" width=\"900px\" footer=\"\">\n    <template slot=\"title\">\n      CPU History\n      <a-select size=\"small\" v-model=\"cpuHistoryModal.bucket\" class=\"ml-10\" style=\"width: 80px\"\n        @change=\"fetchCpuHistoryBucket\">\n        <a-select-option :value=\"2\">2m</a-select-option>\n        <a-select-option :value=\"30\">30m</a-select-option>\n        <a-select-option :value=\"60\">1h</a-select-option>\n        <a-select-option :value=\"120\">2h</a-select-option>\n        <a-select-option :value=\"180\">3h</a-select-option>\n        <a-select-option :value=\"300\">5h</a-select-option>\n      </a-select>\n    </template>\n    <div style=\"padding:16px\">\n      <sparkline :data=\"cpuHistoryLong\" :labels=\"cpuHistoryLabels\" :vb-width=\"840\" :height=\"220\"\n        :stroke=\"status.cpu.color\" :stroke-width=\"2.2\" :show-grid=\"true\" :show-axes=\"true\" :tick-count-x=\"5\"\n        :max-points=\"cpuHistoryLong.length\" :fill-opacity=\"0.18\" :marker-radius=\"3.2\" :show-tooltip=\"true\" />\n      <div style=\"margin-top:4px;font-size:11px;opacity:0.65\">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point (total [[ cpuHistoryLong.length ]] points)</div>\n    </div>\n  </a-modal>\n</a-layout>\n{{template \"page/body_scripts\" .}}\n{{template \"component/aSidebar\" .}}\n{{template \"component/aThemeSwitch\" .}}\n{{template \"component/aCustomStatistic\" .}}\n{{template \"modals/textModal\"}}\n<script>\n  // Tiny Sparkline component using an inline SVG polyline\n  Vue.component('sparkline', {\n    props: {\n      data: { type: Array, required: true },\n      // viewBox width for drawing space; SVG width will be 100% of container\n      vbWidth: { type: Number, default: 320 },\n      height: { type: Number, default: 80 },\n      stroke: { type: String, default: '#008771' },\n      strokeWidth: { type: Number, default: 2 },\n      maxPoints: { type: Number, default: 120 },\n      showGrid: { type: Boolean, default: true },\n      gridColor: { type: String, default: 'rgba(0,0,0,0.1)' },\n      fillOpacity: { type: Number, default: 0.15 },\n      showMarker: { type: Boolean, default: true },\n      markerRadius: { type: Number, default: 2.8 },\n      // New opts for axes/labels/tooltip\n      labels: { type: Array, default: () => [] }, // same length as data for x labels (e.g., timestamps)\n      showAxes: { type: Boolean, default: false },\n      yTickStep: { type: Number, default: 25 }, // percent ticks\n      tickCountX: { type: Number, default: 4 },\n      paddingLeft: { type: Number, default: 32 },\n      paddingRight: { type: Number, default: 6 },\n      paddingTop: { type: Number, default: 6 },\n      paddingBottom: { type: Number, default: 20 },\n      showTooltip: { type: Boolean, default: false },\n    },\n    data() {\n      return {\n        hoverIdx: -1,\n      }\n    },\n    computed: {\n      viewBoxAttr() {\n        return '0 0 ' + this.vbWidth + ' ' + this.height\n      },\n      drawWidth() {\n        return Math.max(1, this.vbWidth - this.paddingLeft - this.paddingRight)\n      },\n      drawHeight() {\n        return Math.max(1, this.height - this.paddingTop - this.paddingBottom)\n      },\n      nPoints() {\n        return Math.min(this.data.length, this.maxPoints)\n      },\n      dataSlice() {\n        const n = this.nPoints\n        if (n === 0) return []\n        return this.data.slice(this.data.length - n)\n      },\n      labelsSlice() {\n        const n = this.nPoints\n        if (!this.labels || this.labels.length === 0 || n === 0) return []\n        const start = Math.max(0, this.labels.length - n)\n        return this.labels.slice(start)\n      },\n      pointsArr() {\n        const n = this.nPoints\n        if (n === 0) return []\n        const slice = this.dataSlice\n        const max = 100\n        const w = this.drawWidth\n        const h = this.drawHeight\n        const dx = n > 1 ? w / (n - 1) : 0\n        return slice.map((v, i) => {\n          const x = Math.round(this.paddingLeft + i * dx)\n          const y = Math.round(this.paddingTop + (h - (Math.max(0, Math.min(100, v)) / max) * h))\n          return [x, y]\n        })\n      },\n      points() {\n        return this.pointsArr.map(p => `${p[0]},${p[1]}`).join(' ')\n      },\n      areaPath() {\n        if (this.pointsArr.length === 0) return ''\n        const first = this.pointsArr[0]\n        const last = this.pointsArr[this.pointsArr.length - 1]\n        const line = this.points\n        // Close to bottom to create an area fill\n        return `M ${first[0]},${this.paddingTop + this.drawHeight} L ${line.replace(/ /g, ' L ')} L ${last[0]},${this.paddingTop + this.drawHeight} Z`\n      },\n      gridLines() {\n        if (!this.showGrid) return []\n        const h = this.drawHeight\n        const w = this.drawWidth\n        // draw at 25%, 50%, 75%\n        return [0, 0.25, 0.5, 0.75, 1]\n          .map(r => Math.round(this.paddingTop + h * r))\n          .map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))\n      },\n      lastPoint() {\n        if (this.pointsArr.length === 0) return null\n        return this.pointsArr[this.pointsArr.length - 1]\n      },\n      yTicks() {\n        if (!this.showAxes) return []\n        const step = Math.max(1, this.yTickStep)\n        const ticks = []\n        for (let p = 0; p <= 100; p += step) {\n          const y = Math.round(this.paddingTop + (this.drawHeight - (p / 100) * this.drawHeight))\n          ticks.push({ y, label: `${p}%` })\n        }\n        return ticks\n      },\n      xTicks() {\n        if (!this.showAxes) return []\n        const labels = this.labelsSlice\n        const n = this.nPoints\n        const m = Math.max(2, this.tickCountX)\n        const ticks = []\n        if (n === 0) return ticks\n        const w = this.drawWidth\n        const dx = n > 1 ? w / (n - 1) : 0\n        const positions = []\n        for (let i = 0; i < m; i++) {\n          const idx = Math.round((i * (n - 1)) / (m - 1))\n          positions.push(idx)\n        }\n        positions.forEach(idx => {\n          const label = labels[idx] != null ? String(labels[idx]) : String(idx)\n          const x = Math.round(this.paddingLeft + idx * dx)\n          ticks.push({ x, label })\n        })\n        return ticks\n      },\n    },\n    methods: {\n      onMouseMove(evt) {\n        if (!this.showTooltip || this.pointsArr.length === 0) return\n        const rect = evt.currentTarget.getBoundingClientRect()\n        const px = evt.clientX - rect.left\n        // translate to viewBox space\n        const x = (px / rect.width) * this.vbWidth\n        const n = this.nPoints\n        const dx = n > 1 ? this.drawWidth / (n - 1) : 0\n        const idx = Math.max(0, Math.min(n - 1, Math.round((x - this.paddingLeft) / (dx || 1))))\n        this.hoverIdx = idx\n      },\n      onMouseLeave() {\n        this.hoverIdx = -1\n      },\n      fmtHoverText() {\n        const labels = this.labelsSlice\n        const idx = this.hoverIdx\n        if (idx < 0 || idx >= this.dataSlice.length) return ''\n        const raw = Math.max(0, Math.min(100, Number(this.dataSlice[idx] || 0)))\n        const val = Number.isFinite(raw) ? raw.toFixed(2) : raw\n        const lab = labels[idx] != null ? labels[idx] : ''\n        return `${val}%${lab ? ' • ' + lab : ''}`\n      },\n    },\n    template: `\n      <svg width=\"100%\" :height=\"height\" :viewBox=\"viewBoxAttr\" preserveAspectRatio=\"none\" class=\"idx-cpu-history-svg\"\n           @mousemove=\"onMouseMove\" @mouseleave=\"onMouseLeave\">\n        <defs>\n          <linearGradient id=\"spkGrad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop offset=\"0%\" :stop-color=\"stroke\" :stop-opacity=\"fillOpacity\"/>\n            <stop offset=\"100%\" :stop-color=\"stroke\" stop-opacity=\"0\"/>\n          </linearGradient>\n        </defs>\n        <g v-if=\"showGrid\">\n          <line v-for=\"(g,i) in gridLines\" :key=\"i\" :x1=\"g.x1\" :y1=\"g.y1\" :x2=\"g.x2\" :y2=\"g.y2\" :stroke=\"gridColor\" stroke-width=\"1\" class=\"cpu-grid-line\" />\n        </g>\n        <g v-if=\"showAxes\">\n          <!-- Y ticks/labels -->\n          <g v-for=\"(t,i) in yTicks\" :key=\"'y'+i\">\n            <text class=\"cpu-grid-y-text\" :x=\"Math.max(0, paddingLeft - 4)\" :y=\"t.y + 4\" text-anchor=\"end\" font-size=\"10\" fill=\"rgba(0,0,0,0.3)\" v-text=\"t.label\"></text>\n          </g>\n          <!-- X ticks/labels -->\n          <g v-for=\"(t,i) in xTicks\" :key=\"'x'+i\">\n            <text class=\"cpu-grid-x-text\" :x=\"t.x\" :y=\"paddingTop + drawHeight + 22\" text-anchor=\"middle\" font-size=\"10\" fill=\"rgba(0,0,0,0.3)\" v-text=\"t.label\"></text>\n          </g>\n        </g>\n        <path v-if=\"areaPath\" :d=\"areaPath\" fill=\"url(#spkGrad)\" stroke=\"none\" />\n        <polyline :points=\"points\" fill=\"none\" :stroke=\"stroke\" :stroke-width=\"strokeWidth\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n        <circle v-if=\"showMarker && lastPoint\" :cx=\"lastPoint[0]\" :cy=\"lastPoint[1]\" :r=\"markerRadius\" :fill=\"stroke\" />\n        <!-- Hover marker/tooltip -->\n        <g v-if=\"showTooltip && hoverIdx >= 0\">\n          <line class=\"cpu-grid-h-line\" :x1=\"pointsArr[hoverIdx][0]\" :x2=\"pointsArr[hoverIdx][0]\" :y1=\"paddingTop\" :y2=\"paddingTop + drawHeight\" stroke=\"rgba(0,0,0,0.2)\" stroke-width=\"1\" />\n          <circle :cx=\"pointsArr[hoverIdx][0]\" :cy=\"pointsArr[hoverIdx][1]\" r=\"3.5\" :fill=\"stroke\" />\n          <text class=\"cpu-grid-text\" :x=\"pointsArr[hoverIdx][0]\" :y=\"paddingTop + 12\" text-anchor=\"middle\" font-size=\"11\" fill=\"rgba(0,0,0,0.8)\" v-text=\"fmtHoverText()\"></text>\n        </g>\n      </svg>\n    `,\n  })\n\n\n  class CurTotal {\n\n    constructor(current, total) {\n      this.current = current;\n      this.total = total;\n    }\n\n    get percent() {\n      if (this.total === 0) {\n        return 0;\n      }\n      return NumberFormatter.toFixed(this.current / this.total * 100, 2);\n    }\n\n    get color() {\n      const percent = this.percent;\n      if (percent < 80) {\n        return '#008771'; // Green\n      } else if (percent < 90) {\n        return \"#f37b24\"; // Orange\n      } else {\n        return \"#cf3c3c\"; // Red\n      }\n    }\n  }\n\n  class Status {\n    constructor(data) {\n      this.cpu = new CurTotal(0, 0);\n      this.cpuCores = 0;\n      this.logicalPro = 0;\n      this.cpuSpeedMhz = 0;\n      this.disk = new CurTotal(0, 0);\n      this.loads = [0, 0, 0];\n      this.mem = new CurTotal(0, 0);\n      this.netIO = { up: 0, down: 0 };\n      this.netTraffic = { sent: 0, recv: 0 };\n      this.publicIP = { ipv4: 0, ipv6: 0 };\n      this.swap = new CurTotal(0, 0);\n      this.tcpCount = 0;\n      this.udpCount = 0;\n      this.uptime = 0;\n      this.appUptime = 0;\n      this.appStats = { threads: 0, mem: 0, uptime: 0 };\n\n      this.xray = { state: 'stop', stateMsg: \"\", errorMsg: \"\", version: \"\", color: \"\" };\n\n      if (data == null) {\n        return;\n      }\n\n      this.cpu = new CurTotal(data.cpu, 100);\n      this.cpuCores = data.cpuCores;\n      this.logicalPro = data.logicalPro;\n      this.cpuSpeedMhz = data.cpuSpeedMhz;\n      this.disk = new CurTotal(data.disk.current, data.disk.total);\n      this.loads = data.loads.map(load => NumberFormatter.toFixed(load, 2));\n      this.mem = new CurTotal(data.mem.current, data.mem.total);\n      this.netIO = data.netIO;\n      this.netTraffic = data.netTraffic;\n      this.publicIP = data.publicIP;\n      this.swap = new CurTotal(data.swap.current, data.swap.total);\n      this.tcpCount = data.tcpCount;\n      this.udpCount = data.udpCount;\n      this.uptime = data.uptime;\n      this.appUptime = data.appUptime;\n      this.appStats = data.appStats;\n      this.xray = data.xray;\n      switch (this.xray.state) {\n        case 'running':\n          this.xray.color = \"green\";\n          this.xray.stateMsg = '{{ i18n \"pages.index.xrayStatusRunning\" }}';\n          break;\n        case 'stop':\n          this.xray.color = \"orange\";\n          this.xray.stateMsg = '{{ i18n \"pages.index.xrayStatusStop\" }}';\n          break;\n        case 'error':\n          this.xray.color = \"red\";\n          this.xray.stateMsg = '{{ i18n \"pages.index.xrayStatusError\" }}';\n          break;\n        default:\n          this.xray.color = \"gray\";\n          this.xray.stateMsg = '{{ i18n \"pages.index.xrayStatusUnknown\" }}';\n          break;\n      }\n    }\n  }\n\n  const versionModal = {\n    visible: false,\n    versions: [],\n    show(versions) {\n      this.visible = true;\n      this.versions = versions;\n    },\n    hide() {\n      this.visible = false;\n    },\n  };\n\n  const logModal = {\n    visible: false,\n    logs: [],\n    rows: 20,\n    level: 'info',\n    syslog: false,\n    loading: false,\n    show(logs) {\n      this.visible = true;\n      this.logs = logs;\n      this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : \"No Record...\";\n    },\n    formatLogs(logs) {\n      let formattedLogs = '';\n      const levels = [\"DEBUG\", \"INFO\", \"NOTICE\", \"WARNING\", \"ERROR\"];\n      const levelColors = [\"#3c89e8\", \"#008771\", \"#008771\", \"#f37b24\", \"#e04141\", \"#bcbcbc\"];\n\n      logs.forEach((log, index) => {\n        let [data, message] = log.split(\" - \", 2);\n        const parts = data.split(\" \")\n        if (index > 0) formattedLogs += '<br>';\n\n        if (parts.length === 3) {\n          const d = parts[0];\n          const t = parts[1];\n          const level = parts[2];\n          const levelIndex = levels.indexOf(level, levels) || 5;\n\n          //formattedLogs += `<span style=\"color: gray;\">${index + 1}.</span>`;\n          formattedLogs += `<span style=\"color: ${levelColors[0]};\">${d} ${t}</span> `;\n          formattedLogs += `<span style=\"color: ${levelColors[levelIndex]}\">${level}</span>`;\n        } else {\n          const levelIndex = levels.indexOf(data, levels) || 5;\n          formattedLogs += `<span style=\"color: ${levelColors[levelIndex]}\">${data}</span>`;\n        }\n\n        if (message) {\n          if (message.startsWith(\"XRAY:\"))\n            message = \"<b>XRAY: </b>\" + message.substring(5);\n          else\n            message = \"<b>X-UI: </b>\" + message;\n        }\n\n        formattedLogs += message ? ' - ' + message : '';\n      });\n\n      return formattedLogs;\n    },\n    hide() {\n      this.visible = false;\n    },\n  };\n\n  const xraylogModal = {\n      visible: false,\n      logs: [],\n      rows: 20,\n      showDirect: true,\n      showBlocked: true,\n      showProxy: true,\n      loading: false,\n      show(logs) {\n        this.visible = true;\n        this.logs = logs;\n        this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : \"No Record...\";\n      },\n      formatLogs(logs) {\n        let formattedLogs = `\n<style>\n  table {\n    border-collapse: collapse;\n    width: auto;\n  }\n\n  table td, table th {\n    padding: 2px 15px;\n  }\n</style>\n\n<table>\n    <tr>\n        <th>Date</th>\n        <th>From</th>\n        <th>To</th>\n        <th>Inbound</th>\n        <th>Outbound</th>\n        <th>Email</th>\n    </tr>\n`;\n\n        logs.reverse().forEach((log, index) => {\n          let outboundColor = '';\n          if (log.Event === 1) {\n            outboundColor = ' style=\"color: #e04141;\"'; //red for blocked\n          }\n          else if (log.Event === 2) {\n            outboundColor = ' style=\"color: #3c89e8;\"'; //blue for proxies\n          }\n\n          let text = ``;\n          if (log.Email !== \"\") {\n            text = `<td>${log.Email}</td>`;\n          }\n\n          formattedLogs += `\n<tr ${outboundColor}>\n    <td><b>${IntlUtil.formatDate(log.DateTime)}</b></td>\n    <td>${log.FromAddress}</td>\n    <td>${log.ToAddress}</td>\n    <td>${log.Inbound}</td>\n    <td>${log.Outbound}</td>\n    ${text}\n</tr>\n`;\n        });\n\n        return formattedLogs += \"</table>\";\n      },\n      hide() {\n        this.visible = false;\n      },\n    };\n  const backupModal = {\n    visible: false,\n    show() {\n      this.visible = true;\n    },\n    hide() {\n      this.visible = false;\n    },\n  };\n\n  const app = new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#app',\n    mixins: [MediaQueryMixin],\n    data: {\n      themeSwitcher,\n      loadingStates: {\n        fetched: false,\n        spinning: false\n      },\n      status: new Status(),\n  cpuHistory: [], // small live widget history\n  cpuHistoryLong: [], // aggregated points from backend\n  cpuHistoryLabels: [],\n  cpuHistoryModal: { visible: false, bucket: 2 },\n      versionModal,\n      logModal,\n      xraylogModal,\n      backupModal,\n      loadingTip: '{{ i18n \"loading\"}}',\n      showAlert: false,\n      showIp: false,\n      ipLimitEnable: false,\n    },\n    methods: {\n      loading(spinning, tip = '{{ i18n \"loading\"}}') {\n        this.loadingStates.spinning = spinning;\n        this.loadingTip = tip;\n      },\n      async getStatus() {\n        try {\n          const msg = await HttpUtil.get('/panel/api/server/status');\n          if (msg.success) {\n            if (!this.loadingStates.fetched) {\n              this.loadingStates.fetched = true;\n            }\n\n            this.setStatus(msg.obj, true);\n          }\n        } catch (e) {\n          console.error(\"Failed to get status:\", e);\n        }\n      },\n      setStatus(data) {\n        this.status = new Status(data);\n        // Push CPU percent into history (clamped 0..100)\n        const v = Math.max(0, Math.min(100, Number(data?.cpu ?? 0)))\n        this.cpuHistory.push(v)\n        const maxPoints = this.isMobile ? 60 : 120\n        if (this.cpuHistory.length > maxPoints) {\n          this.cpuHistory.splice(0, this.cpuHistory.length - maxPoints)\n        }\n        // If modal open, refresh current bucketed data\n        if (this.cpuHistoryModal.visible) {\n          this.fetchCpuHistoryBucket()\n        }\n      },\n      openCpuHistory() {\n        this.cpuHistoryModal.visible = true\n        this.fetchCpuHistoryBucket()\n      },\n      async fetchCpuHistoryBucket() {\n        const bucket = this.cpuHistoryModal.bucket || 2\n        try {\n          const msg = await HttpUtil.get(`/panel/api/server/cpuHistory/${bucket}`)\n          if (msg.success && Array.isArray(msg.obj)) {\n            const vals = []\n            const labels = []\n            for (const p of msg.obj) {\n              const d = new Date(p.t * 1000)\n              const hh = String(d.getHours()).padStart(2,'0')\n              const mm = String(d.getMinutes()).padStart(2,'0')\n              const ss = String(d.getSeconds()).padStart(2,'0')\n              labels.push(bucket>=60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)\n              vals.push(Math.max(0, Math.min(100, p.cpu)))\n            }\n            this.cpuHistoryLabels = labels\n            this.cpuHistoryLong = vals\n          }\n        } catch(e) {\n          console.error('Failed to fetch bucketed cpu history', e)\n        }\n      },\n      async openSelectV2rayVersion() {\n        this.loading(true);\n        const msg = await HttpUtil.get('/panel/api/server/getXrayVersion');\n        this.loading(false);\n        if (!msg.success) {\n          return;\n        }\n        versionModal.show(msg.obj);\n      },\n      switchV2rayVersion(version) {\n        this.$confirm({\n          title: '{{ i18n \"pages.index.xraySwitchVersionDialog\"}}',\n          content: '{{ i18n \"pages.index.xraySwitchVersionDialogDesc\"}}'.replace('#version#', version),\n          okText: '{{ i18n \"confirm\"}}',\n          class: themeSwitcher.currentTheme,\n          cancelText: '{{ i18n \"cancel\"}}',\n          onOk: async () => {\n            versionModal.hide();\n            this.loading(true, '{{ i18n \"pages.index.dontRefresh\"}}');\n            await HttpUtil.post(`/panel/api/server/installXray/${version}`);\n            this.loading(false);\n          },\n        });\n      },\n      updateGeofile(fileName) {\n        const isSingleFile = !!fileName;\n        this.$confirm({\n          title: '{{ i18n \"pages.index.geofileUpdateDialog\" }}',\n          content: isSingleFile\n            ? '{{ i18n \"pages.index.geofileUpdateDialogDesc\" }}'.replace(\"#filename#\", fileName)\n            : '{{ i18n \"pages.index.geofilesUpdateDialogDesc\" }}',\n          okText: '{{ i18n \"confirm\"}}',\n          class: themeSwitcher.currentTheme,\n          cancelText: '{{ i18n \"cancel\"}}',\n          onOk: async () => {\n            versionModal.hide();\n            this.loading(true, '{{ i18n \"pages.index.dontRefresh\"}}');\n            const url = isSingleFile\n              ? `/panel/api/server/updateGeofile/${fileName}`\n              : `/panel/api/server/updateGeofile`;\n            await HttpUtil.post(url);\n            this.loading(false);\n          },\n        });\n      },\n      async stopXrayService() {\n        this.loading(true);\n        const msg = await HttpUtil.post('/panel/api/server/stopXrayService');\n        this.loading(false);\n        if (!msg.success) {\n          return;\n        }\n      },\n      async restartXrayService() {\n        this.loading(true);\n        const msg = await HttpUtil.post('/panel/api/server/restartXrayService');\n        this.loading(false);\n        if (!msg.success) {\n          return;\n        }\n      },\n      async openLogs() {\n        logModal.loading = true;\n        const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, { level: logModal.level, syslog: logModal.syslog });\n        if (!msg.success) {\n          return;\n        }\n        logModal.show(msg.obj);\n        await PromiseUtil.sleep(500);\n        logModal.loading = false;\n      },\n      async openXrayLogs() {\n        xraylogModal.loading = true;\n        const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, { filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy });\n        if (!msg.success) {\n          return;\n        }\n        xraylogModal.show(msg.obj);\n        await PromiseUtil.sleep(500);\n        xraylogModal.loading = false;\n      },\n      downloadXrayLogs() {\n        if (!Array.isArray(this.xraylogModal.logs) || this.xraylogModal.logs.length === 0) {\n          FileManager.downloadTextFile('', 'x-ui.log');\n          return;\n        }\n        const lines = this.xraylogModal.logs.map(l => {\n          try {\n            const dt = l.DateTime ? new Date(l.DateTime) : null;\n            const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';\n            const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };\n            const eventText = eventMap[l.Event] || String(l.Event ?? '');\n            const emailPart = l.Email ? ` Email=${l.Email}` : '';\n            return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();\n          } catch (e) {\n            return JSON.stringify(l);\n          }\n        }).join('\\n');\n        FileManager.downloadTextFile(lines, 'x-ui.log');\n      },\n      async openConfig() {\n        this.loading(true);\n        const msg = await HttpUtil.get('/panel/api/server/getConfigJson');\n        this.loading(false);\n        if (!msg.success) {\n          return;\n        }\n        txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');\n      },\n      openBackup() {\n        backupModal.show();\n      },\n      exportDatabase() {\n        window.location = basePath + 'panel/api/server/getDb';\n      },\n      importDatabase() {\n        const fileInput = document.createElement('input');\n        fileInput.type = 'file';\n        fileInput.accept = '.db';\n        fileInput.addEventListener('change', async (event) => {\n          const dbFile = event.target.files[0];\n          if (dbFile) {\n            const formData = new FormData();\n            formData.append('db', dbFile);\n            backupModal.hide();\n            this.loading(true);\n            const uploadMsg = await HttpUtil.post('/panel/api/server/importDB', formData, {\n              headers: {\n                'Content-Type': 'multipart/form-data',\n              }\n            });\n            this.loading(false);\n            if (!uploadMsg.success) {\n              return;\n            }\n            this.loading(true);\n            const restartMsg = await HttpUtil.post(\"/panel/setting/restartPanel\");\n            this.loading(false);\n            if (restartMsg.success) {\n              this.loading(true);\n              await PromiseUtil.sleep(5000);\n              location.reload();\n            }\n          }\n        });\n        fileInput.click();\n      },\n      startPolling() {\n        // Fallback polling mechanism\n        const pollInterval = setInterval(async () => {\n          if (window.wsClient && window.wsClient.isConnected) {\n            clearInterval(pollInterval);\n            return;\n          }\n          try {\n            await this.getStatus();\n          } catch (e) {\n            console.error(e);\n          }\n        }, 2000);\n      },\n    },\n    async mounted() {\n      if (window.location.protocol !== \"https:\") {\n        this.showAlert = true;\n      }\n\n      const msg = await HttpUtil.post('/panel/setting/defaultSettings');\n      if (msg.success) {\n        this.ipLimitEnable = msg.obj.ipLimitEnable;\n      }\n\n      // Initial status fetch\n      await this.getStatus();\n\n      // Setup WebSocket for real-time updates\n      if (window.wsClient) {\n        window.wsClient.connect();\n        \n        // Listen for status updates\n        window.wsClient.on('status', (payload) => {\n          this.setStatus(payload);\n        });\n\n        // Listen for Xray state changes\n        window.wsClient.on('xray_state', (payload) => {\n          if (this.status && this.status.xray) {\n            this.status.xray.state = payload.state;\n            this.status.xray.errorMsg = payload.errorMsg || '';\n            switch (payload.state) {\n              case 'running':\n                this.status.xray.color = \"green\";\n                this.status.xray.stateMsg = '{{ i18n \"pages.index.xrayStatusRunning\" }}';\n                break;\n              case 'stop':\n                this.status.xray.color = \"orange\";\n                this.status.xray.stateMsg = '{{ i18n \"pages.index.xrayStatusStop\" }}';\n                break;\n              case 'error':\n                this.status.xray.color = \"red\";\n                this.status.xray.stateMsg = '{{ i18n \"pages.index.xrayStatusError\" }}';\n                break;\n            }\n          }\n        });\n\n        // Notifications disabled - white notifications are not needed\n\n        // Fallback to polling if WebSocket fails\n        window.wsClient.on('error', () => {\n          console.warn('WebSocket connection failed, falling back to polling');\n          this.startPolling();\n        });\n\n        window.wsClient.on('disconnected', () => {\n          if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {\n            console.warn('WebSocket reconnection failed, falling back to polling');\n            this.startPolling();\n          }\n        });\n      } else {\n        // Fallback to polling if WebSocket is not available\n        this.startPolling();\n      }\n    },\n  });\n</script>\n{{ template \"page/body_end\" .}}\n"
  },
  {
    "path": "web/html/login.html",
    "content": "{{ template \"page/head_start\" .}}\n{{ template \"page/head_end\" .}}\n\n{{ template \"page/body_start\" .}}\n<a-layout id=\"app\" v-cloak :class=\"themeSwitcher.currentTheme + ' login-app'\">\n  <transition name=\"list\" appear>\n    <a-layout-content class=\"under min-h-0\">\n      <div class=\"waves-header\">\n        <div class=\"waves-inner-header\"></div>\n        <svg class=\"waves\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n          viewBox=\"0 24 150 28\" preserveAspectRatio=\"none\" shape-rendering=\"auto\">\n          <defs>\n            <path id=\"gentle-wave\" d=\"M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z\" />\n          </defs>\n          <g class=\"parallax\">\n            <use xlink:href=\"#gentle-wave\" x=\"48\" y=\"0\" fill=\"rgba(0, 135, 113, 0.08)\" />\n            <use xlink:href=\"#gentle-wave\" x=\"48\" y=\"3\" fill=\"rgba(0, 135, 113, 0.08)\" />\n            <use xlink:href=\"#gentle-wave\" x=\"48\" y=\"5\" fill=\"rgba(0, 135, 113, 0.08)\" />\n            <use xlink:href=\"#gentle-wave\" x=\"48\" y=\"7\" fill=\"#c7ebe2\" />\n          </g>\n        </svg>\n      </div>\n      <a-row type=\"flex\" justify=\"center\" align=\"middle\" class=\"h-100 overflow-y-auto overflow-x-hidden\">\n        <a-col :xs=\"22\" :sm=\"12\" :md=\"10\" :lg=\"8\" :xl=\"6\" :xxl=\"5\" id=\"login\" class=\"my-3rem\">\n          <template v-if=\"!loadingStates.fetched\">\n            <div class=\"text-center\">\n              <a-spin size=\"large\" />\n            </div>\n          </template>\n          <template v-else>\n            <div class=\"setting-section\">\n              <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\" title='{{ i18n \"menu.settings\" }}'\n                placement=\"bottomRight\" trigger=\"click\">\n                <template slot=\"content\">\n                  <a-space direction=\"vertical\" :size=\"10\">\n                    <a-theme-switch-login></a-theme-switch-login>\n                    <span>{{ i18n \"pages.settings.language\" }}</span>\n                    <a-select ref=\"selectLang\" class=\"w-100\" v-model=\"lang\" @change=\"LanguageManager.setLanguage(lang)\"\n                      :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                      <a-select-option :value=\"l.value\" label=\"English\" v-for=\"l in LanguageManager.supportedLanguages\">\n                        <span role=\"img\" aria-label=\"l.name\" v-text=\"l.icon\"></span>\n                        &nbsp;&nbsp;<span v-text=\"l.name\"></span>\n                      </a-select-option>\n                    </a-select>\n                  </a-space>\n                </template>\n                <a-button shape=\"circle\" icon=\"setting\"></a-button>\n              </a-popover>\n            </div>\n            <a-row type=\"flex\" justify=\"center\">\n              <a-col :style=\"{ width: '100%' }\">\n                <h2 class=\"title headline zoom\">\n                  <span class=\"words-wrapper\">\n                    <b class=\"is-visible\">{{ i18n \"pages.login.hello\" }}</b>\n                    <b>{{ i18n \"pages.login.title\" }}</b>\n                  </span>\n                </h2>\n              </a-col>\n            </a-row>\n            <a-row type=\"flex\" justify=\"center\">\n              <a-col span=\"24\">\n                <a-form @submit.prevent=\"login\">\n                  <a-space direction=\"vertical\" size=\"middle\">\n                    <a-form-item>\n                      <a-input autocomplete=\"username\" name=\"username\" v-model.trim=\"user.username\"\n                        placeholder='{{ i18n \"username\" }}' autofocus required>\n                        <a-icon slot=\"prefix\" type=\"user\" class=\"fs-1rem\"></a-icon>\n                      </a-input>\n                    </a-form-item>\n                    <a-form-item>\n                      <a-input-password autocomplete=\"current-password\" name=\"password\" v-model.trim=\"user.password\"\n                        placeholder='{{ i18n \"password\" }}' required>\n                        <a-icon slot=\"prefix\" type=\"lock\" class=\"fs-1rem\"></a-icon>\n                      </a-input-password>\n                    </a-form-item>\n                    <a-form-item v-if=\"twoFactorEnable\">\n                      <a-input autocomplete=\"one-time-code\" name=\"twoFactorCode\" v-model.trim=\"user.twoFactorCode\"\n                        placeholder='{{ i18n \"twoFactorCode\" }}' required>\n                        <a-icon slot=\"prefix\" type=\"key\" class=\"fs-1rem\"></a-icon>\n                      </a-input>\n                    </a-form-item>\n                    <a-form-item>\n                      <a-row justify=\"center\" class=\"centered\">\n                        <div class=\"wave-btn-bg wave-btn-bg-cl h-50px mt-1rem\"\n                          :style=\"loadingStates.spinning ? 'width: 52px' : 'display: inline-block'\">\n                          <a-button class=\"ant-btn-primary-login\" type=\"primary\" :loading=\"loadingStates.spinning\"\n                            :icon=\"loadingStates.spinning ? 'poweroff' : undefined\" html-type=\"submit\">\n                            [[ loadingStates.spinning ? '' : '{{ i18n \"login\" }}' ]]\n                          </a-button>\n                        </div>\n                      </a-row>\n                    </a-form-item>\n                  </a-space>\n                </a-form>\n              </a-col>\n            </a-row>\n          </template>\n        </a-col>\n      </a-row>\n    </a-layout-content>\n  </transition>\n</a-layout>\n{{template \"page/body_scripts\" .}}\n{{template \"component/aThemeSwitch\" .}}\n<script>\n  const app = new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#app',\n    data: {\n      themeSwitcher,\n      loadingStates: { fetched: false, spinning: false },\n      user: { username: \"\", password: \"\", twoFactorCode: \"\" },\n      twoFactorEnable: false,\n      lang: \"\",\n      animationStarted: false\n    },\n    async mounted() {\n      this.lang = LanguageManager.getLanguage();\n      this.twoFactorEnable = await this.getTwoFactorEnable();\n    },\n    methods: {\n      async login() {\n        this.loadingStates.spinning = true;\n        const msg = await HttpUtil.post('/login', this.user);\n        if (msg.success) {\n          location.href = basePath + 'panel/';\n        }\n        this.loadingStates.spinning = false;\n      },\n      async getTwoFactorEnable() {\n        const msg = await HttpUtil.post('/getTwoFactorEnable');\n        if (msg.success) {\n          this.twoFactorEnable = msg.obj;\n          this.loadingStates.fetched = true;\n          this.$nextTick(() => {\n            if (!this.animationStarted) {\n              this.animationStarted = true;\n              this.initHeadline();\n            }\n          });\n          return msg.obj;\n        }\n      },\n      initHeadline() {\n        const animationDelay = 2000;\n        const headlines = this.$el.querySelectorAll('.headline');\n        headlines.forEach((headline) => {\n          const first = headline.querySelector('.is-visible');\n          if (!first) return;\n          setTimeout(() => this.hideWord(first, animationDelay), animationDelay);\n        });\n      },\n      hideWord(word, delay) {\n        const nextWord = this.takeNext(word);\n        this.switchWord(word, nextWord);\n        setTimeout(() => this.hideWord(nextWord, delay), delay);\n      },\n      takeNext(word) {\n        return word.nextElementSibling || word.parentElement.firstElementChild;\n      },\n      switchWord(oldWord, newWord) {\n        oldWord.classList.remove('is-visible');\n        oldWord.classList.add('is-hidden');\n        newWord.classList.remove('is-hidden');\n        newWord.classList.add('is-visible');\n      }\n    },\n  });\n\n  const pm_input_selector = 'input.ant-input, textarea.ant-input';\n  const pm_strip_props = [\n    'background',\n    'background-color',\n    'background-image',\n    'color'\n  ];\n\n  const pm_observed_forms = new WeakSet();\n\n  function pm_strip_inline(el) {\n    if (!el || el.nodeType !== 1 || !el.matches?.(pm_input_selector)) return;\n\n    let did_change = false;\n    for (const prop of pm_strip_props) {\n      if (el.style.getPropertyValue(prop)) {\n        el.style.removeProperty(prop);\n        did_change = true;\n      }\n    }\n\n    if (did_change && el.style.length === 0) {\n      el.removeAttribute('style');\n    }\n  }\n\n  function pm_attach_observer(form) {\n    if (pm_observed_forms.has(form)) return;\n    pm_observed_forms.add(form);\n\n    form.querySelectorAll(pm_input_selector).forEach(pm_strip_inline);\n\n    const pm_mo = new MutationObserver(mutations => {\n      for (const m of mutations) {\n        if (m.type === 'attributes') {\n          pm_strip_inline(m.target);\n        } else if (m.type === 'childList') {\n          for (const n of m.addedNodes) {\n            if (n.nodeType !== 1) continue;\n            if (n.matches?.(pm_input_selector)) pm_strip_inline(n);\n            n.querySelectorAll?.(pm_input_selector).forEach(pm_strip_inline);\n          }\n        }\n      }\n    });\n\n    pm_mo.observe(form, {\n      attributes: true,\n      attributeFilter: ['style'],\n      childList: true,\n      subtree: true\n    });\n  }\n\n  function pm_init() {\n    document.querySelectorAll('form.ant-form').forEach(pm_attach_observer);\n    const pm_host = document.getElementById('login') || document.body;\n    const pm_wait_for_forms = new MutationObserver(mutations => {\n      for (const m of mutations) {\n        for (const n of m.addedNodes) {\n          if (n.nodeType !== 1) continue;\n          if (n.matches?.('form.ant-form')) pm_attach_observer(n);\n          n.querySelectorAll?.('form.ant-form').forEach(pm_attach_observer);\n        }\n      }\n    });\n    pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true });\n  }\n\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', pm_init, { once: true });\n  } else {\n    pm_init();\n  }\n</script>\n{{ template \"page/body_end\" .}}\n"
  },
  {
    "path": "web/html/modals/client_bulk_modal.html",
    "content": "{{define \"modals/clientsBulkModal\"}}\n<a-modal id=\"client-bulk-modal\" v-model=\"clientsBulkModal.visible\" :title=\"clientsBulkModal.title\"\n    @ok=\"clientsBulkModal.ok\" :confirm-loading=\"clientsBulkModal.confirmLoading\" :closable=\"true\" :mask-closable=\"false\"\n    :ok-text=\"clientsBulkModal.okText\" cancel-text='{{ i18n \"close\" }}' :class=\"themeSwitcher.currentTheme\">\n    <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n        <a-form-item label='{{ i18n \"pages.client.method\" }}'>\n            <a-select v-model=\"clientsBulkModal.emailMethod\" buttonStyle=\"solid\"\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option :value=\"0\">Random</a-select-option>\n                <a-select-option :value=\"1\">Random+Prefix</a-select-option>\n                <a-select-option :value=\"2\">Random+Prefix+Num</a-select-option>\n                <a-select-option :value=\"3\">Random+Prefix+Num+Postfix</a-select-option>\n                <a-select-option :value=\"4\">Prefix+Num+Postfix</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.client.first\" }}' v-if=\"clientsBulkModal.emailMethod>1\">\n            <a-input-number v-model.number=\"clientsBulkModal.firstNum\" :min=\"1\"></a-input-number>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.client.last\" }}' v-if=\"clientsBulkModal.emailMethod>1\">\n            <a-input-number v-model.number=\"clientsBulkModal.lastNum\" :min=\"clientsBulkModal.firstNum\"></a-input-number>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.client.prefix\" }}' v-if=\"clientsBulkModal.emailMethod>0\">\n            <a-input v-model.trim=\"clientsBulkModal.emailPrefix\"></a-input>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.client.postfix\" }}' v-if=\"clientsBulkModal.emailMethod>2\">\n            <a-input v-model.trim=\"clientsBulkModal.emailPostfix\"></a-input>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.client.clientCount\" }}' v-if=\"clientsBulkModal.emailMethod < 2\">\n            <a-input-number v-model.number=\"clientsBulkModal.quantity\" :min=\"1\" :max=\"100\"></a-input-number>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"security\" }}' v-if=\"inbound.protocol === Protocols.VMESS\">\n            <a-select v-model=\"clientsBulkModal.security\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option v-for=\"key in USERS_SECURITY\" :value=\"key\">[[ key ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label='Flow' v-if=\"clientsBulkModal.inbound.canEnableTlsFlow()\">\n            <a-select v-model=\"clientsBulkModal.flow\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option value=\"\" selected>{{ i18n \"none\" }}</a-select-option>\n                <a-select-option v-for=\"key in TLS_FLOW_CONTROL\" :value=\"key\">[[ key ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item v-if=\"app.subSettings?.enable\">\n            <template slot=\"label\">\n                <a-tooltip>\n                    <template slot=\"title\">\n                        <span>{{ i18n \"pages.inbounds.subscriptionDesc\" }}</span>\n                    </template>\n                    Subscription\n                    <a-icon @click=\"clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)\" type=\"sync\"></a-icon>\n                </a-tooltip>\n            </template>\n            <a-input v-model.trim=\"clientsBulkModal.subId\"></a-input>\n        </a-form-item>\n        <a-form-item v-if=\"app.tgBotEnable\">\n            <template slot=\"label\">\n                <a-tooltip>\n                    <template slot=\"title\">\n                        <span>{{ i18n \"pages.inbounds.telegramDesc\" }}</span>\n                    </template>\n                    Telegram ChatID\n                    <a-icon type=\"question-circle\"></a-icon>\n                </a-tooltip>\n            </template>\n            <a-input-number :style=\"{ width: '50%' }\" v-model.number=\"clientsBulkModal.tgId\" min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item v-if=\"app.ipLimitEnable\">\n            <template slot=\"label\">\n                <a-tooltip>\n                    <template slot=\"title\">\n                        <span>{{ i18n \"pages.inbounds.IPLimitDesc\" }}</span>\n                    </template>\n                    <span>{{ i18n \"pages.inbounds.IPLimit\" }} </span>\n                    <a-icon type=\"question-circle\"></a-icon>\n                </a-tooltip>\n            </template>\n            <a-input-number v-model.number=\"clientsBulkModal.limitIp\" min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item>\n            <template slot=\"label\">\n                <a-tooltip>\n                    <template slot=\"title\">\n                        0 <span>{{ i18n \"pages.inbounds.meansNoLimit\" }}</span>\n                    </template>\n                    {{ i18n \"pages.inbounds.totalFlow\" }}\n                    <a-icon type=\"question-circle\"></a-icon>\n                </a-tooltip>\n            </template>\n            <a-input-number v-model.number=\"clientsBulkModal.totalGB\" :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.client.delayedStart\" }}'>\n            <a-switch v-model=\"clientsBulkModal.delayedStart\" @click=\"clientsBulkModal.expiryTime=0\"></a-switch>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.client.expireDays\" }}' v-if=\"clientsBulkModal.delayedStart\">\n            <a-input-number v-model.number=\"delayedExpireDays\" :min=\"0\"></a-input-number>\n        </a-form-item>\n        <a-form-item v-else>\n            <template slot=\"label\">\n                <a-tooltip>\n                    <template slot=\"title\">\n                        <span>{{ i18n \"pages.inbounds.leaveBlankToNeverExpire\" }}</span>\n                    </template>\n                    {{ i18n \"pages.inbounds.expireDate\" }}\n                    <a-icon type=\"question-circle\"></a-icon>\n                </a-tooltip>\n            </template>\n            <a-date-picker v-if=\"datepicker == 'gregorian'\" :show-time=\"{ format: 'HH:mm:ss' }\"\n                format=\"YYYY-MM-DD HH:mm:ss\" :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                v-model=\"clientsBulkModal.expiryTime\"></a-date-picker>\n            <a-persian-datepicker v-else placeholder='{{ i18n \"pages.settings.datepickerPlaceholder\" }}'\n                value=\"clientsBulkModal.expiryTime\" v-model=\"clientsBulkModal.expiryTime\">\n            </a-persian-datepicker>\n        </a-form-item>\n        <a-form-item v-if=\"clientsBulkModal.expiryTime != 0\">\n            <template slot=\"label\">\n                <a-tooltip>\n                    <template slot=\"title\">\n                        <span>{{ i18n \"pages.client.renewDesc\" }}</span>\n                    </template>\n                    {{ i18n \"pages.client.renew\" }}\n                    <a-icon type=\"question-circle\"></a-icon>\n                </a-tooltip>\n            </template>\n            <a-input-number v-model.number=\"clientsBulkModal.reset\" :min=\"0\"></a-input-number>\n        </a-form-item>\n    </a-form>\n</a-modal>\n<script>\n\n    const clientsBulkModal = {\n        visible: false,\n        confirmLoading: false,\n        title: '',\n        okText: '',\n        confirm: null,\n        dbInbound: new DBInbound(),\n        inbound: new Inbound(),\n        quantity: 1,\n        totalGB: 0,\n        limitIp: 0,\n        expiryTime: '',\n        emailMethod: 0,\n        firstNum: 1,\n        lastNum: 1,\n        emailPrefix: \"\",\n        emailPostfix: \"\",\n        subId: \"\",\n        tgId: '',\n        security: \"auto\",\n        flow: \"\",\n        delayedStart: false,\n        reset: 0,\n        ok() {\n            clients = [];\n            method = clientsBulkModal.emailMethod;\n            if (method > 1) {\n                start = clientsBulkModal.firstNum;\n                end = clientsBulkModal.lastNum + 1;\n            } else {\n                start = 0;\n                end = clientsBulkModal.quantity;\n            }\n            prefix = (method > 0 && clientsBulkModal.emailPrefix.length > 0) ? clientsBulkModal.emailPrefix : \"\";\n            useNum = (method > 1);\n            postfix = (method > 2 && clientsBulkModal.emailPostfix.length > 0) ? clientsBulkModal.emailPostfix : \"\";\n            for (let i = start; i < end; i++) {\n                newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);\n                if (method == 4) newClient.email = \"\";\n                newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;\n                if (clientsBulkModal.subId.length > 0) newClient.subId = clientsBulkModal.subId;\n                newClient.tgId = clientsBulkModal.tgId;\n                newClient.security = clientsBulkModal.security;\n                newClient.limitIp = clientsBulkModal.limitIp;\n                newClient._totalGB = clientsBulkModal.totalGB;\n                newClient._expiryTime = clientsBulkModal.expiryTime;\n                if (clientsBulkModal.inbound.canEnableTlsFlow()) {\n                    newClient.flow = clientsBulkModal.flow;\n                }\n                newClient.reset = clientsBulkModal.reset;\n                clients.push(newClient);\n            }\n            ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);\n        },\n        show({\n            title = '',\n            okText = '{{ i18n \"sure\" }}',\n            dbInbound = null,\n            confirm = (inbound, dbInbound) => { }\n        }) {\n            this.visible = true;\n            this.title = title;\n            this.okText = okText;\n            this.confirm = confirm;\n            this.quantity = 1;\n            this.totalGB = 0;\n            this.expiryTime = 0;\n            this.emailMethod = 0;\n            this.limitIp = 0;\n            this.firstNum = 1;\n            this.lastNum = 1;\n            this.emailPrefix = \"\";\n            this.emailPostfix = \"\";\n            this.subId = \"\";\n            this.tgId = '';\n            this.security = \"auto\";\n            this.flow = \"\";\n            this.dbInbound = new DBInbound(dbInbound);\n            this.inbound = dbInbound.toInbound();\n            this.delayedStart = false;\n            this.reset = 0;\n        },\n        newClient(protocol) {\n            switch (protocol) {\n                case Protocols.VMESS: return new Inbound.VmessSettings.VMESS();\n                case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();\n                case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();\n                case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);\n                default: return null;\n            }\n        },\n        close() {\n            clientsBulkModal.visible = false;\n            clientsBulkModal.loading(false);\n        },\n        loading(loading = true) {\n            clientsBulkModal.confirmLoading = loading;\n        },\n    };\n\n    const clientsBulkModalApp = new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#client-bulk-modal',\n        data: {\n            clientsBulkModal,\n            get inbound() {\n                return this.clientsBulkModal.inbound;\n            },\n            get delayedExpireDays() {\n                return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;\n            },\n            get datepicker() {\n                return app.datepicker;\n            },\n            set delayedExpireDays(days) {\n                this.clientsBulkModal.expiryTime = -86400000 * days;\n            },\n        },\n    });\n\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/client_modal.html",
    "content": "{{define \"modals/clientsModal\"}}\n<a-modal id=\"client-modal\" v-model=\"clientModal.visible\" :title=\"clientModal.title\" @ok=\"clientModal.ok\"\n         :confirm-loading=\"clientModal.confirmLoading\" :closable=\"true\" :mask-closable=\"false\"\n         :class=\"themeSwitcher.currentTheme\"\n         :ok-text=\"clientModal.okText\" cancel-text='{{ i18n \"close\" }}'>\n    <template v-if=\"isEdit\">\n        <a-tag v-if=\"isExpiry || isTrafficExhausted\" color=\"red\" :style=\"{ marginBottom: '10px', display: 'block', textAlign: 'center' }\">Account is (Expired|Traffic Ended) And Disabled</a-tag>\n    </template>\n    {{template \"form/client\"}}\n</a-modal>\n<script>\n\n    const clientModal = {\n        visible: false,\n        confirmLoading: false,\n        title: '',\n        okText: '',\n        isEdit: false,\n        dbInbound: new DBInbound(),\n        inbound: new Inbound(),\n        clients: [],\n        clientStats: [],\n        oldClientId: \"\",\n        index: null,\n        clientIps: null,\n        delayedStart: false,\n        ok() {\n            if (clientModal.isEdit) {\n                ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);\n            } else {\n                ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);\n            }\n        },\n        show({ title = '', okText = '{{ i18n \"sure\" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {\n            this.visible = true;\n            this.title = title;\n            this.okText = okText;\n            this.isEdit = isEdit;\n            this.dbInbound = new DBInbound(dbInbound);\n            this.inbound = dbInbound.toInbound();\n            this.clients = this.inbound.clients;\n            this.index = index === null ? this.clients.length : index;\n            this.delayedStart = false;\n            if (isEdit) {\n                if (this.clients[index].expiryTime < 0) {\n                    this.delayedStart = true;\n                }\n                this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);\n            } else {\n                this.addClient(this.inbound, this.clients);\n            }\n            this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);\n            this.confirm = confirm;\n        },  \n        getClientId(protocol, client) {\n            switch (protocol) {\n                case Protocols.TROJAN: return client.password;\n                case Protocols.SHADOWSOCKS: return client.email;\n                default: return client.id;\n            }\n        },\n        addClient(inbound, clients) {\n            switch (inbound.protocol) {\n                case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS());\n                case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());\n                case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());\n                case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));\n                default: return null;\n            }\n        },\n        close() {\n            clientModal.visible = false;\n            clientModal.loading(false);\n        },\n        loading(loading=true) {\n            clientModal.confirmLoading = loading;\n        },\n    };\n\n    const clientModalApp = new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#client-modal',\n        data: {\n            clientModal,\n            get inbound() {\n                return this.clientModal.inbound;\n            },\n            get client() {\n                return this.clientModal.clients[this.clientModal.index];\n            },\n            get clientStats() {\n                return this.clientModal.clientStats;\n            },\n            get isEdit() {\n                return this.clientModal.isEdit;\n            },\n            get datepicker() {\n                return app.datepicker;\n            },\n            get isTrafficExhausted() {\n                if (!clientStats) return false\n                if (clientStats.total <= 0) return false\n                if (clientStats.up + clientStats.down < clientStats.total) return false\n                return true\n            },\n            get isExpiry() {\n                return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;\n            },\n            get delayedStart() {\n                return this.clientModal.delayedStart;\n            },\n            set delayedStart(value) {\n                this.clientModal.delayedStart = value;\n            },\n            get delayedExpireDays() {\n                return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;\n            },\n            set delayedExpireDays(days) {\n                this.client.expiryTime = -86400000 * days;\n            },\n        },\n        methods: {\n            async getDBClientIps(email) {\n                const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`);\n                if (!msg.success) {\n                    document.getElementById(\"clientIPs\").value = msg.obj;\n                    return;\n                }\n                let ips = msg.obj;\n                if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) {\n                    try {\n                        ips = JSON.parse(ips);\n                        ips = Array.isArray(ips) ? ips.join(\"\\n\") : ips;\n                    } catch (e) {\n                        console.error('Error parsing JSON:', e);\n                    }\n                }\n                document.getElementById(\"clientIPs\").value = ips;\n            },\n            async clearDBClientIps(email) {\n                try {\n                    const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`);\n                    if (!msg.success) {\n                        return;\n                    }\n                    document.getElementById(\"clientIPs\").value = \"\";\n                } catch (error) {\n                }\n            },\n            resetClientTraffic(email, dbInboundId, iconElement) {\n                this.$confirm({\n                    title: '{{ i18n \"pages.inbounds.resetTraffic\"}}',\n                    content: '{{ i18n \"pages.inbounds.resetTrafficContent\"}}',\n                    class: themeSwitcher.currentTheme,\n                    okText: '{{ i18n \"reset\"}}',\n                    cancelText: '{{ i18n \"cancel\"}}',\n                    onOk: async () => {\n                        iconElement.disabled = true;\n                        const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);\n                        if (msg.success) {\n                            this.clientModal.clientStats.up = 0;\n                            this.clientModal.clientStats.down = 0;\n                        }\n                        iconElement.disabled = false;\n                    },\n                })\n            },\n        },\n    });\n\n</script>\n{{end}}\n"
  },
  {
    "path": "web/html/modals/dns_presets_modal.html",
    "content": "{{define \"modals/dnsPresetsModal\"}}\n<a-modal id=\"dnsPresets-modal\" v-model=\"dnsPresetsModal.visible\" :title=\"dnsPresetsModal.title\" :closable=\"true\"\n  :mask-closable=\"false\" :footer=\"null\" :class=\"themeSwitcher.currentTheme\">\n  <a-list class=\"ant-dns-presets-list\" bordered :style=\"{ width: '100%' }\">\n    <a-list-item v-for=\"dns in dnsPresetsDatabase\" :style=\"{ padding: '12px 16px' }\">\n      <div class=\"ant-dns-presets-line\">\n        <a-space direction=\"horizontal\" size=\"small\" align=\"center\">\n          <a-tag :color=\"dns.family ? 'purple' : 'green'\">[[ dns.family ? '{{ i18n \"pages.xray.dns.dnsPresetFamily\" }}' : 'DNS' ]]</a-tag>\n          <span class=\"ant-dns-presets-list-name\">[[ dns.name ]]</span>\n        </a-space>\n        <a-button class=\"ant-dns-presets-install\" type=\"primary\" @click=\"dnsPresetsModal.install(dns.data)\">{{ i18n \"install\" }}</a-button>\n      </div>\n    </a-list-item>\n  </a-list>\n</a-modal>\n\n<style>\n  .ant-dns-presets-line {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    width: 100%;\n  }\n\n  .ant-dns-presets-install {\n    margin-left: auto;\n  }\n\n  .dark .ant-dns-presets-list {\n    border-color: var(--dark-color-stroke)\n  }\n\n  .dark .ant-dns-presets-list-name {\n    color: var(--dark-color-text-primary);\n  }\n</style>\n\n<script>\n  const dnsPresetsDatabase = [\n    {\n      name: 'Google DNS',\n      family: false,\n      data: [\n        \"8.8.8.8\",\n        \"8.8.4.4\",\n        \"2001:4860:4860::8888\",\n        \"2001:4860:4860::8844\"\n      ]\n    },\n    {\n      name: 'Cloudflare DNS',\n      family: false,\n      data: [\n        \"1.1.1.1\",\n        \"1.0.0.1\",\n        \"2606:4700:4700::1111\",\n        \"2606:4700:4700::1001\"\n      ]\n    },\n    {\n      name: 'Adguard DNS',\n      family: false,\n      data: [\n        \"94.140.14.14\",\n        \"94.140.15.15\",\n        \"2a10:50c0::ad1:ff\",\n        \"2a10:50c0::ad2:ff\"\n      ]\n    },\n    {\n      name: 'Adguard Family DNS',\n      family: true,\n      data: [\n        \"94.140.14.14\",\n        \"94.140.15.15\",\n        \"2a10:50c0::ad1:ff\",\n        \"2a10:50c0::ad2:ff\"\n      ]\n    },\n    {\n      name: 'Cloudflare Family DNS',\n      family: true,\n      data: [\n        \"1.1.1.3\",\n        \"1.0.0.3\",\n        \"2606:4700:4700::1113\",\n        \"2606:4700:4700::1003\"\n      ]\n    }\n  ]\n\n  const dnsPresetsModal = {\n    title: '',\n    visible: false,\n    selected: null,\n    install(selectedPreset) {\n      return ObjectUtil.execute(dnsPresetsModal.selected, selectedPreset);\n    },\n    show({ title = '', selected = (selectedPreset) => { }, isEdit = false }) {\n      this.title = title;\n      this.selected = selected;\n      this.visible = true;\n    },\n    close() {\n      dnsPresetsModal.visible = false;\n    },\n  };\n\n  new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#dnsPresets-modal',\n    data: {\n      dnsPresetsModal: dnsPresetsModal,\n    }\n  });\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/inbound_info_modal.html",
    "content": "{{define \"modals/inboundInfoModal\"}}\n<a-modal id=\"inbound-info-modal\" v-model=\"infoModal.visible\"\n  title='{{ i18n \"pages.inbounds.details\"}}' :closable=\"true\"\n  :mask-closable=\"true\" :footer=\"null\" width=\"600px\"\n  :class=\"themeSwitcher.currentTheme\">\n  <a-row>\n    <a-col :xs=\"24\" :md=\"12\">\n      <table>\n        <tr>\n          <td>{{ i18n \"protocol\" }}</td>\n          <td>\n            <a-tag color=\"purple\">[[ dbInbound.protocol ]]</a-tag>\n          </td>\n        </tr>\n        <tr>\n          <td>{{ i18n \"pages.inbounds.address\" }}</td>\n          <td>\n            <a-tooltip :title=\"[[ dbInbound.address ]]\">\n              <a-tag class=\"info-large-tag\">[[ dbInbound.address ]]</a-tag>\n            </a-tooltip>\n          </td>\n        </tr>\n        <tr>\n          <td>{{ i18n \"pages.inbounds.port\" }}</td>\n          <td>\n            <a-tag>[[ dbInbound.port ]]</a-tag>\n          </td>\n        </tr>\n      </table>\n    </a-col>\n    <a-col :xs=\"24\" :md=\"12\">\n      <template\n        v-if=\"dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS\">\n        <table>\n          <tr>\n            <td>{{ i18n \"transmission\" }}</td>\n            <td>\n              <a-tag color=\"green\">[[ inbound.network ]]</a-tag>\n            </td>\n          </tr>\n          <template\n            v-if=\"inbound.isTcp || inbound.isWs || inbound.isHttpupgrade || inbound.isXHTTP\">\n            <tr>\n              <td>{{ i18n \"host\" }}</td>\n              <td v-if=\"inbound.host\">\n                <a-tooltip :title=\"[[ inbound.host ]]\">\n                  <a-tag class=\"info-large-tag\">[[ inbound.host ]]</a-tag>\n                </a-tooltip>\n              </td>\n              <td v-else>\n                <a-tag color=\"orange\">{{ i18n \"none\" }}</a-tag>\n              </td>\n            </tr>\n          </tr>\n          <tr>\n            <td>{{ i18n \"path\" }}</td>\n            <td v-if=\"inbound.path\">\n              <a-tooltip :title=\"[[ inbound.path ]]\">\n                <a-tag class=\"info-large-tag\">[[ inbound.path ]]</a-tag>\n              </a-tooltip>\n              <td v-else>\n                <a-tag color=\"orange\">{{ i18n \"none\" }}</a-tag>\n              </td>\n            </tr>\n          </template>\n          <template v-if=\"inbound.isXHTTP\">\n            <tr>\n              <td>Mode</td>\n              <td>\n                <a-tag>[[ inbound.stream.xhttp.mode ]]</a-tag>\n              </td>\n            </tr>\n          </template>\n          <template v-if=\"inbound.isGrpc\">\n            <tr>\n              <td>grpc serviceName</td>\n              <td>\n                <a-tooltip :title=\"[[ inbound.serviceName ]]\">\n                  <a-tag class=\"info-large-tag\">[[ inbound.serviceName\n                    ]]</a-tag>\n                </a-tooltip>\n                <tr>\n                  <td>grpc multiMode</td>\n                  <td>\n                    <a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag>\n                  </td>\n                </tr>\n              </template>\n            </table>\n          </template>\n        </a-col>\n        <template v-if=\"dbInbound.hasLink()\">\n          {{ i18n \"security\" }}\n          <a-tag :color=\"inbound.stream.security == 'none' ? 'red' : 'green'\">[[\n            inbound.stream.security ]]</a-tag>\n          <br />\n          <td>Authentication</td>\n          <a-tag v-if=\"inbound.settings.selectedAuth\" color=\"green\">[[\n            inbound.settings.selectedAuth ? inbound.settings.selectedAuth : ''\n            ]]</a-tag>\n          <a-tag v-else color=\"red\">{{ i18n \"none\" }}</a-tag>\n          <br />\n          {{ i18n \"encryption\" }}\n          <a-tag class=\"info-large-tag\"\n            :color=\"inbound.settings.encryption ? 'green' : 'red'\">[[\n            inbound.settings.encryption ? inbound.settings.encryption : ''\n            ]]</a-tag>\n          <a-tooltip title='{{ i18n \"copy\" }}'>\n            <a-button size=\"small\" icon=\"snippets\"\n              @click=\"copy(inbound.settings.encryption)\"></a-button>\n          </a-tooltip>\n          <br />\n          <template v-if=\"inbound.stream.security != 'none'\">\n            {{ i18n \"domainName\" }}\n            <a-tag v-if=\"inbound.serverName\" color=\"green\">[[ inbound.serverName\n              ? inbound.serverName : '' ]]</a-tag>\n            <a-tag v-else color=\"orange\">{{ i18n \"none\" }}</a-tag>\n          </template>\n        </template>\n        <table v-if=\"dbInbound.isSS\"\n          :style=\"{ marginBottom: '10px', width: '100%' }\">\n          <tr>\n            <td>{{ i18n \"encryption\" }}</td>\n            <td>\n              <a-tag color=\"green\">[[ inbound.settings.method ]]</a-tag>\n            </td>\n          </tr>\n          <tr v-if=\"inbound.isSS2022\">\n            <td>{{ i18n \"password\" }}</td>\n            <td>\n              <a-tooltip :title=\"[[ inbound.settings.password  ]]\">\n                <a-tag class=\"info-large-tag\">[[ inbound.settings.password\n                  ]]</a-tag>\n              </a-tooltip>\n            </td>\n          </tr>\n          <tr>\n            <td>{{ i18n \"pages.inbounds.network\" }}</td>\n            <td>\n              <a-tag color=\"green\">[[ inbound.settings.network ]]</a-tag>\n            </td>\n          </tr>\n        </table>\n        <template v-if=\"infoModal.clientSettings\">\n          <a-divider>{{ i18n \"pages.inbounds.client\" }}</a-divider>\n          <table :style=\"{ marginBottom: '10px' }\">\n            <tr>\n              <td>{{ i18n \"pages.inbounds.email\" }}</td>\n              <td v-if=\"infoModal.clientSettings.email\">\n                <a-tag color=\"green\">[[ infoModal.clientSettings.email\n                  ]]</a-tag>\n              </td>\n              <td v-else>\n                <a-tag color=\"red\">{{ i18n \"none\" }}</a-tag>\n              </td>\n            </tr>\n            <tr v-if=\"infoModal.clientSettings.id\">\n              <td>ID</td>\n              <td>\n                <a-tag>[[ infoModal.clientSettings.id ]]</a-tag>\n              </td>\n            </tr>\n            <tr v-if=\"dbInbound.isVMess\">\n              <td>{{ i18n \"security\" }}</td>\n              <td>\n                <a-tag>[[ infoModal.clientSettings.security ]]</a-tag>\n              </td>\n            </tr>\n            <tr v-if=\"infoModal.inbound.canEnableTlsFlow()\">\n              <td>Flow</td>\n              <td v-if=\"infoModal.clientSettings.flow\">\n                <a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>\n              </td>\n              <td v-else>\n                <a-tag color=\"orange\">{{ i18n \"none\" }}</a-tag>\n              </td>\n            </tr>\n            <tr v-if=\"infoModal.clientSettings.password\">\n              <td>{{ i18n \"password\" }}</td>\n              <td>\n                <a-tooltip :title=\"[[ infoModal.clientSettings.password  ]]\">\n                  <a-tag class=\"info-large-tag\">[[\n                    infoModal.clientSettings.password ]]</a-tag>\n                </a-tooltip>\n              </td>\n            </tr>\n            <tr>\n              <td>{{ i18n \"status\" }}</td>\n              <td>\n                <a-tag v-if=\"isDepleted\" color=\"red\">{{ i18n \"depleted\"\n                  }}</a-tag>\n                <a-tag v-else-if=\"isEnable\" color=\"green\">{{ i18n \"enabled\"\n                  }}</a-tag>\n                <a-tag v-else>{{ i18n \"disabled\" }}</a-tag>\n              </td>\n            </tr>\n            <tr v-if=\"infoModal.clientStats\">\n              <td>{{ i18n \"usage\" }}</td>\n              <td>\n                <a-tag color=\"green\">[[\n                  SizeFormatter.sizeFormat(infoModal.clientStats.up +\n                  infoModal.clientStats.down) ]]</a-tag>\n                <a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up)\n                  ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down)\n                  ]] ↓</a-tag>\n              </td>\n            </tr>\n            <tr>\n              <td>{{ i18n \"pages.inbounds.createdAt\" }}</td>\n              <td>\n                <template\n                  v-if=\"infoModal.clientSettings && infoModal.clientSettings.created_at\">\n                  <a-tag>[[\n                    IntlUtil.formatDate(infoModal.clientSettings.created_at)\n                    ]]</a-tag>\n                </template>\n                <template v-else>\n                  <a-tag>-</a-tag>\n                </template>\n              </td>\n            </tr>\n            <tr>\n              <td>{{ i18n \"pages.inbounds.updatedAt\" }}</td>\n              <td>\n                <template\n                  v-if=\"infoModal.clientSettings && infoModal.clientSettings.updated_at\">\n                  <a-tag>[[\n                    IntlUtil.formatDate(infoModal.clientSettings.updated_at)\n                    ]]</a-tag>\n                </template>\n                <template v-else>\n                  <a-tag>-</a-tag>\n                </template>\n              </td>\n            </tr>\n            <tr>\n              <td>{{ i18n \"lastOnline\" }}</td>\n              <td>\n                <a-tag>[[ app.formatLastOnline(infoModal.clientSettings &&\n                  infoModal.clientSettings.email ?\n                  infoModal.clientSettings.email : '') ]]</a-tag>\n              </td>\n            </tr>\n            <tr v-if=\"infoModal.clientSettings.comment\">\n              <td>{{ i18n \"comment\" }}</td>\n              <td>\n                <a-tooltip :title=\"[[ infoModal.clientSettings.comment  ]]\">\n                  <a-tag class=\"info-large-tag\">[[\n                    infoModal.clientSettings.comment ]]</a-tag>\n                </a-tooltip>\n              </td>\n            </tr>\n            <tr v-if=\"app.ipLimitEnable\">\n              <td>{{ i18n \"pages.inbounds.IPLimit\" }}</td>\n              <td>\n                <a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>\n              </td>\n            </tr>\n            <tr\n              v-if=\"app.ipLimitEnable && infoModal.clientSettings.limitIp > 0\">\n              <td>{{ i18n \"pages.inbounds.IPLimitlog\" }}</td>\n              <td>\n                <div\n                  style=\"max-height: 150px; overflow-y: auto; text-align: left;\">\n                  <div\n                    v-if=\"infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0\">\n                    <a-tag\n                      v-for=\"(ipInfo, idx) in infoModal.clientIpsArray\"\n                      :key=\"idx\"\n                      color=\"blue\"\n                      style=\"margin: 2px 0; display: block; font-family: monospace; font-size: 11px;\">\n                      [[ formatIpInfo(ipInfo) ]]\n                    </a-tag>\n                  </div>\n                  <a-tag v-else>[[ infoModal.clientIps || 'No IP Record'\n                    ]]</a-tag>\n                </div>\n                <div style=\"margin-top: 5px;\">\n                  <a-icon type=\"sync\" :spin=\"refreshing\" @click=\"refreshIPs\"\n                    :style=\"{ margin: '0 5px' }\"></a-icon>\n                  <a-tooltip>\n                    <template slot=\"title\">\n                      <span>{{ i18n \"pages.inbounds.IPLimitlogclear\" }}</span>\n                    </template>\n                    <a-icon type=\"delete\" @click=\"clearClientIps\"></a-icon>\n                  </a-tooltip>\n                </div>\n              </td>\n            </tr>\n          </table>\n          <table\n            :style=\"{ display: 'inline-table', marginBlock: '10px', width: '100%', textAlign: 'center' }\">\n            <tr>\n              <th>{{ i18n \"remained\" }}</th>\n              <th>{{ i18n \"pages.inbounds.totalFlow\" }}</th>\n              <th>{{ i18n \"pages.inbounds.expireDate\" }}</th>\n            </tr>\n            <tr>\n              <td>\n                <a-tag\n                  v-if=\"infoModal.clientStats && infoModal.clientSettings.totalGB > 0\"\n                  :color=\"statsColor(infoModal.clientStats)\"> [[ getRemStats()\n                  ]] </a-tag>\n              </td>\n              <td>\n                <a-tag v-if=\"infoModal.clientSettings.totalGB > 0\"\n                  :color=\"statsColor(infoModal.clientStats)\"> [[\n                  SizeFormatter.sizeFormat(infoModal.clientSettings.totalGB) ]]\n                </a-tag>\n                <a-tag v-else color=\"purple\" class=\"infinite-tag\">\n                  <svg height=\"10px\" width=\"14px\" viewBox=\"0 0 640 512\"\n                    fill=\"currentColor\">\n                    <path\n                      d=\"M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z\"\n                      fill=\"currentColor\"></path>\n                  </svg>\n                </a-tag>\n              </td>\n              <td>\n                <template v-if=\"infoModal.clientSettings.expiryTime > 0\">\n                  <a-tag\n                    :color=\"ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)\">\n                    [[ IntlUtil.formatDate(infoModal.clientSettings.expiryTime)\n                    ]]\n                  </a-tag>\n                </template>\n                <a-tag v-else-if=\"infoModal.clientSettings.expiryTime < 0\"\n                  color=\"green\">[[ infoModal.clientSettings.expiryTime /\n                  -86400000 ]] {{ i18n \"pages.client.days\" }}\n                </a-tag>\n                <a-tag v-else color=\"purple\" class=\"infinite-tag\">\n                  <svg height=\"10px\" width=\"14px\" viewBox=\"0 0 640 512\"\n                    fill=\"currentColor\">\n                    <path\n                      d=\"M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z\"\n                      fill=\"currentColor\"></path>\n                  </svg>\n                </a-tag>\n              </td>\n            </tr>\n          </table>\n          <template\n            v-if=\"app.subSettings.enable && infoModal.clientSettings.subId\">\n            <a-divider>Subscription URL</a-divider>\n            <tr-info-row class=\"tr-info-row\">\n              <tr-info-title class=\"tr-info-title\">\n                <a-tag color=\"purple\">Subscription Link</a-tag>\n                <a-tooltip title='{{ i18n \"copy\" }}'>\n                  <a-button size=\"small\" icon=\"snippets\"\n                    @click=\"copy(infoModal.subLink)\"></a-button>\n                </a-tooltip>\n              </tr-info-title>\n              <a :href=\"[[ infoModal.subLink ]]\" target=\"_blank\">[[\n                infoModal.subLink ]]</a>\n            </tr-info-row>\n            <tr-info-row class=\"tr-info-row\"\n              v-if=\"app.subSettings.subJsonEnable\">\n              <tr-info-title class=\"tr-info-title\">\n                <a-tag color=\"purple\">Json Link</a-tag>\n                <a-tooltip title='{{ i18n \"copy\" }}'>\n                  <a-button size=\"small\" icon=\"snippets\"\n                    @click=\"copy(infoModal.subJsonLink)\"></a-button>\n                </a-tooltip>\n              </tr-info-title>\n              <a :href=\"[[ infoModal.subJsonLink ]]\" target=\"_blank\">[[\n                infoModal.subJsonLink ]]</a>\n            </tr-info-row>\n          </template>\n          <template v-if=\"app.tgBotEnable && infoModal.clientSettings.tgId\">\n            <a-divider>Telegram ChatID</a-divider>\n            <tr-info-row class=\"tr-info-row\">\n              <tr-info-title class=\"tr-info-title\">\n                <a-tag color=\"blue\">[[ infoModal.clientSettings.tgId ]]</a-tag>\n                <a-tooltip title='{{ i18n \"copy\" }}'>\n                  <a-button size=\"small\" icon=\"snippets\"\n                    @click=\"copy(infoModal.clientSettings.tgId)\"></a-button>\n                </a-tooltip>\n              </tr-info-title>\n            </tr-info-row>\n          </template>\n          <template v-if=\"dbInbound.hasLink()\">\n            <a-divider>URL</a-divider>\n            <tr-info-row v-for=\"(link,index) in infoModal.links\"\n              class=\"tr-info-row\">\n              <tr-info-title class=\"tr-info-title\">\n                <a-tag class=\"tr-info-tag\" color=\"green\">[[ link.remark\n                  ]]</a-tag>\n                <a-tooltip title='{{ i18n \"copy\" }}'>\n                  <a-button :style=\"{ minWidth: '24px' }\" size=\"small\"\n                    icon=\"snippets\" @click=\"copy(link.link)\"></a-button>\n                </a-tooltip>\n              </tr-info-title>\n              <code>[[ link.link ]]</code>\n            </tr-info-row>\n          </template>\n        </template>\n        <template v-else>\n          <template v-if=\"dbInbound.isSS && !inbound.isSSMultiUser\">\n            <a-divider>URL</a-divider>\n            <tr-info-row v-for=\"(link,index) in infoModal.links\"\n              class=\"tr-info-row\">\n              <tr-info-title class=\"tr-info-title\">\n                <a-tag class=\"tr-info-tag\" color=\"green\">[[ link.remark\n                  ]]</a-tag>\n                <a-tooltip title='{{ i18n \"copy\" }}'>\n                  <a-button :style=\"{ minWidth: '24px' }\" size=\"small\"\n                    icon=\"snippets\" @click=\"copy(link.link)\"></a-button>\n                </a-tooltip>\n              </tr-info-title>\n              <code>[[ link.link ]]</code>\n            </tr-info-row>\n          </template>\n          <table v-if=\"inbound.protocol == Protocols.TUNNEL\"\n            class=\"tr-info-table\">\n            <tr>\n              <th>{{ i18n \"pages.inbounds.targetAddress\" }}</th>\n              <th>{{ i18n \"pages.inbounds.destinationPort\" }}</th>\n              <th>{{ i18n \"pages.inbounds.network\" }}</th>\n              <th>FollowRedirect</th>\n            </tr>\n            <tr>\n              <td>\n                <a-tag color=\"green\">[[ inbound.settings.address ]]</a-tag>\n              </td>\n              <td>\n                <a-tag color=\"green\">[[ inbound.settings.port ]]</a-tag>\n              </td>\n              <td>\n                <a-tag color=\"green\">[[ inbound.settings.network ]]</a-tag>\n              </td>\n              <td>\n                <a-tag color=\"green\">[[ inbound.settings.followRedirect\n                  ]]</a-tag>\n              </td>\n            </tr>\n          </table>\n          <table v-if=\"dbInbound.isMixed\" class=\"tr-info-table\">\n            <tr>\n              <th>{{ i18n \"password\" }} Auth</th>\n              <th>{{ i18n \"pages.inbounds.enable\" }} udp</th>\n              <th>IP</th>\n            </tr>\n            <tr>\n              <td>\n                <a-tag color=\"green\">[[ inbound.settings.auth ]]</a-tag>\n              </td>\n              <td>\n                <a-tag color=\"green\">[[ inbound.settings.udp]]</a-tag>\n              </td>\n              <td>\n                <a-tag color=\"green\">[[ inbound.settings.ip ]]</a-tag>\n              </td>\n            </tr>\n            <template v-if=\"inbound.settings.auth == 'password'\">\n              <tr>\n                <td></td>\n                <td>{{ i18n \"username\" }}</td>\n                <td>{{ i18n \"password\" }}</td>\n              </tr>\n              <tr v-for=\"account,index in inbound.settings.accounts\">\n                <td>[[ index ]]</td>\n                <td>\n                  <a-tag color=\"green\">[[ account.user ]]</a-tag>\n                </td>\n                <td>\n                  <a-tag color=\"green\">[[ account.pass ]]</a-tag>\n                </td>\n              </tr>\n            </template>\n          </table>\n          <table v-if=\"dbInbound.isHTTP\" class=\"tr-info-table\">\n            <tr>\n              <th></th>\n              <th>{{ i18n \"username\" }}</th>\n              <th>{{ i18n \"password\" }}</th>\n            </tr>\n            <tr v-for=\"account,index in inbound.settings.accounts\">\n              <td>[[ index ]]</td>\n              <td>\n                <a-tag color=\"green\">[[ account.user ]]</a-tag>\n              </td>\n              <td>\n                <a-tag color=\"green\">[[ account.pass ]]</a-tag>\n              </td>\n            </tr>\n          </table>\n          <table v-if=\"dbInbound.isWireguard\" class=\"tr-info-table\">\n            <tr class=\"client-table-odd-row\">\n              <td>{{ i18n \"pages.xray.wireguard.secretKey\" }}</td>\n              <td>[[ inbound.settings.secretKey ]]</td>\n            </tr>\n            <tr>\n              <td>{{ i18n \"pages.xray.wireguard.publicKey\" }}</td>\n              <td>[[ inbound.settings.pubKey ]]</td>\n            </tr>\n            <tr class=\"client-table-odd-row\">\n              <td>MTU</td>\n              <td>[[ inbound.settings.mtu ]]</td>\n            </tr>\n            <tr>\n              <td>No Kernel Tun</td>\n              <td>[[ inbound.settings.noKernelTun ]]</td>\n            </tr>\n            <template v-for=\"(peer, index) in inbound.settings.peers\">\n              <tr>\n                <td colspan=\"2\">\n                  <a-divider>Peer [[ index + 1 ]]</a-divider>\n                </td>\n              </tr>\n              <tr class=\"client-table-odd-row\">\n                <td>{{ i18n \"pages.xray.wireguard.secretKey\" }}</td>\n                <td>[[ peer.privateKey ]]</td>\n              </tr>\n              <tr>\n                <td>{{ i18n \"pages.xray.wireguard.publicKey\" }}</td>\n                <td>[[ peer.publicKey ]]</td>\n              </tr>\n              <tr class=\"client-table-odd-row\">\n                <td>{{ i18n \"pages.xray.wireguard.psk\" }}</td>\n                <td>[[ peer.psk ]]</td>\n              </tr>\n              <tr>\n                <td>{{ i18n \"pages.xray.wireguard.allowedIPs\" }}</td>\n                <td>[[ peer.allowedIPs.join(\",\") ]]</td>\n              </tr>\n              <tr class=\"client-table-odd-row\">\n                <td>Keep Alive</td>\n                <td>[[ peer.keepAlive ]]</td>\n              </tr>\n              <tr>\n                <td colspan=\"2\">\n                  <tr-info-row class=\"tr-info-row\">\n                    <tr-info-title class=\"tr-info-title\">\n                      <a-tag color=\"blue\">Config</a-tag>\n                      <a-tooltip title='{{ i18n \"copy\" }}'>\n                        <a-button :style=\"{ minWidth: '24px' }\" size=\"small\"\n                          icon=\"snippets\"\n                          @click=\"copy(infoModal.links[index])\"></a-button>\n                      </a-tooltip>\n                      <a-tooltip title='{{ i18n \"download\" }}'>\n                        <a-button :style=\"{ minWidth: '24px' }\" size=\"small\"\n                          icon=\"download\"\n                          @click=\"FileManager.downloadTextFile(infoModal.links[index], `peer-${index + 1}.conf`)\"></a-button>\n                      </a-tooltip>\n                    </tr-info-title>\n                    <div\n                      v-html=\"infoModal.links[index].replaceAll(`\\n`,`<br />`)\"\n                      :style=\"{ borderRadius: '1rem', padding: '0.5rem' }\"\n                      class=\"client-table-odd-row\">\n                    </div>\n                  </tr-info-row>\n                </td>\n              </tr>\n            </table>\n          </template>\n        </template>\n      </a-modal>\n      <script>\n  function refreshIPs(email) {\n    return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {\n      if (!msg.success) {\n        return { text: 'No IP Record', array: [] };\n      }\n\n      const formatIpRecord = (record) => {\n        if (record == null) {\n          return '';\n        }\n        if (typeof record === 'string' || typeof record === 'number') {\n          return String(record);\n        }\n        const ip = record.ip || record.IP || '';\n        const timestamp = record.timestamp || record.Timestamp || 0;\n        if (!ip) {\n          return String(record);\n        }\n        if (!timestamp) {\n          return String(ip);\n        }\n        const date = new Date(Number(timestamp) * 1000);\n        const timeStr = date\n          .toLocaleString('en-GB', {\n            year: 'numeric',\n            month: '2-digit',\n            day: '2-digit',\n            hour: '2-digit',\n            minute: '2-digit',\n            second: '2-digit',\n            hour12: false,\n          })\n          .replace(',', '');\n        return `${ip} (${timeStr})`;\n      };\n\n      try {\n        let ips = msg.obj;\n        // If msg.obj is a string, try to parse it\n        if (typeof ips === 'string') {\n          try {\n            ips = JSON.parse(ips);\n          } catch (e) {\n            return { text: String(ips), array: [String(ips)] };\n          }\n        }\n\n        // Normalize single object response to array\n        if (ips && !Array.isArray(ips) && typeof ips === 'object') {\n          ips = [ips];\n        }\n\n        // New format or object array\n        if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {\n          const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);\n          return { text: result.join(' | '), array: result };\n        }\n\n        // Old format - simple array of IPs\n        if (Array.isArray(ips) && ips.length > 0) {\n          const result = ips.map((ip) => String(ip));\n          return { text: result.join(', '), array: result };\n        }\n\n        // Fallback for any other format\n        return { text: String(ips), array: [String(ips)] };\n\n      } catch (e) {\n        return { text: 'Error loading IPs', array: [] };\n      }\n    });\n  }\n\n  const infoModal = {\n    visible: false,\n    inbound: new Inbound(),\n    dbInbound: new DBInbound(),\n    clientSettings: null,\n    clientStats: [],\n    upStats: 0,\n    downStats: 0,\n    links: [],\n    index: null,\n    isExpired: false,\n    subLink: '',\n    subJsonLink: '',\n    clientIps: '',\n    clientIpsArray: [],\n    show(dbInbound, index) {\n      this.index = index;\n      this.inbound = dbInbound.toInbound();\n      this.dbInbound = new DBInbound(dbInbound);\n      this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;\n      this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;\n      this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) || null) : null;\n\n      if (\n        [\n          Protocols.VMESS, \n          Protocols.VLESS,\n          Protocols.TROJAN, \n          Protocols.SHADOWSOCKS\n        ].includes(this.inbound.protocol)\n      ) {\n        if (app.ipLimitEnable && this.clientSettings.limitIp) {\n          refreshIPs(this.clientStats.email).then((result) => {\n            this.clientIps = result.text;\n            this.clientIpsArray = result.array;\n          })\n        }\n      }\n      if (this.inbound.protocol == Protocols.WIREGUARD) {\n        this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\\r\\n')\n      } else {\n        this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);\n      }\n      if (this.clientSettings) {\n        if (this.clientSettings.subId) {\n          this.subLink = this.genSubLink(this.clientSettings.subId);\n          this.subJsonLink = app.subSettings.subJsonEnable ? this.genSubJsonLink(this.clientSettings.subId) : '';\n        }\n      }\n      this.visible = true;\n    },\n    close() {\n      infoModal.visible = false;\n    },\n    genSubLink(subID) {\n      return app.subSettings.subURI + subID;\n    },\n    genSubJsonLink(subID) {\n      return app.subSettings.subJsonURI + subID;\n    }\n  };\n  const infoModalApp = new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#inbound-info-modal',\n    data: {\n      infoModal,\n      refreshing: false,\n      get dbInbound() {\n        return this.infoModal.dbInbound;\n      },\n      get inbound() {\n        return this.infoModal.inbound;\n      },\n      get isActive() {\n        if (infoModal.clientStats) {\n          return infoModal.clientStats.enable;\n        }\n        return true;\n      },\n      get isEnable() {\n        if (infoModal.clientSettings) {\n          return infoModal.clientSettings.enable;\n        }\n        return infoModal.dbInbound.isEnable;\n      },\n      get isDepleted() {\n        const stats = infoModal.clientStats;\n        const settings = infoModal.clientSettings;\n        if (!stats || !settings) {\n          return false;\n        }\n        const total = stats.total ?? 0;\n        const used = (stats.up ?? 0) + (stats.down ?? 0);\n        const hasTotal = total > 0;\n        const exhausted = hasTotal && used >= total;\n\n        const expiryTime = settings.expiryTime ?? 0;\n        const hasExpiry = expiryTime > 0;\n        const now = Date.now();\n        const expired = hasExpiry && now >= expiryTime;\n\n        return expired || exhausted;\n      },\n    },\n    methods: {\n      formatIpInfo(ipInfo) {\n        if (ipInfo == null) {\n          return '';\n        }\n        if (typeof ipInfo === 'string' || typeof ipInfo === 'number') {\n          return String(ipInfo);\n        }\n        const ip = ipInfo.ip || ipInfo.IP || '';\n        const timestamp = ipInfo.timestamp || ipInfo.Timestamp || 0;\n        if (!ip) {\n          return String(ipInfo);\n        }\n        if (!timestamp) {\n          return String(ip);\n        }\n        const date = new Date(Number(timestamp) * 1000);\n        const timeStr = date\n          .toLocaleString('en-GB', {\n            year: 'numeric',\n            month: '2-digit',\n            day: '2-digit',\n            hour: '2-digit',\n            minute: '2-digit',\n            second: '2-digit',\n            hour12: false,\n          })\n          .replace(',', '');\n        return `${ip} (${timeStr})`;\n      },\n      copy(content) {\n        ClipboardManager\n          .copyText(content)\n          .then(() => {\n            app.$message.success('{{ i18n \"copied\" }}')\n          })\n      },\n      statsColor(stats) {\n        return ColorUtils.usageColor(stats.up + stats.down, app.trafficDiff, stats.total);\n      },\n      getRemStats() {\n        remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;\n        return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';\n      },\n      refreshIPs() {\n        this.refreshing = true;\n        refreshIPs(this.infoModal.clientStats.email)\n          .then((result) => {\n            this.infoModal.clientIps = result.text;\n            this.infoModal.clientIpsArray = result.array;\n          })\n          .finally(() => {\n            this.refreshing = false;\n          });\n      },\n      clearClientIps() {\n        HttpUtil.post(`/panel/api/inbounds/clearClientIps/${this.infoModal.clientStats.email}`)\n          .then((msg) => {\n            if (!msg.success) {\n              return;\n            }\n            this.infoModal.clientIps = 'No IP Record';\n            this.infoModal.clientIpsArray = [];\n          })\n          .catch(() => {});\n      },\n    },\n  });\n</script>\n      {{end}}\n"
  },
  {
    "path": "web/html/modals/inbound_modal.html",
    "content": "{{define \"modals/inboundModal\"}}\n<a-modal id=\"inbound-modal\" v-model=\"inModal.visible\" :title=\"inModal.title\" :dialog-style=\"{ top: '20px' }\"\n    @ok=\"inModal.ok\" :confirm-loading=\"inModal.confirmLoading\" :closable=\"true\" :mask-closable=\"false\"\n    :class=\"themeSwitcher.currentTheme\" :ok-text=\"inModal.okText\" cancel-text='{{ i18n \"close\" }}'>\n    {{template \"form/inbound\"}}\n</a-modal>\n<script>\n\n    // Make inModal globally available to ensure it works with any base path\n    const inModal = window.inModal = {\n        title: '',\n        visible: false,\n        confirmLoading: false,\n        okText: '{{ i18n \"sure\" }}',\n        isEdit: false,\n        confirm: null,\n        inbound: new Inbound(),\n        dbInbound: new DBInbound(),\n        ok() {\n            ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);\n        },\n        show({ title = '', okText = '{{ i18n \"sure\" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {\n            this.title = title;\n            this.okText = okText;\n            if (inbound) {\n                this.inbound = Inbound.fromJson(inbound.toJson());\n            } else {\n                this.inbound = new Inbound();\n            }\n            // Always ensure testseed is initialized for VLESS protocol (even if vision flow is not set yet)\n            // This ensures Vue reactivity works properly\n            if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {\n                if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed) || this.inbound.settings.testseed.length < 4) {\n                    // Create a new array to ensure Vue reactivity\n                    this.inbound.settings.testseed = [900, 500, 900, 256].slice();\n                }\n            }\n            if (dbInbound) {\n                this.dbInbound = new DBInbound(dbInbound);\n            } else {\n                this.dbInbound = new DBInbound();\n            }\n            this.confirm = confirm;\n            this.visible = true;\n            this.isEdit = isEdit;\n        },\n        close() {\n            inModal.visible = false;\n            inModal.loading(false);\n        },\n        loading(loading = true) {\n            inModal.confirmLoading = loading;\n        },\n        // Vision Seed methods - always available regardless of Vue context\n        updateTestseed(index, value) {\n            // Use inModal.inbound explicitly to ensure correct context\n            if (!inModal.inbound || !inModal.inbound.settings) return;\n            // Ensure testseed is initialized\n            if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed)) {\n                inModal.inbound.settings.testseed = [900, 500, 900, 256];\n            }\n            // Ensure array has enough elements\n            while (inModal.inbound.settings.testseed.length <= index) {\n                inModal.inbound.settings.testseed.push(0);\n            }\n            // Update value\n            inModal.inbound.settings.testseed[index] = value;\n        },\n        setRandomTestseed() {\n            // Use inModal.inbound explicitly to ensure correct context\n            if (!inModal.inbound || !inModal.inbound.settings) return;\n            // Ensure testseed is initialized\n            if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4) {\n                inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();\n            }\n            // Create new array with random values\n            inModal.inbound.settings.testseed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];\n        },\n        resetTestseed() {\n            // Use inModal.inbound explicitly to ensure correct context\n            if (!inModal.inbound || !inModal.inbound.settings) return;\n            // Reset testseed to default values\n            inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();\n        }\n    };\n\n    // Store Vue instance globally to ensure methods are always accessible\n    let inboundModalVueInstance = null;\n    \n    inboundModalVueInstance = new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#inbound-modal',\n        data: {\n            inModal: inModal,\n            delayedStart: false,\n            get inbound() {\n                return inModal.inbound;\n            },\n            get dbInbound() {\n                return inModal.dbInbound;\n            },\n            get isEdit() {\n                return inModal.isEdit;\n            },\n            get client() {\n                return inModal.inbound && inModal.inbound.clients && inModal.inbound.clients.length > 0 ? inModal.inbound.clients[0] : null;\n            },\n            get datepicker() {\n                return app.datepicker;\n            },\n            get delayedExpireDays() {\n                return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;\n            },\n            set delayedExpireDays(days) {\n                this.client.expiryTime = -86400000 * days;\n            },\n            get externalProxy() {\n                return this.inbound.stream.externalProxy.length > 0;\n            },\n            set externalProxy(value) {\n                if (value) {\n                    inModal.inbound.stream.externalProxy = [{\n                        forceTls: \"same\",\n                        dest: window.location.hostname,\n                        port: inModal.inbound.port,\n                        remark: \"\"\n                    }];\n                } else {\n                    inModal.inbound.stream.externalProxy = [];\n                }\n            }\n        },\n        watch: {\n            'inModal.inbound.stream.security'(newVal, oldVal) {\n                // Clear flow when security changes from reality/tls to none\n                if (inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {\n                    inModal.inbound.settings.vlesses.forEach(client => {\n                        client.flow = \"\";\n                    });\n                }\n            },\n            // Ensure testseed is always initialized when vision flow is enabled\n            'inModal.inbound.settings.vlesses': {\n                handler() {\n                    if (inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.settings && inModal.inbound.settings.vlesses) {\n                        const hasVisionFlow = inModal.inbound.settings.vlesses.some(c => c.flow === 'xtls-rprx-vision' || c.flow === 'xtls-rprx-vision-udp443');\n                        if (hasVisionFlow && (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed) || inModal.inbound.settings.testseed.length < 4)) {\n                            inModal.inbound.settings.testseed = [900, 500, 900, 256];\n                        }\n                    }\n                },\n                deep: true\n            }\n        },\n        methods: {\n            streamNetworkChange() {\n                if (!inModal.inbound.canEnableTls()) {\n                    this.inModal.inbound.stream.security = 'none';\n                }\n                if (!inModal.inbound.canEnableReality()) {\n                    this.inModal.inbound.reality = false;\n                }\n                if (this.inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {\n                    this.inModal.inbound.settings.vlesses.forEach(client => {\n                        client.flow = \"\";\n                    });\n                }\n            },\n            SSMethodChange() {\n                this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)\n\n                if (this.inModal.inbound.isSSMultiUser) {\n                    if (this.inModal.inbound.settings.shadowsockses.length == 0) {\n                        this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];\n                    }\n                    if (!this.inModal.inbound.isSS2022) {\n                        this.inModal.inbound.settings.shadowsockses.forEach(client => {\n                            client.method = this.inModal.inbound.settings.method;\n                        })\n                    } else {\n                        this.inModal.inbound.settings.shadowsockses.forEach(client => {\n                            client.method = \"\";\n                        })\n                    }\n                    this.inModal.inbound.settings.shadowsockses.forEach(client => {\n                        client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)\n                    })\n                } else {\n                    if (this.inModal.inbound.settings.shadowsockses.length > 0) {\n                        this.inModal.inbound.settings.shadowsockses = [];\n                    }\n                }\n            },\n            setDefaultCertData(index) {\n                inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;\n                inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;\n            },\n            async getNewX25519Cert() {\n                inModal.loading(true);\n                const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');\n                inModal.loading(false);\n                if (!msg.success) {\n                    return;\n                }\n                inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;\n                inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;\n            },\n            clearX25519Cert() {\n                this.inbound.stream.reality.privateKey = '';\n                this.inbound.stream.reality.settings.publicKey = '';\n            },\n            async getNewmldsa65() {\n                inModal.loading(true);\n                const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');\n                inModal.loading(false);\n                if (!msg.success) {\n                    return;\n                }\n                inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;\n                inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;\n            },\n            clearMldsa65() {\n                this.inbound.stream.reality.mldsa65Seed = '';\n                this.inbound.stream.reality.settings.mldsa65Verify = '';\n            },\n            randomizeRealityTarget() {\n                if (typeof getRandomRealityTarget !== 'undefined') {\n                    const randomTarget = getRandomRealityTarget();\n                    this.inbound.stream.reality.target = randomTarget.target;\n                    this.inbound.stream.reality.serverNames = randomTarget.sni;\n                }\n            },\n            async getNewEchCert() {\n                inModal.loading(true);\n                const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });\n                inModal.loading(false);\n                if (!msg.success) {\n                    return;\n                }\n                inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;\n                inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;\n            },\n            clearEchCert() {\n                this.inbound.stream.tls.echServerKeys = '';\n                this.inbound.stream.tls.settings.echConfigList = '';\n            },\n            async getNewVlessEnc() {\n                inModal.loading(true);\n                const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');\n                inModal.loading(false);\n\n                if (!msg.success) {\n                    return;\n                }\n\n                const auths = msg.obj.auths || [];\n                const selected = inModal.inbound.settings.selectedAuth;\n                const block = auths.find(a => a.label === selected);\n\n                if (!block) {\n                    console.error(\"No auth block for\", selected);\n                    return;\n                }\n\n                inModal.inbound.settings.decryption = block.decryption;\n                inModal.inbound.settings.encryption = block.encryption;\n            },\n            clearVlessEnc() {\n                this.inbound.settings.decryption = 'none';\n                this.inbound.settings.encryption = 'none';\n                this.inbound.settings.selectedAuth = undefined;\n            },\n            // Vision Seed methods - must be in Vue methods for proper binding\n            updateTestseed(index, value) {\n                // Ensure testseed is initialized\n                if (!this.inbound.settings.testseed || !Array.isArray(this.inbound.settings.testseed)) {\n                    this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);\n                }\n                // Ensure array has enough elements\n                while (this.inbound.settings.testseed.length <= index) {\n                    this.inbound.settings.testseed.push(0);\n                }\n                // Update value using Vue.set for reactivity\n                this.$set(this.inbound.settings.testseed, index, value);\n            },\n            setRandomTestseed() {\n                // Create new array with random values and use Vue.set for reactivity\n                const newSeed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];\n                this.$set(this.inbound.settings, 'testseed', newSeed);\n            },\n            resetTestseed() {\n                // Reset testseed to default values using Vue.set for reactivity\n                this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);\n            }\n        },\n    });\n\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/prompt_modal.html",
    "content": "{{define \"modals/promptModal\"}}\n<a-modal id=\"prompt-modal\" v-model=\"promptModal.visible\" :title=\"promptModal.title\"\n         :closable=\"true\" @ok=\"promptModal.ok\" :mask-closable=\"false\"\n         :confirm-loading=\"promptModal.confirmLoading\"\n         :ok-text=\"promptModal.okText\" cancel-text='{{ i18n \"cancel\" }}' :class=\"themeSwitcher.currentTheme\">\n    <a-input id=\"prompt-modal-input\" :type=\"promptModal.type\"\n             v-model=\"promptModal.value\"\n             :autosize=\"{minRows: 10, maxRows: 20}\"\n             @keydown.enter.native=\"promptModal.keyEnter\"\n             @keydown.ctrl.83=\"promptModal.ctrlS\"></a-input>\n</a-modal>\n\n<script>\n\n    const promptModal = {\n        title: '',\n        type: '',\n        value: '',\n        okText: '{{ i18n \"sure\"}}',\n        visible: false,\n        confirmLoading: false,\n        keyEnter(e) {\n            if (this.type !== 'textarea') {\n                e.preventDefault();\n                this.ok();\n            }\n        },\n        ctrlS(e) {\n            if (this.type === 'textarea') {\n                e.preventDefault();\n                promptModal.confirm(promptModal.value);\n            }\n        },\n        ok() {\n            promptModal.confirm(promptModal.value);\n        },\n        confirm() {},\n        open({\n            title = '',\n            type = 'text',\n            value = '',\n            okText = '{{ i18n \"sure\"}}',\n            confirm = () => {},\n        }) {\n            this.title = title;\n            this.type = type;\n            this.value = value;\n            this.okText = okText;\n            this.confirm = confirm;\n            this.visible = true;\n            promptModalApp.$nextTick(() => {\n                document.querySelector('#prompt-modal-input').focus();\n            });\n        },\n        close() {\n            this.visible = false;\n        },\n        loading(loading=true) {\n            this.confirmLoading = loading;\n        },\n    };\n\n    const promptModalApp = new Vue({\n        el: '#prompt-modal',\n        data: {\n            promptModal: promptModal,\n        },\n    });\n\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/qrcode_modal.html",
    "content": "{{define \"modals/qrcodeModal\"}}\n<a-modal id=\"qrcode-modal\" v-model=\"qrModal.visible\" :closable=\"true\" :class=\"themeSwitcher.currentTheme\"\n  width=\"fit-content\" :dialog-style=\"isMobile ? { top: '18px' } : {}\" :footer=\"null\">\n  <template #title>\n    <a-space direction=\"horizontal\">\n      <span>[[ qrModal.title ]]</span>\n      <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\" trigger=\"click\" placement=\"bottom\">\n        <template slot=\"content\">\n          <a-space direction=\"vertical\">\n            <template v-for=\"(row, index) in qrModal.qrcodes\">\n              <b>[[ row.remark ]]</b>\n              <a-space direction=\"horizontal\">\n                <a-switch size=\"small\" :checked=\"row.useIPv4\" @click=\"toggleIPv4(index)\"></a-switch>\n                <span>{{ i18n \"useIPv4ForHost\" }}</span>\n              </a-space>\n            </template>\n          </a-space>\n        </template>\n        <a-icon type=\"setting\"></a-icon>\n      </a-popover>\n    </a-space>\n  </template>\n  <tr-qr-modal class=\"qr-modal\">\n    <template v-if=\"app.subSettings?.enable && qrModal.subId\">\n      <tr-qr-box class=\"qr-box\">\n        <a-tag color=\"purple\" class=\"qr-tag\"><span>{{ i18n \"pages.settings.subSettings\"}}</span></a-tag>\n        <tr-qr-bg class=\"qr-bg-sub\">\n          <tr-qr-bg-inner class=\"qr-bg-sub-inner\">\n            <canvas @click=\"copy(genSubLink(qrModal.client.subId))\" id=\"qrCode-sub\" class=\"qr-cv\"></canvas>\n          </tr-qr-bg-inner>\n        </tr-qr-bg>\n      </tr-qr-box>\n      <tr-qr-box class=\"qr-box\" v-if=\"app.subSettings.subJsonEnable\">\n        <a-tag color=\"purple\" class=\"qr-tag\"><span>{{ i18n \"pages.settings.subSettings\"}} Json</span></a-tag>\n        <tr-qr-bg class=\"qr-bg-sub\">\n          <tr-qr-bg-inner class=\"qr-bg-sub-inner\">\n            <canvas @click=\"copy(genSubJsonLink(qrModal.client.subId))\" id=\"qrCode-subJson\" class=\"qr-cv\"></canvas>\n          </tr-qr-bg-inner>\n        </tr-qr-bg>\n      </tr-qr-box>\n    </template>\n    <template v-for=\"(row, index) in qrModal.qrcodes\">\n      <tr-qr-box class=\"qr-box\">\n        <a-tag color=\"green\" class=\"qr-tag\"><span>[[ row.remark ]]</span></a-tag>\n        <tr-qr-bg class=\"qr-bg\">\n          <canvas @click=\"copy(row.link)\" :id=\"'qrCode-'+index\" class=\"qr-cv\"></canvas>\n        </tr-qr-bg>\n      </tr-qr-box>\n    </template>\n  </tr-qr-modal>\n</a-modal>\n\n<style>\n  .ant-table:not(.ant-table-expanded-row .ant-table) {\n    outline: 1px solid #f0f0f0;\n    outline-offset: -1px;\n    border-radius: 1rem;\n    overflow-x: hidden;\n  }\n  \n  /* QR code transition effects */\n  .qr-cv {\n    transition: all 0.3s ease-in-out;\n  }\n  \n  .qr-transition-enter-active, .qr-transition-leave-active {\n    transition: opacity 0.3s, transform 0.3s;\n  }\n  \n  .qr-transition-enter, .qr-transition-leave-to {\n    opacity: 0;\n    transform: scale(0.9);\n  }\n  \n  .qr-transition-enter-to, .qr-transition-leave {\n    opacity: 1;\n    transform: scale(1);\n  }\n  \n  .qr-flash {\n    animation: qr-flash-animation 0.6s;\n  }\n  \n  @keyframes qr-flash-animation {\n    0% {\n      opacity: 1;\n      transform: scale(1);\n    }\n    50% {\n      opacity: 0.5;\n      transform: scale(0.95);\n    }\n    100% {\n      opacity: 1;\n      transform: scale(1);\n    }\n  }\n</style>\n\n<script>\n  const qrModal = {\n    title: '',\n    dbInbound: new DBInbound(),\n    client: null,\n    qrcodes: [],\n    visible: false,\n    subId: '',\n    show: function (title = '', dbInbound, client) {\n      this.title = title;\n      this.dbInbound = dbInbound;\n      this.inbound = dbInbound.toInbound();\n      this.client = client;\n      this.subId = '';\n      this.qrcodes = [];\n      // Reset the status fetched flag when showing the modal\n      if (qrModalApp) qrModalApp.statusFetched = false;\n      if (this.inbound.protocol == Protocols.WIREGUARD) {\n        this.inbound.genInboundLinks(dbInbound.remark).split('\\r\\n').forEach((l, index) => {\n          this.qrcodes.push({\n            remark: \"Peer \" + (index + 1),\n            link: l,\n            useIPv4: false,\n            originalLink: l\n          });\n        });\n      } else {\n        this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {\n          this.qrcodes.push({\n            remark: l.remark,\n            link: l.link,\n            useIPv4: false,\n            originalLink: l.link\n          });\n        });\n      }\n      this.visible = true;\n    },\n    close: function () {\n      this.visible = false;\n    },\n  };\n  const qrModalApp = new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#qrcode-modal',\n    mixins: [MediaQueryMixin],\n    data: {\n      qrModal: qrModal,\n      serverStatus: null,\n      statusFetched: false,\n    },\n    methods: {\n      async getStatus() {\n        try {\n          const msg = await HttpUtil.get('/panel/api/server/status');\n          if (msg.success) {\n            this.serverStatus = msg.obj;\n          }\n        } catch (e) {\n          console.error(\"Failed to get status:\", e);\n        }\n      },\n      \n      toggleIPv4(index) {\n        const row = qrModal.qrcodes[index];\n        row.useIPv4 = !row.useIPv4;\n        this.updateLink(index);\n      },\n      updateLink(index) {\n        const row = qrModal.qrcodes[index];\n        if (!this.serverStatus || !this.serverStatus.publicIP) {\n          return;\n        }\n        \n        if (row.useIPv4 && this.serverStatus.publicIP.ipv4) {\n          // Replace the hostname or IP in the link with the IPv4 address\n          const originalLink = row.originalLink;\n          const url = new URL(originalLink);\n          const ipv4 = this.serverStatus.publicIP.ipv4;\n          \n          if (qrModal.inbound.protocol == Protocols.WIREGUARD) {\n            // Special handling for WireGuard config\n            const endpointRegex = /Endpoint = ([^:]+):(\\d+)/;\n            const match = originalLink.match(endpointRegex);\n            if (match) {\n              row.link = originalLink.replace(\n                `Endpoint = ${match[1]}:${match[2]}`,\n                `Endpoint = ${ipv4}:${match[2]}`\n              );\n            }\n          } else {\n            // For other protocols using URL format\n            url.hostname = ipv4;\n            row.link = url.toString();\n          }\n        } else {\n          // Restore original link\n          row.link = row.originalLink;\n        }\n        \n        // Update QR code with transition effect\n        const canvasElement = document.querySelector('#qrCode-' + index);\n        if (canvasElement) {\n          // Add flash animation class\n          canvasElement.classList.add('qr-flash');\n          \n          // Remove the class after animation completes\n          setTimeout(() => {\n            canvasElement.classList.remove('qr-flash');\n          }, 600);\n        }\n        \n        this.setQrCode(\"qrCode-\" + index, row.link);\n      },\n      copy(content) {\n        ClipboardManager\n          .copyText(content)\n          .then(() => {\n            app.$message.success('{{ i18n \"copied\" }}')\n          })\n      },\n      setQrCode(elementId, content) {\n        new QRious({\n          element: document.querySelector('#' + elementId),\n          size: 400,\n          value: content,\n          background: 'white',\n          backgroundAlpha: 0,\n          foreground: 'black',\n          padding: 2,\n          level: 'L'\n        });\n      },\n      genSubLink(subID) {\n        return app.subSettings.subURI + subID;\n      },\n      genSubJsonLink(subID) {\n        return app.subSettings.subJsonURI + subID;\n      },\n      revertOverflow() {\n        const elements = document.querySelectorAll(\".qr-tag\");\n        elements.forEach((element) => {\n          element.classList.remove(\"tr-marquee\");\n          element.children[0].style.animation = '';\n          while (element.children.length > 1) {\n            element.removeChild(element.lastChild);\n          }\n        });\n      }\n    },\n    updated() {\n      if (this.qrModal.visible) {\n        fixOverflow();\n        if (!this.statusFetched) {\n          this.getStatus();\n          this.statusFetched = true;\n        }\n      } else {\n        this.revertOverflow();\n        // Reset the flag when modal is closed so it will fetch again next time\n        this.statusFetched = false;\n      }\n      if (qrModal.client && qrModal.client.subId) {\n        qrModal.subId = qrModal.client.subId;\n        this.setQrCode(\"qrCode-sub\", this.genSubLink(qrModal.subId));\n        if (app.subSettings.subJsonEnable) {\n          this.setQrCode(\"qrCode-subJson\", this.genSubJsonLink(qrModal.subId));\n        }\n      }\n      qrModal.qrcodes.forEach((element, index) => {\n        this.setQrCode(\"qrCode-\" + index, element.link);\n        // Update links based on current toggle state\n        if (element.useIPv4 && this.serverStatus && this.serverStatus.publicIP) {\n          this.updateLink(index);\n        }\n      });\n    }\n  });\n\n  function fixOverflow() {\n    const elements = document.querySelectorAll(\".qr-tag\");\n    elements.forEach((element) => {\n      function isElementOverflowing(element) {\n        const overflowX = element.offsetWidth < element.scrollWidth,\n          overflowY = element.offsetHeight < element.scrollHeight;\n        return overflowX || overflowY;\n      }\n\n      function wrapContentsInMarquee(element) {\n        element.classList.add(\"tr-marquee\");\n        element.children[0].style.animation = `move-ltr ${(element.children[0].clientWidth / element.clientWidth) * 5\n          }s ease-in-out infinite`;\n        const marqueeText = element.children[0];\n        if (element.children.length < 2) {\n          for (let i = 0; i < 1; i++) {\n            const marqueeText = element.children[0].cloneNode(true);\n            element.children[0].after(marqueeText);\n          }\n        }\n      }\n      if (isElementOverflowing(element)) {\n        wrapContentsInMarquee(element);\n      }\n    });\n  }\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/text_modal.html",
    "content": "{{define \"modals/textModal\"}}\n<a-modal id=\"text-modal\" v-model=\"txtModal.visible\" :title=\"txtModal.title\" :closable=\"true\"\n    :class=\"themeSwitcher.currentTheme\">\n    <a-input :style=\"{ overflowY: 'auto' }\" type=\"textarea\" v-model=\"txtModal.content\"\n        :autosize=\"{ minRows: 10, maxRows: 20}\"></a-input>\n    <template slot=\"footer\">\n        <a-button v-if=\"!ObjectUtil.isEmpty(txtModal.fileName)\" icon=\"download\"\n            @click=\"FileManager.downloadTextFile(txtModal.content, txtModal.fileName)\">\n            <span>[[ txtModal.fileName ]]</span>\n        </a-button>\n        <a-button type=\"primary\" icon=\"copy\" @click=\"txtModal.copy(txtModal.content)\">\n            <span>{{ i18n \"copy\" }}</span>\n        </a-button>\n    </template>\n</a-modal>\n\n<script>\n    const txtModal = {\n        title: '',\n        content: '',\n        fileName: '',\n        qrcode: null,\n        visible: false,\n        show: function (title = '', content = '', fileName = '') {\n            this.title = title;\n            this.content = content;\n            this.fileName = fileName;\n            this.visible = true;\n        },\n        copy: function (content = '') {\n            ClipboardManager\n                .copyText(content)\n                .then(() => {\n                    app.$message.success('{{ i18n \"copied\" }}')\n                    this.close();\n                })\n        },\n        close: function () {\n            this.visible = false;\n        },\n    };\n\n    const textModalApp = new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#text-modal',\n        data: {\n            txtModal: txtModal,\n        },\n    });\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/two_factor_modal.html",
    "content": "{{define \"modals/twoFactorModal\"}}\n<a-modal id=\"two-factor-modal\" v-model=\"twoFactorModal.visible\" :title=\"twoFactorModal.title\" :closable=\"true\"\n    :class=\"themeSwitcher.currentTheme\">\n    <template v-if=\"twoFactorModal.type === 'set'\">\n        <p>{{ i18n \"pages.settings.security.twoFactorModalSteps\" }}</p>\n        <a-divider></a-divider>\n        <p>{{ i18n \"pages.settings.security.twoFactorModalFirstStep\" }}</p>\n        <div :style=\"{ display: 'flex', alignItems: 'center', flexDirection: 'column', gap: '12px' }\">\n            <div class=\"qr-bg\" :style=\"{ width: '180px', height: '180px' }\">\n                <canvas @click=\"copy(twoFactorModal.token)\" id=\"twofactor-qrcode\" class=\"qr-cv\"></canvas>\n            </div>\n            <span :style=\"{ fontSize: '12px', fontFamily: 'monospace' }\">[[ twoFactorModal.token ]]</span>\n        </div>\n        <a-divider></a-divider>\n        <p>{{ i18n \"pages.settings.security.twoFactorModalSecondStep\" }}</p>\n        <a-input v-model.trim=\"twoFactorModal.enteredCode\" :style=\"{ width: '100%' }\"></a-input>\n    </template>\n    <template v-if=\"twoFactorModal.type === 'confirm'\">\n        <p>[[ twoFactorModal.description ]]</p>\n        <a-input v-model.trim=\"twoFactorModal.enteredCode\" :style=\"{ width: '100%' }\"></a-input>\n    </template>\n    <template slot=\"footer\">\n        <a-button @click=\"twoFactorModal.cancel\">\n            <span>{{ i18n \"cancel\" }}</span>\n        </a-button>\n        <a-button type=\"primary\" :disabled=\"twoFactorModal.enteredCode.length < 6\" @click=\"twoFactorModal.ok\">\n            <span>{{ i18n \"confirm\" }}</span>\n        </a-button>\n    </template>\n</a-modal>\n\n<script>\n    const twoFactorModal = {\n        title: '',\n        description: '',\n        fileName: '',\n        token: '',\n        enteredCode: '',\n        visible: false,\n        type: 'set',\n        confirm: null,\n        totpObject: null,\n        qrImage: \"\",\n        ok() {\n            if (twoFactorModal.totpObject.generate() === twoFactorModal.enteredCode) {\n                ObjectUtil.execute(twoFactorModal.confirm, true)\n\n                twoFactorModal.close()\n            } else {\n                Vue.prototype.$message['error']('{{ i18n \"pages.settings.security.twoFactorModalError\" }}')\n            }\n        },\n        cancel() {\n            ObjectUtil.execute(twoFactorModal.confirm, false)\n\n            twoFactorModal.close()\n        },\n        show: function ({\n            title = '',\n            description = '',\n            token = '',\n            type = 'set',\n            confirm = (success) => { }\n        }) {\n            this.title = title;\n            this.description = description;\n            this.token = token;\n            this.visible = true;\n            this.confirm = confirm;\n            this.type = type;\n\n            this.totpObject = new OTPAuth.TOTP({\n                issuer: \"3x-ui\",\n                label: \"Administrator\",\n                algorithm: \"SHA1\",\n                digits: 6,\n                period: 30,\n                secret: twoFactorModal.token,\n            });\n        },\n        close: function () {\n            twoFactorModal.enteredCode = \"\";\n            twoFactorModal.visible = false;\n        },\n    };\n\n    const twoFactorModalApp = new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#two-factor-modal',\n        data: {\n            twoFactorModal: twoFactorModal,\n        },\n        updated() {\n          if (\n            this.twoFactorModal.visible &&\n            this.twoFactorModal.type === 'set' &&\n            document.getElementById('twofactor-qrcode')\n          ) {\n            this.setQrCode('twofactor-qrcode', this.twoFactorModal.totpObject.toString());\n          }\n        },\n        methods: {\n          setQrCode(elementId, content) {\n            new QRious({\n              element: document.getElementById(elementId),\n              size: 200,\n              value: content,\n              background: 'white',\n              backgroundAlpha: 0,\n              foreground: 'black',\n              padding: 2,\n              level: 'L'\n            });\n          },\n          copy(content) {\n            ClipboardManager\n              .copyText(content)\n              .then(() => {\n                app.$message.success('{{ i18n \"copied\" }}')\n              })\n          },\n        }\n    });\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/warp_modal.html",
    "content": "{{define \"modals/warpModal\"}}\n<a-modal id=\"warp-modal\" v-model=\"warpModal.visible\" title=\"Cloudflare WARP\"\n         :confirm-loading=\"warpModal.confirmLoading\" :closable=\"true\" :mask-closable=\"true\"\n         :footer=\"null\" :class=\"themeSwitcher.currentTheme\">\n    <template v-if=\"ObjectUtil.isEmpty(warpModal.warpData)\">\n        <a-button icon=\"api\" @click=\"register\" :loading=\"warpModal.confirmLoading\">{{ i18n \"create\" }}</a-button>\n    </template>\n    <template v-else>\n        <table :style=\"{ margin: '5px 0', width: '100%' }\">\n            <tr class=\"client-table-odd-row\">\n                <td>Access Token</td>\n                <td>[[ warpModal.warpData.access_token ]]</td>\n            </tr>\n            <tr>\n                <td>Device ID</td>\n                <td>[[ warpModal.warpData.device_id ]]</td>\n            </tr>\n            <tr class=\"client-table-odd-row\">\n                <td>License Key</td>\n                <td>[[ warpModal.warpData.license_key ]]</td>\n            </tr>\n            <tr>\n                <td>Private Key</td>\n                <td>[[ warpModal.warpData.private_key ]]</td>\n            </tr>\n        </table>\n        <a-button @click=\"delConfig\" :loading=\"warpModal.confirmLoading\" type=\"danger\">{{ i18n \"delete\" }}</a-button>\n        <a-divider :style=\"{ margin: '0' }\">{{ i18n \"pages.xray.outbound.settings\" }}</a-divider>\n        <a-collapse :style=\"{ margin: '10px 0' }\">\n            <a-collapse-panel header='WARP/WARP+ License Key'>\n                <a-form :colon=\"false\" :label-col=\"{ md: {span:6} }\" :wrapper-col=\"{ md: {span:14} }\">\n                    <a-form-item label=\"Key\">\n                        <a-input v-model=\"warpPlus\"></a-input>\n                        <a-button @click=\"updateLicense(warpPlus)\" :disabled=\"warpPlus.length<26\"\n                            :loading=\"warpModal.confirmLoading\">{{ i18n \"update\" }}</a-button>\n                    </a-form-item>\n                </a-form>\n            </a-collapse-panel>\n        </a-collapse>\n        <a-divider :style=\"{ margin: '0' }\">{{ i18n \"pages.xray.outbound.accountInfo\" }}</a-divider>\n        <a-button icon=\"sync\" @click=\"getConfig\" :style=\"{ marginTop: '5px', marginBottom: '10px' }\"\n            :loading=\"warpModal.confirmLoading\" type=\"primary\">{{ i18n \"info\" }}</a-button>\n        <template v-if=\"!ObjectUtil.isEmpty(warpModal.warpConfig)\">\n            <table :style=\"{ width: '100%' }\">\n                <tr class=\"client-table-odd-row\">\n                    <td>Device Name</td>\n                    <td>[[ warpModal.warpConfig.name ]]</td>\n                </tr>\n                <tr>\n                    <td>Device Model</td>\n                    <td>[[ warpModal.warpConfig.model ]]</td>\n                </tr>\n                <tr class=\"client-table-odd-row\">\n                    <td>Device Enabled</td>\n                    <td>[[ warpModal.warpConfig.enabled ]]</td>\n                </tr>\n                <template v-if=\"!ObjectUtil.isEmpty(warpModal.warpConfig.account)\">\n                    <tr>\n                        <td>Account Type</td>\n                        <td>[[ warpModal.warpConfig.account.account_type ]]</td>\n                    </tr>\n                    <tr class=\"client-table-odd-row\">\n                        <td>Role</td>\n                        <td>[[ warpModal.warpConfig.account.role ]]</td>\n                    </tr>\n                    <tr>\n                        <td>WARP+ Data</td>\n                        <td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.premium_data) ]]</td>\n                    </tr>\n                    <tr class=\"client-table-odd-row\">\n                        <td>Quota</td>\n                        <td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.quota) ]]</td>\n                    </tr>\n                    <tr v-if=\"!ObjectUtil.isEmpty(warpModal.warpConfig.account.usage)\">\n                        <td>Usage</td>\n                        <td>[[ SizeFormatter.sizeFormat(warpModal.warpConfig.account.usage) ]]</td>\n                    </tr>\n                </template>\n            </table>\n            <a-divider :style=\"{ margin: '10px 0' }\">{{ i18n \"pages.xray.outbound.outboundStatus\" }}</a-divider>\n            <a-form :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n                <template v-if=\"warpOutboundIndex>=0\">\n                    <a-tag color=\"green\" :style=\"{ lineHeight: '31px' }\">{{ i18n \"enabled\" }}</a-tag>\n                    <a-button @click=\"resetOutbound\" :loading=\"warpModal.confirmLoading\" type=\"danger\">{{ i18n \"reset\" }}</a-button>\n                </template>\n                <template v-else>\n                    <a-tag color=\"orange\" :style=\"{ lineHeight: '31px' }\">{{ i18n \"disabled\" }}</a-tag>\n                    <a-button @click=\"addOutbound\" :loading=\"warpModal.confirmLoading\" type=\"primary\">{{ i18n \"pages.xray.outbound.addOutbound\" }}</a-button>\n                </template>\n                </a-form-item>\n            </a-form>\n        </template>\n    </template>\n</a-modal>\n<script>\n\n    const warpModal = {\n        visible: false,\n        confirmLoading: false,\n        warpData: null,\n        warpConfig: null,\n        warpOutbound: null,\n        show() {\n            this.visible = true;\n            this.warpConfig = null;\n            this.getData();\n        },\n        close() {\n            this.visible = false;\n            this.loading(false);\n        },\n        loading(loading = true) {\n            this.confirmLoading = loading;\n        },\n        async getData() {\n            this.loading(true);\n            const msg = await HttpUtil.post('/panel/xray/warp/data');\n            this.loading(false);\n            if (msg.success) {\n                this.warpData = msg.obj.length > 0 ? JSON.parse(msg.obj) : null;\n            }\n        },\n    };\n\n    new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#warp-modal',\n        data: {\n            warpModal: warpModal,\n            warpPlus: '',\n        },\n        methods: {\n            collectConfig() {\n                config = warpModal.warpConfig.config;\n                peer = config.peers[0];\n                if (config) {\n                    warpModal.warpOutbound = Outbound.fromJson({\n                        tag: 'warp',\n                        protocol: Protocols.Wireguard,\n                        settings: {\n                            mtu: 1420,\n                            secretKey: warpModal.warpData.private_key,\n                            address: this.getAddresses(config.interface.addresses),\n                            reserved: this.getResolved(config.client_id),\n                            domainStrategy: 'ForceIP',\n                            peers: [{\n                                publicKey: peer.public_key,\n                                endpoint: peer.endpoint.host,\n                            }],\n                            noKernelTun: false,\n                        }\n                    });\n                }\n            },\n            getAddresses(addrs) {\n                let addresses = [];\n                if (addrs.v4) addresses.push(addrs.v4 + \"/32\");\n                if (addrs.v6) addresses.push(addrs.v6 + \"/128\");\n                return addresses;\n            },\n            getResolved(client_id) {\n                let reserved = [];\n                let decoded = atob(client_id);\n                let hexString = '';\n                for (let i = 0; i < decoded.length; i++) {\n                    let hex = decoded.charCodeAt(i).toString(16);\n                    hexString += (hex.length === 1 ? '0' : '') + hex;\n                }\n\n                for (let i = 0; i < hexString.length; i += 2) {\n                    let hexByte = hexString.slice(i, i + 2);\n                    let decValue = parseInt(hexByte, 16);\n                    reserved.push(decValue);\n                }\n                return reserved;\n            },\n            async register() {\n                warpModal.loading(true);\n                const keys = Wireguard.generateKeypair();\n                const msg = await HttpUtil.post('/panel/xray/warp/reg', keys);\n                if (msg.success) {\n                    const resp = JSON.parse(msg.obj);\n                    warpModal.warpData = resp.data;\n                    warpModal.warpConfig = resp.config;\n                    this.collectConfig();\n                }\n                warpModal.loading(false);\n            },\n            async updateLicense(l) {\n                warpModal.loading(true);\n                const msg = await HttpUtil.post('/panel/xray/warp/license', { license: l });\n                if (msg.success) {\n                    warpModal.warpData = JSON.parse(msg.obj);\n                    warpModal.warpConfig = null;\n                    this.warpPlus = '';\n                }\n                warpModal.loading(false);\n            },\n            async getConfig() {\n                warpModal.loading(true);\n                const msg = await HttpUtil.post('/panel/xray/warp/config');\n                warpModal.loading(false);\n                if (msg.success) {\n                    warpModal.warpConfig = JSON.parse(msg.obj);\n                    this.collectConfig();\n                }\n            },\n            async delConfig() {\n                warpModal.loading(true);\n                const msg = await HttpUtil.post('/panel/xray/warp/del');\n                warpModal.loading(false);\n                if (msg.success) {\n                    warpModal.warpData = null;\n                    warpModal.warpConfig = null;\n                    this.delOutbound();\n                }\n            },\n            addOutbound() {\n                app.templateSettings.outbounds.push(warpModal.warpOutbound.toJson());\n                app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);\n                warpModal.close();\n            },\n            resetOutbound() {\n                app.templateSettings.outbounds[this.warpOutboundIndex] = warpModal.warpOutbound.toJson();\n                app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);\n                warpModal.close();\n            },\n            delOutbound() {\n                if (this.warpOutboundIndex != -1) {\n                    app.templateSettings.outbounds.splice(this.warpOutboundIndex, 1);\n                    app.outboundSettings = JSON.stringify(app.templateSettings.outbounds);\n                }\n                warpModal.close();\n            }\n        },\n        computed: {\n            warpOutboundIndex: {\n                get: function () {\n                    return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag == 'warp') : -1;\n                }\n            }\n        }\n    });\n\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/xray_balancer_modal.html",
    "content": "{{define \"modals/balancerModal\"}}\n<a-modal \n    id=\"balancer-modal\"\n    v-model=\"balancerModal.visible\"\n    :title=\"balancerModal.title\"\n    @ok=\"balancerModal.ok\"\n    :confirm-loading=\"balancerModal.confirmLoading\"\n    :ok-button-props=\"{ props: { disabled: !balancerModal.isValid } }\"\n    :closable=\"true\"\n    :mask-closable=\"false\"\n    :ok-text=\"balancerModal.okText\"\n    cancel-text='{{ i18n \"close\" }}'\n    :class=\"themeSwitcher.currentTheme\">\n    <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n        <a-form-item label='{{ i18n \"pages.xray.balancer.tag\" }}' has-feedback\n            :validate-status=\"balancerModal.duplicateTag? 'warning' : 'success'\">\n            <a-input v-model.trim=\"balancerModal.balancer.tag\" @change=\"balancerModal.check()\"\n                placeholder='{{ i18n \"pages.xray.balancer.tagDesc\" }}'></a-input>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.xray.balancer.balancerStrategy\" }}'>\n            <a-select v-model=\"balancerModal.balancer.strategy\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option value=\"random\">Random</a-select-option>\n                <a-select-option value=\"roundRobin\">Round Robin</a-select-option>\n                <a-select-option value=\"leastLoad\">Least Load</a-select-option>\n                <a-select-option value=\"leastPing\">Least Ping</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.xray.balancer.balancerSelectors\" }}' has-feedback\n            :validate-status=\"balancerModal.emptySelector? 'warning' : 'success'\">\n            <a-select v-model=\"balancerModal.balancer.selector\" mode=\"tags\" @change=\"balancerModal.checkSelector()\"\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option v-for=\"tag in balancerModal.outboundTags\" :value=\"tag\">[[ tag ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label=\"Fallback\">\n            <a-select v-model=\"balancerModal.balancer.fallbackTag\" clearable\n                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option v-for=\"tag in [ '', ...balancerModal.outboundTags]\" :value=\"tag\">[[ tag ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        </table>\n    </a-form>\n</a-modal>\n<script>\n    const balancerModal = {\n        title: '',\n        visible: false,\n        confirmLoading: false,\n        okText: '{{ i18n \"sure\" }}',\n        isEdit: false,\n        confirm: null,\n        duplicateTag: false,\n        emptySelector: false,\n        balancer: {\n            tag: '',\n            strategy: 'random',\n            selector: [],\n            fallbackTag: ''\n        },\n        outboundTags: [],\n        balancerTags:[],\n        ok() {\n            if (balancerModal.balancer.selector.length == 0) {\n                balancerModal.emptySelector = true;\n                return;\n            }\n            balancerModal.emptySelector = false;\n            ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);\n        },\n        show({ title = '', okText = '{{ i18n \"sure\" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) {\n            this.title = title;\n            this.okText = okText;\n            this.confirm = confirm;\n            this.visible = true;\n            if (isEdit) {\n                balancerModal.balancer = balancer;\n            } else {\n                balancerModal.balancer = {\n                    tag: '',\n                    strategy: 'random',\n                    selector: [],\n                    fallbackTag: ''\n                };\n            }\n            this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);\n            this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);\n            this.isEdit = isEdit;\n            this.check();\n            this.checkSelector();\n        },\n        close() {\n            this.visible = false;\n            this.loading(false);\n        },\n        loading(loading=true) {\n            this.confirmLoading = loading;\n        },\n        check() {\n            if (this.balancer.tag == '' || this.balancerTags.includes(this.balancer.tag)) {\n                this.duplicateTag = true;\n                this.isValid = false;\n            } else {\n                this.duplicateTag = false;\n                this.isValid = true;\n            }\n        },\n        checkSelector() {\n            this.emptySelector = this.balancer.selector.length == 0;\n        }\n    };\n\n    new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#balancer-modal',\n        data: {\n            balancerModal: balancerModal\n        },\n        methods: {\n        }\n    });\n\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/xray_dns_modal.html",
    "content": "{{define \"modals/dnsModal\"}}\n<a-modal id=\"dns-modal\" v-model=\"dnsModal.visible\" :title=\"dnsModal.title\" @ok=\"dnsModal.ok\" :closable=\"true\"\n  :mask-closable=\"false\" :ok-text=\"dnsModal.okText\" cancel-text='{{ i18n \"close\" }}'\n  :class=\"themeSwitcher.currentTheme\">\n  <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='{{ i18n \"pages.xray.outbound.address\" }}'>\n      <a-input v-model.trim=\"dnsModal.dnsServer.address\"></a-input>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.inbounds.port\" }}'>\n      <a-input-number v-model.number=\"dnsModal.dnsServer.port\" :min=\"1\" :max=\"65535\"></a-input-number>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.xray.dns.strategy\" }}'>\n      <a-select v-model=\"dnsModal.dnsServer.queryStrategy\" :style=\"{ width: '100%' }\"\n        :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option :value=\"l\" :label=\"l\" v-for=\"l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']\"> [[ l ]]\n        </a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-divider :style=\"{ margin: '5px 0' }\"></a-divider>\n    <a-form-item label='{{ i18n \"pages.xray.dns.domains\" }}'>\n      <a-button icon=\"plus\" size=\"small\" type=\"primary\" @click=\"dnsModal.dnsServer.domains.push('')\"></a-button>\n      <template v-for=\"(domain, index) in dnsModal.dnsServer.domains\">\n        <a-input v-model.trim=\"dnsModal.dnsServer.domains[index]\">\n          <a-button icon=\"minus\" size=\"small\" slot=\"addonAfter\"\n            @click=\"dnsModal.dnsServer.domains.splice(index,1)\"></a-button>\n        </a-input>\n      </template>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.xray.dns.expectIPs\"}}'>\n      <a-button icon=\"plus\" size=\"small\" type=\"primary\" @click=\"dnsModal.dnsServer.expectIPs.push('')\"></a-button>\n      <template v-for=\"(domain, index) in dnsModal.dnsServer.expectIPs\">\n        <a-input v-model.trim=\"dnsModal.dnsServer.expectIPs[index]\">\n          <a-button icon=\"minus\" size=\"small\" slot=\"addonAfter\"\n            @click=\"dnsModal.dnsServer.expectIPs.splice(index,1)\"></a-button>\n        </a-input>\n      </template>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.xray.dns.unexpectIPs\"}}'>\n      <a-button icon=\"plus\" size=\"small\" type=\"primary\" @click=\"dnsModal.dnsServer.unexpectedIPs.push('')\"></a-button>\n      <template v-for=\"(domain, index) in dnsModal.dnsServer.unexpectedIPs\">\n        <a-input v-model.trim=\"dnsModal.dnsServer.unexpectedIPs[index]\">\n          <a-button icon=\"minus\" size=\"small\" slot=\"addonAfter\"\n            @click=\"dnsModal.dnsServer.unexpectedIPs.splice(index,1)\"></a-button>\n        </a-input>\n      </template>\n    </a-form-item>\n    <a-divider :style=\"{ margin: '5px 0' }\"></a-divider>\n    <a-form-item label='Skip Fallback'>\n      <a-switch v-model=\"dnsModal.dnsServer.skipFallback\"></a-switch>\n    </a-form-item>\n    <a-form-item label='Disable Cache'>\n      <a-switch v-model=\"dnsModal.dnsServer.disableCache\"></a-switch>\n    </a-form-item>\n    <a-form-item label='Final Query'>\n      <a-switch v-model=\"dnsModal.dnsServer.finalQuery\"></a-switch>\n    </a-form-item>\n  </a-form>\n</a-modal>\n<script>\n  const defaultDnsObject = {\n    address: \"localhost\",\n    port: 53,\n    domains: [],\n    expectIPs: [],\n    unexpectedIPs: [],\n    queryStrategy: 'UseIP',\n    skipFallback: true,\n    disableCache: false,\n    finalQuery: false\n  }\n\n  const dnsModal = {\n    title: '',\n    visible: false,\n    okText: '{{ i18n \"confirm\" }}',\n    isEdit: false,\n    confirm: null,\n    dnsServer: { ...defaultDnsObject },\n    ok() {\n      ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer });\n    },\n    show({\n      title = '',\n      okText = '{{ i18n \"confirm\" }}',\n      dnsServer,\n      confirm = (dnsServer) => { },\n      isEdit = false\n    }) {\n      this.title = title;\n      this.okText = okText;\n      this.confirm = confirm;\n      this.visible = true;\n      this.isEdit = isEdit;\n\n      if (isEdit) {\n        switch (typeof dnsServer) {\n          case 'string':\n            const dnsObj = { ...defaultDnsObject };\n\n            dnsObj.address = dnsServer;\n\n            this.dnsServer = dnsObj;\n            break;\n          case 'object':\n            this.dnsServer = dnsServer;\n            break;\n        }\n      } else {\n        this.dnsServer = { ...defaultDnsObject };\n\n        this.dnsServer.domains = [];\n        this.dnsServer.expectIPs = [];\n        this.dnsServer.unexpectedIPs = [];\n      }\n    },\n    close() {\n      dnsModal.visible = false;\n    },\n  };\n  new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#dns-modal',\n    data: {\n      dnsModal: dnsModal,\n    }\n  });\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/xray_fakedns_modal.html",
    "content": "{{define \"modals/fakednsModal\"}}\n<a-modal id=\"fakedns-modal\" v-model=\"fakednsModal.visible\" :title=\"fakednsModal.title\" @ok=\"fakednsModal.ok\"\n  :closable=\"true\" :mask-closable=\"false\" :ok-text=\"fakednsModal.okText\" cancel-text='{{ i18n \"close\" }}'\n  :class=\"themeSwitcher.currentTheme\">\n  <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item label='{{ i18n \"pages.xray.fakedns.ipPool\" }}'>\n      <a-input v-model.trim=\"fakednsModal.fakeDns.ipPool\"></a-input>\n    </a-form-item>\n    <a-form-item label='{{ i18n \"pages.xray.fakedns.poolSize\" }}'>\n      <a-input-number v-model.number=\"fakednsModal.fakeDns.poolSize\" :min=\"1\"></a-input-number>\n    </a-form-item>\n  </a-form>\n</a-modal>\n<script>\n  const fakednsDefaultData = {\n    ipPool: \"198.18.0.0/16\",\n    poolSize: 65535,\n  }\n\n  const fakednsModal = {\n    title: '',\n    visible: false,\n    okText: '{{ i18n \"confirm\" }}',\n    isEdit: false,\n    confirm: null,\n    fakeDns: { ...fakednsDefaultData },\n    ok() {\n      ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);\n    },\n    show({ title = '', okText = '{{ i18n \"confirm\" }}', fakeDns, confirm = (fakeDns) => { }, isEdit = false }) {\n      this.title = title;\n      this.okText = okText;\n      this.confirm = confirm;\n      this.visible = true;\n      if (isEdit) {\n        this.fakeDns = fakeDns;\n      } else {\n        this.fakeDns = { ...fakednsDefaultData }\n      }\n      this.isEdit = isEdit;\n    },\n    close() {\n      fakednsModal.visible = false;\n    },\n  };\n\n  new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#fakedns-modal',\n    data: {\n      fakednsModal: fakednsModal,\n    }\n  });\n\n</script>\n{{end}}"
  },
  {
    "path": "web/html/modals/xray_outbound_modal.html",
    "content": "{{define \"modals/outModal\"}}\n<a-modal id=\"out-modal\" v-model=\"outModal.visible\" :title=\"outModal.title\" @ok=\"outModal.ok\"\n         :confirm-loading=\"outModal.confirmLoading\" :closable=\"true\" :mask-closable=\"false\"\n         :ok-button-props=\"{ props: { disabled: !outModal.isValid } }\" :style=\"{ overflow: 'hidden' }\"\n         :ok-text=\"outModal.okText\" cancel-text='{{ i18n \"close\" }}' :class=\"themeSwitcher.currentTheme\">\n         {{template \"form/outbound\"}}\n</a-modal>\n<script>\n\n    const outModal = {\n        title: '',\n        visible: false,\n        confirmLoading: false,\n        okText: '{{ i18n \"sure\" }}',\n        isEdit: false,\n        confirm: null,\n        outbound: new Outbound(),\n        jsonMode: false,\n        link: '',\n        cm: null,\n        duplicateTag: false,\n        isValid: true,\n        activeKey: '1',\n        tags: [],\n        ok() {\n            ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());\n        },\n        show({ title='', okText='{{ i18n \"sure\" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[]  }) {\n            this.title = title;\n            this.okText = okText;\n            this.confirm = confirm;\n            this.jsonMode = false;\n            this.link = '';\n            this.activeKey = '1';\n            this.visible = true;\n            this.outbound = isEdit ? Outbound.fromJson(outbound) : new Outbound();\n            this.isEdit = isEdit;\n            this.tags = tags;\n            this.check()\n        },\n        close() {\n            outModal.visible = false;\n            outModal.loading(false);\n        },\n        loading(loading=true) {\n            outModal.confirmLoading = loading;\n        },\n        check(){\n            if(outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)){\n                this.duplicateTag = true;\n                this.isValid = false;\n            } else {\n                this.duplicateTag = false;\n                this.isValid = true;\n            }\n        },\n        toggleJson(jsonTab) {\n            textAreaObj = document.getElementById('outboundJson');\n            if(jsonTab){\n                if(this.cm != null) {\n                        this.cm.toTextArea();\n                        this.cm=null;\n                }\n                textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);\n                this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions);\n                this.cm.on('change',editor => {\n                    value = editor.getValue();\n                    if(this.isJsonString(value)){\n                        this.outbound = Outbound.fromJson(JSON.parse(value));\n                        this.check();\n                    }\n                });\n                this.activeKey = '2';\n            } else {\n                if(this.cm != null) {\n                        this.cm.toTextArea();\n                        this.cm=null;\n                }\n                this.activeKey = '1';\n            }\n        },\n        isJsonString(str) {\n            try {\n                JSON.parse(str);\n            } catch (e) {\n                return false;\n            }\n            return true;\n        },\n    };\n\n    new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#out-modal',\n        data: {\n            outModal: outModal,\n            get outbound() {\n                return outModal.outbound;\n            },\n        },\n        methods: {\n            streamNetworkChange() {\n                if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound.canEnableTlsFlow()) {\n                    delete this.outModal.outbound.settings.flow;\n                }\n            },\n            canEnableTls() {\n                return this.outModal.outbound.canEnableTls();\n            },\n            convertLink(){\n                newOutbound = Outbound.fromLink(outModal.link);\n                if(newOutbound){\n                    this.outModal.outbound = newOutbound;\n                    this.outModal.toggleJson(true);\n                    this.outModal.check();\n                    this.$message.success('Link imported successfully...');      \n                    outModal.link = '';\n                } else {\n                    this.$message.error('Wrong Link!');\n                    outModal.link = '';\n                }\n            },\n        },\n    });\n\n</script>\n{{end}}\n"
  },
  {
    "path": "web/html/modals/xray_reverse_modal.html",
    "content": "{{define \"modals/reverseModal\"}}\n<a-modal id=\"reverse-modal\" v-model=\"reverseModal.visible\" :title=\"reverseModal.title\" @ok=\"reverseModal.ok\"\n         :confirm-loading=\"reverseModal.confirmLoading\" :closable=\"true\" :mask-closable=\"false\"\n         :ok-text=\"reverseModal.okText\" cancel-text='{{ i18n \"close\" }}' :class=\"themeSwitcher.currentTheme\">\n    <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n        <a-form-item label='{{ i18n \"pages.xray.outbound.type\" }}'>\n            <a-select v-model=\"reverseModal.reverse.type\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option v-for=\"x,y in reverseTypes\" :value=\"y\">[[ x ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.xray.outbound.tag\" }}'>\n            <a-input v-model.trim=\"reverseModal.reverse.tag\"></a-input>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.xray.outbound.domain\" }}'>\n            <a-input v-model.trim=\"reverseModal.reverse.domain\"></a-input>\n        </a-form-item>\n        <template v-if=\"reverseModal.reverse.type=='bridge'\">\n        <a-form-item label='{{ i18n \"pages.xray.outbound.intercon\" }}'>\n            <a-select v-model=\"reverseModal.rules[0].outboundTag\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option v-for=\"x in reverseModal.outboundTags\" :value=\"x\">[[ x ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        <a-form-item label='{{ i18n \"pages.xray.rules.outbound\" }}'>\n            <a-select v-model=\"reverseModal.rules[1].outboundTag\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                <a-select-option v-for=\"x in reverseModal.outboundTags\" :value=\"x\">[[ x ]]</a-select-option>\n            </a-select>\n        </a-form-item>\n        </template>\n        <template v-else>\n            <a-form-item label='{{ i18n \"pages.xray.outbound.intercon\" }}'>\n                <a-checkbox-group\n                    v-model=\"reverseModal.rules[0].inboundTag\"\n                    :options=\"reverseModal.inboundTags\"></a-checkbox-group>\n            </a-form-item>\n            <a-form-item label='{{ i18n \"pages.xray.rules.inbound\" }}'>\n                <a-checkbox-group\n                    v-model=\"reverseModal.rules[1].inboundTag\"\n                    :options=\"reverseModal.inboundTags\"></a-checkbox-group>\n            </a-form-item>\n        </template>\n    </a-form>\n</a-modal>\n<script>\n    const reverseModal = {\n        title: '',\n        visible: false,\n        confirmLoading: false,\n        okText: '{{ i18n \"sure\" }}',\n        isEdit: false,\n        confirm: null,\n        reverse: {\n            tag: \"\",\n            type: \"\",\n            domain: \"\"\n        },\n        rules: [\n            { outboundTag: '', inboundTag: []},\n            { outboundTag: '', inboundTag: []}\n        ],\n        inboundTags: [],\n        outboundTags: [],\n        ok() {\n            reverseModal.rules[0].domain = [\"full:\" + reverseModal.reverse.domain];\n            reverseModal.rules[0].type = 'field';\n            reverseModal.rules[1].type = 'field';\n\n            if(reverseModal.reverse.type == 'bridge'){\n                reverseModal.rules[0].inboundTag = [reverseModal.reverse.tag];\n                reverseModal.rules[1].inboundTag = [reverseModal.reverse.tag];\n            } else {\n                reverseModal.rules[0].outboundTag = reverseModal.reverse.tag;\n                reverseModal.rules[1].outboundTag = reverseModal.reverse.tag;\n            }\n            ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules);\n        },\n        show({ title='', okText='{{ i18n \"sure\" }}', reverse, rules, confirm=(reverse, rules)=>{}, isEdit=false  }) {\n            this.title = title;\n            this.okText = okText;\n            this.confirm = confirm;\n            this.visible = true;\n            if(isEdit) {\n                this.reverse = {\n                    tag: reverse.tag,\n                    type: reverse.type,\n                    domain: reverse.domain,\n                };\n                    reverse;\n                rules0 = rules.filter(r => r.domain != null);\n                if(rules0.length == 0) rules0 = [{ outboundTag: '', domain: [\"full:\" + this.reverse.domain], inboundTag: []}];\n                rules1 = rules.filter(r => r.domain == null);\n                if(rules1.length == 0) rules1 = [{ outboundTag: '', inboundTag: []}];\n                this.rules = [];\n                this.rules.push({\n                    domain: rules0[0].domain,\n                    outboundTag: rules0[0].outboundTag,\n                    inboundTag: rules0.map(r => r.inboundTag).flat()\n                });\n                this.rules.push({\n                    outboundTag: rules1[0].outboundTag,\n                    inboundTag: rules1.map(r => r.inboundTag).flat()\n                });\n            } else {\n                this.reverse = {\n                    tag: \"reverse-\" + app.reverseData.length,\n                    type: \"bridge\",\n                    domain: \"reverse.xui\"\n                }\n                this.rules = [\n                    { outboundTag: '', inboundTag: []},\n                    { outboundTag: '', inboundTag: []}\n                ]\n            }\n            this.isEdit = isEdit;\n            this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);\n            this.inboundTags.push(...app.inboundTags);\n            if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)\n            this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);\n        },\n        close() {\n            reverseModal.visible = false;\n            reverseModal.loading(false);\n        },\n        loading(loading=true) {\n            reverseModal.confirmLoading = loading;\n        },\n    };\n\n    new Vue({\n        delimiters: ['[[', ']]'],\n        el: '#reverse-modal',\n        data: {\n            reverseModal: reverseModal,\n            reverseTypes: { bridge: '{{ i18n \"pages.xray.outbound.bridge\" }}', portal:'{{ i18n \"pages.xray.outbound.portal\" }}'},\n        },\n    });\n\n</script>\n{{end}}\n"
  },
  {
    "path": "web/html/modals/xray_rule_modal.html",
    "content": "{{define \"modals/ruleModal\"}}\n<a-modal id=\"rule-modal\" v-model=\"ruleModal.visible\" :title=\"ruleModal.title\" @ok=\"ruleModal.ok\" :confirm-loading=\"ruleModal.confirmLoading\" :closable=\"true\" :mask-closable=\"false\" :ok-text=\"ruleModal.okText\" cancel-text='{{ i18n \"close\" }}' :class=\"themeSwitcher.currentTheme\">\n  <a-form :colon=\"false\" :label-col=\"{ md: {span:8} }\" :wrapper-col=\"{ md: {span:14} }\">\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n          </template> Source IPs <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"ruleModal.rule.sourceIP\" placeholder=\"e.g. 0.0.0.0/8, fc00::/7, geoip:ir\"></a-input>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n          </template> Source Port <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"ruleModal.rule.sourcePort\" placeholder=\"e.g. 53,443,1000-2000\"></a-input>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n          </template> VLESS Route <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"ruleModal.rule.vlessRoute\" placeholder=\"e.g. 53,443,1000-2000\"></a-input>\n    </a-form-item>\n    <a-form-item label='Network'>\n      <a-select v-model=\"ruleModal.rule.network\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option v-for=\"x in ['','TCP','UDP','TCP,UDP']\" :value=\"x\">[[ x ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-form-item label='Protocol'>\n      <a-select v-model=\"ruleModal.rule.protocol\" mode=\"multiple\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option v-for=\"x in ['http','tls','bittorrent','quic']\" :value=\"x\">[[ x ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-form-item label='Attributes'>\n      <a-button icon=\"plus\" size=\"small\" :style=\"{ marginLeft: '10px' }\" @click=\"ruleModal.rule.attrs.push(['', ''])\"></a-button>\n    </a-form-item>\n    <a-form-item :wrapper-col=\"{span: 24}\">\n      <a-input-group compact v-for=\"(attr,index) in ruleModal.rule.attrs\">\n        <a-input :style=\"{ width: '50%' }\" v-model=\"attr[0]\" placeholder='{{ i18n \"pages.inbounds.stream.general.name\" }}'>\n          <template slot=\"addonBefore\" :style=\"{ margin: '0' }\">[[ index+1 ]]</template>\n        </a-input>\n        <a-input :style=\"{ width: '50%' }\" v-model=\"attr[1]\" placeholder='{{ i18n \"pages.inbounds.stream.general.value\" }}'>\n          <a-button icon=\"minus\" slot=\"addonAfter\" size=\"small\" @click=\"ruleModal.rule.attrs.splice(index,1)\"></a-button>\n        </a-input>\n      </a-input-group>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n          </template> IP <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"ruleModal.rule.ip\" placeholder=\"e.g. 0.0.0.0/8, fc00::/7, geoip:ir\"></a-input>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n          </template> Domain <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"ruleModal.rule.domain\" placeholder=\"e.g. google.com, geosite:cn\"></a-input>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n          </template> User <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"ruleModal.rule.user\" placeholder=\"e.g. email address\"></a-input>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"pages.xray.rules.useComma\" }}</span>\n          </template> Port <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-input v-model.trim=\"ruleModal.rule.port\" placeholder=\"e.g. 53,443,1000-2000\"></a-input>\n    </a-form-item>\n    <a-form-item label='Inbound Tags'>\n      <a-select v-model=\"ruleModal.rule.inboundTag\" mode=\"multiple\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option v-for=\"tag in ruleModal.inboundTags\" :value=\"tag\">[[ tag ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-form-item label='Outbound Tag'>\n      <a-select v-model=\"ruleModal.rule.outboundTag\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option v-for=\"tag in ruleModal.outboundTags\" :value=\"tag\">[[ tag ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n    <a-form-item>\n      <template slot=\"label\">\n        <a-tooltip>\n          <template slot=\"title\">\n            <span>{{ i18n \"pages.xray.balancer.balancerDesc\" }}</span>\n          </template> Balancer Tag <a-icon type=\"question-circle\"></a-icon>\n        </a-tooltip>\n      </template>\n      <a-select v-model=\"ruleModal.rule.balancerTag\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n        <a-select-option v-for=\"tag in ruleModal.balancerTags\" :value=\"tag\">[[ tag ]]</a-select-option>\n      </a-select>\n    </a-form-item>\n  </a-form>\n</a-modal>\n<script>\n  const ruleModal = {\n    title: '',\n    visible: false,\n    confirmLoading: false,\n    okText: '{{ i18n \"sure\" }}',\n    isEdit: false,\n    confirm: null,\n    rule: {\n      type: \"field\",\n      domain: \"\",\n      ip: \"\",\n      port: \"\",\n      sourcePort: \"\",\n      vlessRoute: \"\",\n      network: \"\",\n      sourceIP: \"\",\n      user: \"\",\n      inboundTag: [],\n      protocol: [],\n      attrs: [],\n      outboundTag: \"\",\n      balancerTag: \"\",\n    },\n    inboundTags: [],\n    outboundTags: [],\n    users: [],\n    balancerTags: [],\n    ok() {\n      newRule = ruleModal.getResult();\n      ObjectUtil.execute(ruleModal.confirm, newRule);\n    },\n    show({\n      title = '',\n      okText = '{{ i18n \"sure\" }}',\n      rule,\n      confirm = (rule) => {},\n      isEdit = false\n    }) {\n      this.title = title;\n      this.okText = okText;\n      this.confirm = confirm;\n      this.visible = true;\n      if (isEdit) {\n        this.rule.domain = rule.domain ? rule.domain.join(',') : [];\n        this.rule.ip = rule.ip ? rule.ip.join(',') : [];\n        this.rule.port = rule.port;\n        this.rule.sourcePort = rule.sourcePort;\n        this.rule.vlessRoute = rule.vlessRoute;\n        this.rule.network = rule.network;\n        this.rule.sourceIP = rule.sourceIP ? rule.sourceIP.join(',') : [];\n        this.rule.user = rule.user ? rule.user.join(',') : [];\n        this.rule.inboundTag = rule.inboundTag;\n        this.rule.protocol = rule.protocol;\n        this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];\n        this.rule.outboundTag = rule.outboundTag;\n        this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : \"\";\n      } else {\n        this.rule = {\n          domain: \"\",\n          ip: \"\",\n          port: \"\",\n          sourcePort: \"\",\n          vlessRoute: \"\",\n          network: \"\",\n          sourceIP: \"\",\n          user: \"\",\n          inboundTag: [],\n          protocol: [],\n          attrs: [],\n          outboundTag: \"\",\n          balancerTag: \"\",\n        }\n      }\n      this.isEdit = isEdit;\n      this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);\n      this.inboundTags.push(...app.inboundTags);\n      if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)\n      this.outboundTags = [\"\", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];\n      if (app.templateSettings.reverse) {\n        if (app.templateSettings.reverse.bridges) {\n          this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));\n        }\n        if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));\n      }\n      if (app.templateSettings.routing && app.templateSettings.routing.balancers) {\n        this.balancerTags = [\"\", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];\n      }\n    },\n    close() {\n      ruleModal.visible = false;\n      ruleModal.loading(false);\n    },\n    loading(loading = true) {\n      ruleModal.confirmLoading = loading;\n    },\n    getResult() {\n      value = ruleModal.rule;\n      rule = {};\n      newRule = {};\n      rule.type = \"field\";\n      rule.domain = value.domain.length > 0 ? value.domain.split(',').map(s => s.trim()) : [];\n      rule.ip = value.ip.length > 0 ? value.ip.split(',').map(s => s.trim()) : [];\n      rule.port = value.port;\n      rule.sourcePort = value.sourcePort;\n      rule.vlessRoute = value.vlessRoute;\n      rule.network = value.network;\n      rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',').map(s => s.trim()) : [];\n      rule.user = value.user.length > 0 ? value.user.split(',').map(s => s.trim()) : [];\n      rule.inboundTag = value.inboundTag;\n      rule.protocol = value.protocol;\n      rule.attrs = Object.fromEntries(value.attrs);\n      rule.outboundTag = value.outboundTag == \"\" ? undefined : value.outboundTag;\n      rule.balancerTag = value.balancerTag == \"\" ? undefined : value.balancerTag;\n      for (const [key, value] of Object.entries(rule)) {\n        if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {\n          newRule[key] = value;\n        }\n      }\n      return newRule;\n    }\n  };\n  new Vue({\n    delimiters: ['[[', ']]'],\n    el: '#rule-modal',\n    data: {\n      ruleModal: ruleModal,\n    }\n  });\n</script>\n{{end}}\n"
  },
  {
    "path": "web/html/settings/panel/general.html",
    "content": "{{define \"settings/panel/general\"}}\n<a-collapse default-active-key=\"1\">\n    <a-collapse-panel key=\"1\" header='{{ i18n \"pages.xray.generalConfigs\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>\n                {{ i18n \"pages.settings.remarkModel\"}}\n            </template>\n            <template #description>\n                {{ i18n \"pages.settings.sampleRemark\"}}: <i>#[[ remarkSample ]]</i>\n            </template>\n            <template #control>\n                <a-input-group :style=\"{ width: '100%' }\">\n                    <a-select :style=\"{ paddingRight: '.5rem', minWidth: '80%', width: 'auto' }\" mode=\"multiple\"\n                        v-model=\"remarkModel\" :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                        <a-select-option v-for=\"(value, key) in remarkModels\" :value=\"key\">[[ value ]]</a-select-option>\n                    </a-select>\n                    <a-select :style=\"{ width: '20%' }\" v-model=\"remarkSeparator\"\n                        :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                        <a-select-option v-for=\"key in remarkSeparators\" :value=\"key\">[[ key ]]</a-select-option>\n                    </a-select>\n                </a-input-group>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.panelListeningIP\"}}</template>\n            <template #description>{{ i18n \"pages.settings.panelListeningIPDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.webListen\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.panelListeningDomain\"}}</template>\n            <template #description>{{ i18n \"pages.settings.panelListeningDomainDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.webDomain\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.panelPort\"}}</template>\n            <template #description>{{ i18n \"pages.settings.panelPortDesc\"}}</template>\n            <template #control>\n                <a-input-number :min=\"1\" :min=\"65535\" v-model=\"allSetting.webPort\" :style=\"{ width: '100%' }\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.panelUrlPath\"}}</template>\n            <template #description>{{ i18n \"pages.settings.panelUrlPathDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.webBasePath\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.sessionMaxAge\" }}</template>\n            <template #description>{{ i18n \"pages.settings.sessionMaxAgeDesc\" }}</template>\n            <template #control>\n                <a-input-number :min=\"60\" v-model=\"allSetting.sessionMaxAge\" :style=\"{ width: '100%' }\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.pageSize\" }}</template>\n            <template #description>{{ i18n \"pages.settings.pageSizeDesc\" }}</template>\n            <template #control>\n                <a-input-number :min=\"0\" step=\"5\" v-model=\"allSetting.pageSize\" :style=\"{ width: '100%' }\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.language\"}}</template>\n            <template #control>\n                <a-select ref=\"selectLang\" v-model=\"lang\" @change=\"LanguageManager.setLanguage(lang)\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\" :style=\"{ width: '100%' }\">\n                    <a-select-option :value=\"l.value\" :label=\"l.value\" v-for=\"l in LanguageManager.supportedLanguages\">\n                        <span role=\"img\" :aria-label=\"l.name\" v-text=\"l.icon\"></span> &nbsp;&nbsp; <span\n                            v-text=\"l.name\"></span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"2\" header='{{ i18n \"pages.settings.notifications\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.expireTimeDiff\" }}</template>\n            <template #description>{{ i18n \"pages.settings.expireTimeDiffDesc\" }}</template>\n            <template #control>\n                <a-input-number :min=\"0\" v-model=\"allSetting.expireDiff\" :style=\"{ width: '100%' }\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.trafficDiff\" }}</template>\n            <template #description>{{ i18n \"pages.settings.trafficDiffDesc\" }}</template>\n            <template #control>\n                <a-input-number :min=\"0\" v-model=\"allSetting.trafficDiff\" :style=\"{ width: '100%' }\"></a-input>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"3\" header='{{ i18n \"pages.settings.certs\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.publicKeyPath\"}}</template>\n            <template #description>{{ i18n \"pages.settings.publicKeyPathDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.webCertFile\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.privateKeyPath\"}}</template>\n            <template #description>{{ i18n \"pages.settings.privateKeyPathDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.webKeyFile\"></a-input>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"4\" header='{{ i18n \"pages.settings.externalTraffic\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.externalTrafficInformEnable\"}}</template>\n            <template #description>{{ i18n \"pages.settings.externalTrafficInformEnableDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.externalTrafficInformEnable\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.externalTrafficInformURI\"}}</template>\n            <template #description>{{ i18n \"pages.settings.externalTrafficInformURIDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" placeholder=\"(http|https)://domain[:port]/path/\"\n                    v-model=\"allSetting.externalTrafficInformURI\"></a-input>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"5\" header='{{ i18n \"pages.settings.dateAndTime\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.timeZone\"}}</template>\n            <template #description>{{ i18n \"pages.settings.timeZoneDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.timeLocation\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.datepicker\"}}</template>\n            <template #description>{{ i18n \"pages.settings.datepickerDescription\"}}</template>\n            <template #control>\n                <a-select :style=\"{ width: '100%' }\" :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                    v-model=\"datepicker\">\n                    <a-select-option v-for=\"item in datepickerList\" :value=\"item.value\">\n                        <span v-text=\"item.name\"></span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"6\" header='LDAP'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Enable LDAP sync</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.ldapEnable\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>LDAP Host</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapHost\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>LDAP Port</template>\n            <template #control>\n                <a-input-number :min=\"1\" :max=\"65535\" v-model=\"allSetting.ldapPort\" :style=\"{ width: '100%' }\"></a-input-number>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Use TLS (LDAPS)</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.ldapUseTLS\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Bind DN</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapBindDN\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Password</template>\n            <template #control>\n                <a-input type=\"password\" v-model=\"allSetting.ldapPassword\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Base DN</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapBaseDN\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>User filter</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapUserFilter\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>User attribute (username/email)</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapUserAttr\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>VLESS flag attribute</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapVlessField\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Generic flag attribute (optional)</template>\n            <template #description>If set, overrides VLESS flag; e.g. shadowInactive</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapFlagField\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Truthy values</template>\n            <template #description>Comma-separated; default: true,1,yes,on</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapTruthyValues\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Invert flag</template>\n            <template #description>Enable when attribute means disabled (e.g., shadowInactive)</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.ldapInvertFlag\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Sync schedule</template>\n            <template #description>cron-like string, e.g. @every 1m</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.ldapSyncCron\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Inbound tags</template>\n            <template #description>Select inbounds to manage (auto create/delete)</template>\n            <template #control>\n                <a-select mode=\"multiple\" :dropdown-class-name=\"themeSwitcher.currentTheme\" :style=\"{ width: '100%' }\" v-model=\"ldapInboundTagList\">\n                    <a-select-option v-for=\"opt in inboundOptions\" :key=\"opt.value\" :value=\"opt.value\">[[ opt.label ]]</a-select-option>\n                </a-select>\n                <div v-if=\"inboundOptions.length==0\" style=\"margin-top:6px;color:#999\">No inbounds found. Please create one in Inbounds.</div>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Auto create clients</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.ldapAutoCreate\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Auto delete clients</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.ldapAutoDelete\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Default total (GB)</template>\n            <template #control>\n                <a-input-number :min=\"0\" v-model=\"allSetting.ldapDefaultTotalGB\" :style=\"{ width: '100%' }\"></a-input-number>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Default expiry (days)</template>\n            <template #control>\n                <a-input-number :min=\"0\" v-model=\"allSetting.ldapDefaultExpiryDays\" :style=\"{ width: '100%' }\"></a-input-number>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Default Limit IP</template>\n            <template #control>\n                <a-input-number :min=\"0\" v-model=\"allSetting.ldapDefaultLimitIP\" :style=\"{ width: '100%' }\"></a-input-number>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n</a-collapse>\n{{end}}"
  },
  {
    "path": "web/html/settings/panel/security.html",
    "content": "{{define \"settings/panel/security\"}}\n<a-collapse default-active-key=\"1\">\n    <a-collapse-panel key=\"1\" header='{{ i18n \"pages.settings.security.admin\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.oldUsername\"}}</template>\n            <template #control>\n                <a-input autocomplete=\"username\" v-model=\"user.oldUsername\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.currentPassword\"}}</template>\n            <template #control>\n                <a-input-password autocomplete=\"current-password\" v-model=\"user.oldPassword\"></a-input-password>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.newUsername\"}}</template>\n            <template #control>\n                <a-input v-model=\"user.newUsername\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.newPassword\"}}</template>\n            <template #control>\n                <a-input-password autocomplete=\"new-password\" v-model=\"user.newPassword\"></a-input-password>\n            </template>\n        </a-setting-list-item>\n        <a-list-item>\n            <a-space direction=\"horizontal\" :style=\"{ padding: '0 20px' }\">\n                <a-button type=\"primary\" @click=\"updateUser\">{{ i18n \"confirm\" }}</a-button>\n            </a-space>\n        </a-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"2\" header='{{ i18n \"pages.settings.security.twoFactor\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.security.twoFactorEnable\" }}</template>\n            <template #description>{{ i18n \"pages.settings.security.twoFactorEnableDesc\" }}</template>\n            <template #control>\n                <a-switch @click=\"toggleTwoFactor\" :checked=\"allSetting.twoFactorEnable\"></a-switch>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n</a-collapse>\n{{end}}"
  },
  {
    "path": "web/html/settings/panel/subscription/general.html",
    "content": "{{define \"settings/panel/subscription/general\"}}\n<a-collapse default-active-key=\"1\">\n    <a-collapse-panel key=\"1\" header='{{ i18n \"pages.xray.generalConfigs\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subEnable\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subEnableDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.subEnable\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>JSON Subscription</template>\n            <template #description>{{ i18n \"pages.settings.subJsonEnable\"}}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.subJsonEnable\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subListen\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subListenDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subListen\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subDomain\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subDomainDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subDomain\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subPort\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subPortDesc\"}}</template>\n            <template #control>\n                <a-input-number v-model=\"allSetting.subPort\" :min=\"1\" :min=\"65535\"\n                    :style=\"{ width: '100%' }\"></a-input-number>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subPath\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subPathDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subPath\"\n                    @input=\"allSetting.subPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')\"\n                    @blur=\"allSetting.subPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\\/+/g,'/'); })(allSetting.subPath)\"\n                    placeholder=\"/sub/\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subURI\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subURIDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" placeholder=\"(http|https)://domain[:port]/path/\"\n                    v-model=\"allSetting.subURI\"></a-input>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"2\" header='{{ i18n \"pages.settings.information\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subEncrypt\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subEncryptDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.subEncrypt\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subShowInfo\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subShowInfoDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.subShowInfo\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-divider>{{ i18n \"pages.xray.basicTemplate\"}}</a-divider>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subTitle\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subTitleDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subTitle\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subSupportUrl\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subSupportUrlDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subSupportUrl\" placeholder=\"https://example.com\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subProfileUrl\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subProfileUrlDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subProfileUrl\" placeholder=\"https://example.com\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subAnnounce\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subAnnounceDesc\"}}</template>\n            <template #control>\n                <a-textarea v-model=\"allSetting.subAnnounce\"></a-textarea>\n            </template>\n        </a-setting-list-item>\n        <a-divider>{{ i18n \"pages.xray.advancedTemplate\"}} (Happ)</a-divider>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subEnableRouting\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subEnableRoutingDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.subEnableRouting\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subRoutingRules\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subRoutingRulesDesc\"}}</template>\n            <template #control>\n                <a-textarea v-model=\"allSetting.subRoutingRules\" placeholder=\"happ://routing/add/...\"></a-textarea>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"3\" header='{{ i18n \"pages.settings.certs\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subCertPath\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subCertPathDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subCertFile\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subKeyPath\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subKeyPathDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subKeyFile\"></a-input>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"4\" header='{{ i18n \"pages.settings.intervals\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subUpdates\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subUpdatesDesc\"}}</template>\n            <template #control>\n                <a-input-number :min=\"1\" v-model=\"allSetting.subUpdates\" :style=\"{ width: '100%' }\"></a-input-number>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n</a-collapse>\n{{end}}"
  },
  {
    "path": "web/html/settings/panel/subscription/json.html",
    "content": "{{define \"settings/panel/subscription/json\"}}\n<a-collapse default-active-key=\"1\">\n    <a-collapse-panel key=\"1\" header='{{ i18n \"pages.xray.generalConfigs\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subPath\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subPathDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.subJsonPath\"\n                    @input=\"allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')\"\n                    @blur=\"allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\\/+/g,'/'); })(allSetting.subJsonPath)\"\n                    placeholder=\"/json/\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.subURI\"}}</template>\n            <template #description>{{ i18n \"pages.settings.subURIDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" placeholder=\"(http|https)://domain[:port]/path/\"\n                    v-model=\"allSetting.subJsonURI\"></a-input>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"2\" header='{{ i18n \"pages.settings.fragment\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.fragment\"}}</template>\n            <template #description>{{ i18n \"pages.settings.fragmentDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"fragment\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-list-item v-if=\"fragment\" :style=\"{ padding: '10px 20px' }\">\n            <a-collapse>\n                <a-collapse-panel header='{{ i18n \"pages.settings.fragmentSett\"}}' v-if=\"fragment\">\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>Packets</template>\n                        <template #control>\n                            <a-input type=\"text\" v-model=\"fragmentPackets\"\n                                placeholder=\"1-1 | 1-3 | tlshello | ...\"></a-input>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>Length</template>\n                        <template #control>\n                            <a-input type=\"text\" v-model=\"fragmentLength\" placeholder=\"100-200\"></a-input>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>Interval</template>\n                        <template #control>\n                            <a-input type=\"text\" v-model=\"fragmentInterval\" placeholder=\"10-20\"></a-input>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>MaxSplit</template>\n                        <template #control>\n                            <a-input type=\"text\" v-model=\"fragmentMaxSplit\" placeholder=\"300-400\"></a-input>\n                        </template>\n                    </a-setting-list-item>\n                </a-collapse-panel>\n            </a-collapse>\n        </a-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"3\" header=\"Noises\">\n        <a-setting-list-item paddings=\"small\">\n            <template #title>Noises</template>\n            <template #description>{{ i18n \"pages.settings.noisesDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"noises\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-list-item v-if=\"noises\" :style=\"{ padding: '10px 20px' }\">\n            <a-collapse>\n                <a-collapse-panel v-for=\"(noise, index) in noisesArray\" :key=\"index\" :header=\"`Noise №${index + 1}`\">\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>Type</template>\n                        <template #control>\n                            <a-select :value=\"noise.type\" :style=\"{ width: '100%' }\"\n                                :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                                @change=\"(value) => updateNoiseType(index, value)\">\n                                <a-select-option :value=\"p\" :label=\"p\" v-for=\"p in ['rand', 'base64', 'str', 'hex']\"\n                                    :key=\"p\">\n                                    <span>[[ p ]]</span>\n                                </a-select-option>\n                            </a-select>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>Packet</template>\n                        <template #control>\n                            <a-input type=\"text\" :value=\"noise.packet\"\n                                @input=\"(value) => updateNoisePacket(index, event.target.value)\"\n                                placeholder=\"5-10\"></a-input>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>Delay (ms)</template>\n                        <template #control>\n                            <a-input type=\"text\" :value=\"noise.delay\"\n                                @input=\"(value) => updateNoiseDelay(index, event.target.value)\"\n                                placeholder=\"10-20\"></a-input>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>ApplyTo</template>\n                        <template #control>\n                            <a-select :value=\"noise.applyTo\" :style=\"{ width: '100%' }\"\n                                :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                                @change=\"(value) => updateNoiseApplyTo(index, value)\">\n                                <a-select-option :value=\"p\" :label=\"p\" v-for=\"p in ['ip', 'ipv4', 'ipv6']\" :key=\"p\">\n                                    <span>[[ p ]]</span>\n                                </a-select-option>\n                            </a-select>\n                        </template>\n                    </a-setting-list-item>\n                    <a-space direction=\"horizontal\" :style=\"{ padding: '10px 20px' }\">\n                        <a-button v-if=\"noisesArray.length > 1\" type=\"danger\"\n                            @click=\"removeNoise(index)\">Remove</a-button>\n                    </a-space>\n                </a-collapse-panel>\n            </a-collapse>\n            <a-button v-if=\"noises\" type=\"primary\" @click=\"addNoise\" :style=\"{ marginTop: '10px' }\">Add Noise</a-button>\n        </a-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"4\" header='{{ i18n \"pages.settings.mux\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.mux\"}}</template>\n            <template #description>{{ i18n \"pages.settings.muxDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"enableMux\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-list-item v-if=\"enableMux\" :style=\"{ padding: '10px 20px' }\">\n            <a-collapse>\n                <a-collapse-panel header='{{ i18n \"pages.settings.muxSett\"}}'>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>Concurrency</template>\n                        <template #control>\n                            <a-input-number v-model=\"muxConcurrency\" :min=\"-1\" :max=\"1024\"\n                                :style=\"{ width: '100%' }\"></a-input-number>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>xudp Concurrency</template>\n                        <template #control>\n                            <a-input-number v-model=\"muxXudpConcurrency\" :min=\"-1\" :max=\"1024\"\n                                :style=\"{ width: '100%' }\"></a-input-number>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>xudp UDP 443</template>\n                        <template #control>\n                            <a-select v-model=\"muxXudpProxyUDP443\" :style=\"{ width: '100%' }\"\n                                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                                <a-select-option :value=\"p\" :label=\"p\" v-for=\"p in ['reject', 'allow', 'skip']\">\n                                    <span>[[ p ]]</span>\n                                </a-select-option>\n                            </a-select>\n                        </template>\n                    </a-setting-list-item>\n                </a-collapse-panel>\n            </a-collapse>\n        </a-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"5\" header='{{ i18n \"pages.settings.direct\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.direct\"}}</template>\n            <template #description>{{ i18n \"pages.settings.directDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"enableDirect\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-list-item v-if=\"enableDirect\" :style=\"{ padding: '10px 20px' }\">\n            <a-collapse>\n                <a-collapse-panel header='{{ i18n \"pages.settings.direct\"}}'>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>{{ i18n \"pages.xray.directips\" }}</template>\n                        <template #control>\n                            <a-select mode=\"tags\" :style=\"{ width: '100%' }\" v-model=\"directIPs\"\n                                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                                <a-select-option :value=\"p.value\" :label=\"p.label\" v-for=\"p in directIPsOptions\">\n                                    <span>[[ p.label ]]</span>\n                                </a-select-option>\n                            </a-select>\n                        </template>\n                    </a-setting-list-item>\n                    <a-setting-list-item paddings=\"small\">\n                        <template #title>{{ i18n \"pages.xray.directdomains\" }}</template>\n                        <template #control>\n                            <a-select mode=\"tags\" :style=\"{ width: '100%' }\" v-model=\"directDomains\"\n                                :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                                <a-select-option :value=\"p.value\" :label=\"p.label\" v-for=\"p in diretDomainsOptions\">\n                                    <span>[[ p.label ]]</span>\n                                </a-select-option>\n                            </a-select>\n                        </template>\n                    </a-setting-list-item>\n                </a-collapse-panel>\n            </a-collapse>\n        </a-list-item>\n    </a-collapse-panel>\n</a-collapse>\n{{end}}"
  },
  {
    "path": "web/html/settings/panel/subscription/subpage.html",
    "content": "{{ template \"page/head_start\" .}}\n<script src=\"{{ .base_path }}assets/moment/moment.min.js\"></script>\n<script src=\"{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/ant-design-vue/antd.min.js\"></script>\n<script src=\"{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}\"></script>\n<style>\n    .subscription-page .subscription-link-box {\n        cursor: pointer;\n        border-radius: 12px;\n        padding: 25px 20px 15px 20px;\n        margin-top: -12px;\n        word-break: break-all;\n        font-size: 13px;\n        line-height: 1.5;\n        text-align: left;\n        font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n        transition: all 0.3s;\n        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n    }\n\n    .dark.subscription-page .subscription-link-box {\n        background: rgba(0, 0, 0, 0.2);\n        border: 1px solid rgba(255, 255, 255, 0.1);\n        color: #fff;\n    }\n\n    .dark.subscription-page .subscription-link-box:hover {\n        background: rgba(0, 0, 0, 0.3);\n        border-color: rgba(255, 255, 255, 0.2);\n    }\n\n    .light.subscription-page .subscription-link-box {\n        background: rgba(0, 0, 0, 0.03);\n        border: 1px solid rgba(0, 0, 0, 0.08);\n        color: rgba(0, 0, 0, 0.85);\n    }\n\n    .light.subscription-page .subscription-link-box:hover {\n        background: rgba(0, 0, 0, 0.05);\n        border-color: rgba(0, 0, 0, 0.14);\n    }\n</style>\n{{ template \"page/head_end\" .}}\n\n{{ template \"page/body_start\" .}}\n<a-layout id=\"app\" v-cloak :class=\"themeSwitcher.currentTheme + ' subscription-page'\">\n    <a-layout-content class=\"p-2\">\n        <a-row type=\"flex\" justify=\"center\" class=\"mt-2\">\n            <a-col :xs=\"24\" :sm=\"22\" :md=\"18\" :lg=\"14\" :xl=\"12\">\n                <a-card hoverable class=\"subscription-card\">\n                    <template #title>\n                        <a-space>\n                            <span>{{ i18n \"subscription.title\" }}</span>\n                            <a-tag>{{ .sId }}</a-tag>\n                        </a-space>\n                    </template>\n                    <template #extra>\n                        <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\" title='{{ i18n \"menu.settings\" }}'\n                            placement=\"bottomRight\" trigger=\"click\">\n                            <template #content>\n                                <a-space direction=\"vertical\" :size=\"10\">\n                                    <a-theme-switch-login></a-theme-switch-login>\n                                    <span>{{ i18n \"pages.settings.language\"\n                                        }}</span>\n                                    <a-select ref=\"selectLang\" class=\"w-100\" v-model=\"lang\"\n                                        @change=\"LanguageManager.setLanguage(lang)\"\n                                        :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                                        <a-select-option :value=\"l.value\" label=\"English\"\n                                            v-for=\"l in LanguageManager.supportedLanguages\" :key=\"l.value\">\n                                            <span role=\"img\" :aria-label=\"l.name\" v-text=\"l.icon\"></span>\n                                            &nbsp;&nbsp;<span v-text=\"l.name\"></span>\n                                        </a-select-option>\n                                    </a-select>\n                                </a-space>\n                            </template>\n                            <a-button shape=\"circle\" icon=\"setting\"></a-button>\n                        </a-popover>\n                    </template>\n\n                    <a-form layout=\"vertical\">\n                        <a-form-item>\n                            <a-space direction=\"vertical\" align=\"center\">\n                                <a-row type=\"flex\" :gutter=\"[8,8]\" justify=\"center\" style=\"width:100%\">\n                                    <a-col :xs=\"24\" :sm=\"app.subJsonUrl ? 12 : 24\" style=\"text-align:center;\">\n                                        <tr-qr-box class=\"qr-box\">\n                                            <a-tag color=\"purple\" class=\"qr-tag\">\n                                                <span>{{ i18n\n                                                    \"pages.settings.subSettings\"}}</span>\n                                            </a-tag>\n                                            <tr-qr-bg class=\"qr-bg-sub\">\n                                                <tr-qr-bg-inner class=\"qr-bg-sub-inner\">\n                                                    <canvas id=\"qrcode\" class=\"qr-cv\" title='{{ i18n \"copy\" }}'\n                                                        @click=\"copy(app.subUrl)\"></canvas>\n                                                </tr-qr-bg-inner>\n                                            </tr-qr-bg>\n                                        </tr-qr-box>\n                                    </a-col>\n                                    <a-col v-if=\"app.subJsonUrl\" :xs=\"24\" :sm=\"12\" style=\"text-align:center;\">\n                                        <tr-qr-box class=\"qr-box\">\n                                            <a-tag color=\"purple\" class=\"qr-tag\">\n                                                <span>{{ i18n\n                                                    \"pages.settings.subSettings\"}}\n                                                    Json</span>\n                                            </a-tag>\n                                            <tr-qr-bg class=\"qr-bg-sub\">\n                                                <tr-qr-bg-inner class=\"qr-bg-sub-inner\">\n                                                    <canvas id=\"qrcode-subjson\" class=\"qr-cv\" title='{{ i18n \"copy\" }}'\n                                                        @click=\"copy(app.subJsonUrl)\"></canvas>\n                                                </tr-qr-bg-inner>\n                                            </tr-qr-bg>\n                                        </tr-qr-box>\n                                    </a-col>\n                                </a-row>\n                            </a-space>\n                        </a-form-item>\n\n                        <a-form-item>\n                            <a-descriptions bordered :column=\"1\" size=\"small\">\n                                <a-descriptions-item label='{{ i18n \"subscription.subId\" }}'>[[\n                                    app.sId\n                                    ]]</a-descriptions-item>\n                                <a-descriptions-item label='{{ i18n \"subscription.status\" }}'>\n                                    <template v-if=\"isUnlimited\">\n                                        <a-tag color=\"purple\">{{ i18n\n                                            \"subscription.unlimited\" }}</a-tag>\n                                    </template>\n                                    <template v-else>\n                                        <a-tag :color=\"isActive ? 'green' : 'red'\">[[\n                                            isActive ? '{{ i18n\n                                            \"subscription.active\" }}' : '{{ i18n\n                                            \"subscription.inactive\" }}'\n                                            ]]</a-tag>\n                                    </template>\n                                </a-descriptions-item>\n                                <a-descriptions-item label='{{ i18n \"subscription.downloaded\" }}'>[[\n                                    app.download\n                                    ]]</a-descriptions-item>\n                                <a-descriptions-item label='{{ i18n \"subscription.uploaded\" }}'>[[\n                                    app.upload\n                                    ]]</a-descriptions-item>\n                                <a-descriptions-item label='{{ i18n \"usage\" }}'>[[ app.used\n                                    ]]</a-descriptions-item>\n                                <a-descriptions-item label='{{ i18n \"subscription.totalQuota\" }}'>[[\n                                    app.total\n                                    ]]</a-descriptions-item>\n                                <a-descriptions-item v-if=\"app.totalByte > 0\" label='{{ i18n \"remained\" }}'>[[\n                                    app.remained ]]</a-descriptions-item>\n                                <a-descriptions-item label='{{ i18n \"lastOnline\" }}'>\n                                    <template v-if=\"app.lastOnlineMs > 0\">\n                                        [[ IntlUtil.formatDate(app.lastOnlineMs) ]]\n                                    </template>\n                                    <template v-else>\n                                        <span>-</span>\n                                    </template>\n                                </a-descriptions-item>\n                                <a-descriptions-item label='{{ i18n \"subscription.expiry\" }}'>\n                                    <template v-if=\"app.expireMs === 0\">\n                                        {{ i18n \"subscription.noExpiry\" }}\n                                    </template>\n                                    <template v-else>\n                                        [[ IntlUtil.formatDate(app.expireMs) ]]\n                                    </template>\n                                </a-descriptions-item>\n                            </a-descriptions>\n                        </a-form-item>\n                    </a-form>\n\n                    <br />\n                    <div v-for=\"(link, idx) in links\" :key=\"link\"\n                        style=\"position: relative; margin-bottom: 20px; text-align: center;\">\n                        <div class=\"qr-box\" style=\"display: inline-block; width: 100%; max-width: 100%;\">\n                            <a-tag color=\"purple\"\n                                style=\"margin-bottom: -10px; position: relative; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.2);\">\n                                <span>[[ linkName(link, idx) ]]</span>\n                            </a-tag>\n                            <div @click=\"copy(link)\" class=\"subscription-link-box\">\n                                [[ link ]]\n                            </div>\n                        </div>\n                    </div>\n\n                    </div>\n                    <br />\n\n                    <a-form layout=\"vertical\">\n                        <a-form-item>\n                            <a-row type=\"flex\" justify=\"center\" :gutter=\"[8,8]\" style=\"width:100%\">\n                                <a-col :xs=\"24\" :sm=\"12\" style=\"text-align:center;\">\n                                    <!-- Android dropdown -->\n                                    <a-dropdown :trigger=\"['click']\">\n                                        <a-button icon=\"android\" :block=\"isMobile\"\n                                            :style=\"{ marginTop: isMobile ? '6px' : 0 }\" size=\"large\" type=\"primary\">\n                                            Android <a-icon type=\"down\" />\n                                        </a-button>\n                                        <a-menu slot=\"overlay\" :class=\"themeSwitcher.currentTheme\">\n                                            <a-menu-item key=\"android-v2box\"\n                                                @click=\"open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))\">V2Box</a-menu-item>\n                                            <a-menu-item key=\"android-v2rayng\"\n                                                @click=\"open('v2rayng://install-config?url=' + encodeURIComponent(app.subUrl))\">V2RayNG</a-menu-item>\n                                            <a-menu-item key=\"android-singbox\"\n                                                @click=\"copy(app.subUrl)\">Sing-box</a-menu-item>\n                                            <a-menu-item key=\"android-v2raytun\"\n                                                @click=\"copy(app.subUrl)\">V2RayTun</a-menu-item>\n                                            <a-menu-item key=\"android-npvtunnel\" @click=\"copy(app.subUrl)\">NPV\n                                                Tunnel</a-menu-item>\n                                            <a-menu-item key=\"android-happ\"\n                                                @click=\"open('happ://add/' + app.subUrl)\">Happ</a-menu-item>\n                                        </a-menu>\n                                    </a-dropdown>\n                                </a-col>\n                                <a-col :xs=\"24\" :sm=\"12\" style=\"text-align:center;\">\n                                    <!-- iOS dropdown -->\n                                    <a-dropdown :trigger=\"['click']\">\n                                        <a-button icon=\"apple\" :block=\"isMobile\"\n                                            :style=\"{ marginTop: isMobile ? '6px' : 0 }\" size=\"large\" type=\"primary\">\n                                            iOS <a-icon type=\"down\" />\n                                        </a-button>\n                                        <a-menu slot=\"overlay\" :class=\"themeSwitcher.currentTheme\">\n                                            <a-menu-item key=\"ios-shadowrocket\"\n                                                @click=\"open(shadowrocketUrl)\">Shadowrocket</a-menu-item>\n                                            <a-menu-item key=\"ios-v2box\" @click=\"open(v2boxUrl)\">V2Box</a-menu-item>\n                                            <a-menu-item key=\"ios-streisand\"\n                                                @click=\"open(streisandUrl)\">Streisand</a-menu-item>\n                                            <a-menu-item key=\"ios-v2raytun\"\n                                                @click=\"copy(v2raytunUrl)\">V2RayTun</a-menu-item>\n                                            <a-menu-item key=\"ios-npvtunnel\" @click=\"copy(npvtunUrl)\">NPV\n                                                Tunnel\n                                            </a-menu-item>\n                                            <a-menu-item key=\"ios-happ\" @click=\"open(happUrl)\">Happ</a-menu-item>\n                                        </a-menu>\n                                    </a-dropdown>\n                                </a-col>\n                            </a-row>\n                        </a-form-item>\n                    </a-form>\n                </a-card>\n            </a-col>\n        </a-row>\n    </a-layout-content>\n</a-layout>\n\n<!-- Bootstrap data for external JS -->\n<template id=\"subscription-data\" data-sid=\"{{ .sId }}\" data-sub-url=\"{{ .subUrl }}\" data-subjson-url=\"{{ .subJsonUrl }}\"\n    data-download=\"{{ .download }}\" data-upload=\"{{ .upload }}\" data-used=\"{{ .used }}\" data-total=\"{{ .total }}\"\n    data-remained=\"{{ .remained }}\" data-expire=\"{{ .expire }}\" data-lastonline=\"{{ .lastOnline }}\"\n    data-downloadbyte=\"{{ .downloadByte }}\" data-uploadbyte=\"{{ .uploadByte }}\" data-totalbyte=\"{{ .totalByte }}\"\n    data-datepicker=\"{{ .datepicker }}\"></template>\n<textarea id=\"subscription-links\" style=\"display:none\">{{ range .result }}{{ . }}\n{{ end }}</textarea>\n\n{{template \"component/aThemeSwitch\" .}}\n<script src=\"{{ .base_path }}assets/js/subscription.js?{{ .cur_ver }}\"></script>\n\n{{ template \"page/body_end\" .}}"
  },
  {
    "path": "web/html/settings/panel/telegram.html",
    "content": "{{define \"settings/panel/telegram\"}}\n<a-collapse default-active-key=\"1\">\n    <a-collapse-panel key=\"1\" header='{{ i18n \"pages.xray.generalConfigs\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.telegramBotEnable\" }}</template>\n            <template #description>{{ i18n \"pages.settings.telegramBotEnableDesc\" }}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.tgBotEnable\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.telegramToken\"}}</template>\n            <template #description>{{ i18n \"pages.settings.telegramTokenDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.tgBotToken\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.telegramChatId\"}}</template>\n            <template #description>{{ i18n \"pages.settings.telegramChatIdDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.tgBotChatId\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.telegramBotLanguage\"}}</template>\n            <template #control>\n                <a-select ref=\"selectBotLang\" v-model=\"allSetting.tgLang\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\" :style=\"{ width: '100%' }\">\n                    <a-select-option :value=\"l.value\" :label=\"l.value\" v-for=\"l in LanguageManager.supportedLanguages\">\n                        <span role=\"img\" :aria-label=\"l.name\" v-text=\"l.icon\"></span> &nbsp;&nbsp; <span\n                            v-text=\"l.name\"></span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"2\" header='{{ i18n \"pages.settings.notifications\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.telegramNotifyTime\"}}</template>\n            <template #description>{{ i18n \"pages.settings.telegramNotifyTimeDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" v-model=\"allSetting.tgRunTime\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.tgNotifyBackup\" }}</template>\n            <template #description>{{ i18n \"pages.settings.tgNotifyBackupDesc\" }}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.tgBotBackup\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.tgNotifyLogin\" }}</template>\n            <template #description>{{ i18n \"pages.settings.tgNotifyLoginDesc\" }}</template>\n            <template #control>\n                <a-switch v-model=\"allSetting.tgBotLoginNotify\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.tgNotifyCpu\" }}</template>\n            <template #description>{{ i18n \"pages.settings.tgNotifyCpuDesc\" }}</template>\n            <template #control>\n                <a-input-number :min=\"0\" :min=\"100\" v-model=\"allSetting.tgCpu\" :style=\"{ width: '100%' }\"></a-switch>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"3\" header='{{ i18n \"pages.settings.proxyAndServer\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.telegramProxy\"}}</template>\n            <template #description>{{ i18n \"pages.settings.telegramProxyDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" placeholder=\"socks5://user:pass@host:port\"\n                    v-model=\"allSetting.tgBotProxy\"></a-input>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.settings.telegramAPIServer\"}}</template>\n            <template #description>{{ i18n \"pages.settings.telegramAPIServerDesc\"}}</template>\n            <template #control>\n                <a-input type=\"text\" placeholder=\"https://api.example.com\"\n                    v-model=\"allSetting.tgBotAPIServer\"></a-input>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n</a-collapse>\n{{end}}"
  },
  {
    "path": "web/html/settings/xray/advanced.html",
    "content": "{{define \"settings/xray/advanced\"}}\n<a-space direction=\"vertical\" size=\"small\" :style=\"{ marginTop: '20px' }\">\n    <a-list-item-meta title='{{ i18n \"pages.xray.Template\"}}'\n        description='{{ i18n \"pages.xray.TemplateDesc\"}}'></a-list-item-meta>\n    <a-radio-group v-model=\"advSettings\" @change=\"changeCode\" button-style=\"solid\" :style=\"{ margin: '10px 0' }\"\n        :size=\"isMobile ? 'small' : ''\">\n        <a-radio-button value=\"xraySetting\">{{ i18n \"pages.xray.completeTemplate\"}}</a-radio-button>\n        <a-radio-button value=\"inboundSettings\">{{ i18n \"pages.xray.Inbounds\" }}</a-radio-button>\n        <a-radio-button value=\"outboundSettings\">{{ i18n \"pages.xray.Outbounds\" }}</a-radio-button>\n        <a-radio-button value=\"routingRuleSettings\">{{ i18n \"pages.xray.Routings\" }}</a-radio-button>\n    </a-radio-group>\n    <textarea :style=\"{ position: 'absolute', left: '-800px' }\" id=\"xraySetting\"></textarea>\n</a-space>\n{{end}}"
  },
  {
    "path": "web/html/settings/xray/balancers.html",
    "content": "{{define \"settings/xray/balancers\"}}\n<template v-if=\"balancersData.length > 0\">\n    <a-space direction=\"vertical\" size=\"middle\">\n        <a-button type=\"primary\" icon=\"plus\" @click=\"addBalancer()\">\n            <span>{{ i18n \"pages.xray.balancer.addBalancer\"}}</span>\n        </a-button>\n        <a-table :columns=\"balancerColumns\" bordered :row-key=\"r => r.key\" :data-source=\"balancersData\"\n            :scroll=\"isMobile ? {} : { x: 200 }\" :pagination=\"false\" :indent-size=\"0\" :locale='{ filterConfirm: `{{ i18n \"confirm\" }}`, filterReset: `{{ i18n \"reset\" }}` }'>\n            <template slot=\"action\" slot-scope=\"text, balancer, index\">\n                <span>[[ index+1 ]]</span>\n                <a-dropdown :trigger=\"['click']\">\n                    <a-icon @click=\"e => e.preventDefault()\" type=\"more\"\n                        :style=\"{ fontSize: '16px', textDecoration: 'bold' }\"></a-icon>\n                    <a-menu slot=\"overlay\" :theme=\"themeSwitcher.currentTheme\">\n                        <a-menu-item @click=\"editBalancer(index)\">\n                            <a-icon type=\"edit\"></a-icon>\n                            <span>{{ i18n \"edit\" }}</span>\n                        </a-menu-item>\n                        <a-menu-item @click=\"deleteBalancer(index)\">\n                            <span :style=\"{ color: '#FF4D4F' }\">\n                                <a-icon type=\"delete\"></a-icon> \n                                <span>{{ i18n \"delete\"}}</span>\n                            </span>\n                        </a-menu-item>\n                    </a-menu>\n                </a-dropdown>\n            </template>\n            <template slot=\"strategy\" slot-scope=\"text, balancer, index\">\n                <a-tag :style=\"{ margin: '0' }\" v-if=\"balancer.strategy=='random'\" color=\"purple\">Random</a-tag>\n                <a-tag :style=\"{ margin: '0' }\" v-if=\"balancer.strategy=='roundRobin'\" color=\"green\">Round Robin</a-tag>\n                <a-tag :style=\"{ margin: '0' }\" v-if=\"balancer.strategy=='leastLoad'\" color=\"green\">Least Load</a-tag>\n                <a-tag :style=\"{ margin: '0' }\" v-if=\"balancer.strategy=='leastPing'\" color=\"green\">Least Ping</a-tag>\n            </template>\n            <template slot=\"selector\" slot-scope=\"text, balancer, index\">\n                <a-tag class=\"info-large-tag\" :style=\"{ margin: '1' }\" v-for=\"sel in balancer.selector\">[[ sel ]]</a-tag>\n            </template>\n        </a-table>\n        <a-radio-group v-if=\"observatoryEnable || burstObservatoryEnable\" v-model=\"obsSettings\" @change=\"changeObsCode\"\n            button-style=\"solid\" :size=\"isMobile ? 'small' : ''\">\n            <a-radio-button value=\"observatory\" v-if=\"observatoryEnable\">Observatory</a-radio-button>\n            <a-radio-button value=\"burstObservatory\" v-if=\"burstObservatoryEnable\">Burst Observatory</a-radio-button>\n        </a-radio-group>\n        <textarea :style=\"{ position: 'absolute', left: '-800px' }\" id=\"obsSetting\"></textarea>\n    </a-space>\n</template>\n<template v-else>\n    <a-empty description='{{ i18n \"emptyBalancersDesc\" }}' :style=\"{ margin: '10px' }\">\n        <a-button type=\"primary\" icon=\"plus\" @click=\"addBalancer()\" :style=\"{ marginTop: '10px' }\">\n            <span>{{ i18n \"pages.xray.balancer.addBalancer\"}}</span>\n        </a-button>\n    </a-empty>\n</template>\n{{end}}"
  },
  {
    "path": "web/html/settings/xray/basics.html",
    "content": "{{define \"settings/xray/basics\"}}\n<a-collapse default-active-key=\"1\">\n    <a-collapse-panel key=\"1\" header='{{ i18n \"pages.xray.generalConfigs\"}}'>\n        <a-row :xs=\"24\" :sm=\"24\" :lg=\"12\">\n            <a-alert type=\"warning\" :style=\"{ textAlign: 'center' }\">\n                <template slot=\"message\">\n                    <a-icon type=\"exclamation-circle\" theme=\"filled\"\n                        :style=\"{ color: '#FFA031' }\"></a-icon>\n                    <span>{{ i18n \"pages.xray.generalConfigsDesc\" }}</span>\n                </template>\n            </a-alert>\n        </a-row>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.FreedomStrategy\" }}</template>\n            <template #description>{{ i18n \"pages.xray.FreedomStrategyDesc\"\n                }}</template>\n            <template #control>\n                <a-select v-model=\"freedomStrategy\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                    :style=\"{ width: '100%' }\">\n                    <a-select-option v-for=\"s in OutboundDomainStrategies\"\n                        :value=\"s\">\n                        <span>[[ s ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.RoutingStrategy\" }}</template>\n            <template #description>{{ i18n \"pages.xray.RoutingStrategyDesc\"\n                }}</template>\n            <template #control>\n                <a-select v-model=\"routingStrategy\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                    :style=\"{ width: '100%' }\">\n                    <a-select-option v-for=\"s in routingDomainStrategies\"\n                        :value=\"s\">\n                        <span>[[ s ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.outboundTestUrl\" }}</template>\n            <template #description>{{ i18n \"pages.xray.outboundTestUrlDesc\"\n                }}</template>\n            <template #control>\n                <a-input v-model=\"outboundTestUrl\"\n                    :placeholder=\"'https://www.google.com/generate_204'\"\n                    :style=\"{ width: '100%' }\"></a-input>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"2\" header='{{ i18n \"pages.xray.statistics\" }}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.statsInboundUplink\"\n                }}</template>\n            <template #description>{{ i18n \"pages.xray.statsInboundUplinkDesc\"\n                }}</template>\n            <template #control>\n                <a-switch v-model=\"statsInboundUplink\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.statsInboundDownlink\"\n                }}</template>\n            <template #description>{{ i18n \"pages.xray.statsInboundDownlinkDesc\"\n                }}</template>\n            <template #control>\n                <a-switch v-model=\"statsInboundDownlink\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.statsOutboundUplink\"\n                }}</template>\n            <template #description>{{ i18n \"pages.xray.statsOutboundUplinkDesc\"\n                }}</template>\n            <template #control>\n                <a-switch v-model=\"statsOutboundUplink\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.statsOutboundDownlink\"\n                }}</template>\n            <template #description>{{ i18n\n                \"pages.xray.statsOutboundDownlinkDesc\" }}</template>\n            <template #control>\n                <a-switch v-model=\"statsOutboundDownlink\"></a-switch>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"3\" header='{{ i18n \"pages.xray.logConfigs\" }}'>\n        <a-row :xs=\"24\" :sm=\"24\" :lg=\"12\">\n            <a-alert type=\"warning\" :style=\"{ textAlign: 'center' }\">\n                <template slot=\"message\">\n                    <a-icon type=\"exclamation-circle\" theme=\"filled\"\n                        :style=\"{ color: '#FFA031' }\"></a-icon>\n                    <span>{{ i18n \"pages.xray.logConfigsDesc\" }}</span>\n                </template>\n            </a-alert>\n        </a-row>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.logLevel\" }}</template>\n            <template #description>{{ i18n \"pages.xray.logLevelDesc\"\n                }}</template>\n            <template #control>\n                <a-select v-model=\"logLevel\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                    :style=\"{ width: '100%' }\">\n                    <a-select-option v-for=\"s in log.loglevel\" :value=\"s\">\n                        <span>[[ s ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.accessLog\" }}</template>\n            <template #description>{{ i18n \"pages.xray.accessLogDesc\"\n                }}</template>\n            <template #control>\n                <a-select v-model=\"accessLog\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                    :style=\"{ width: '100%' }\">\n                    <a-select-option value>\n                        <span>Empty</span>\n                    </a-select-option>\n                    <a-select-option v-for=\"s in log.access\" :value=\"s\">\n                        <span>[[ s ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.errorLog\" }}</template>\n            <template #description>{{ i18n \"pages.xray.errorLogDesc\"\n                }}</template>\n            <template #control>\n                <a-select v-model=\"errorLog\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                    :style=\"{ width: '100%' }\">\n                    <a-select-option value>\n                        <span>Empty</span>\n                    </a-select-option>\n                    <a-select-option v-for=\"s in log.error\" :value=\"s\">\n                        <span>[[ s ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.maskAddress\" }}</template>\n            <template #description>{{ i18n \"pages.xray.maskAddressDesc\"\n                }}</template>\n            <template #control>\n                <a-select v-model=\"maskAddressLog\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\"\n                    :style=\"{ width: '100%' }\">\n                    <a-select-option value>\n                        <span>Empty</span>\n                    </a-select-option>\n                    <a-select-option v-for=\"s in log.maskAddress\" :value=\"s\">\n                        <span>[[ s ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.dnsLog\"}}</template>\n            <template #description>{{ i18n \"pages.xray.dnsLogDesc\"}}</template>\n            <template #control>\n                <a-switch v-model=\"dnslog\"></a-switch>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"4\" header='{{ i18n \"pages.xray.basicRouting\"}}'>\n        <a-row :xs=\"24\" :sm=\"24\" :lg=\"12\">\n            <a-alert type=\"warning\" :style=\"{ textAlign: 'center' }\">\n                <template slot=\"message\">\n                    <a-icon type=\"exclamation-circle\" theme=\"filled\"\n                        :style=\"{ color: '#FFA031' }\"></a-icon>\n                    <span>{{ i18n \"pages.xray.blockConfigsDesc\" }}</span>\n                </template>\n            </a-alert>\n        </a-row>\n        <a-setting-list-item paddings=\"small\" :style=\"{ marginBottom: '20px' }\">\n            <template #title>{{ i18n \"pages.xray.Torrent\"}}</template>\n            <template #control>\n                <a-switch v-model=\"torrentSettings\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <a-row :xs=\"24\" :sm=\"24\" :lg=\"12\">\n            <a-alert type=\"warning\" :style=\"{ textAlign: 'center' }\">\n                <template slot=\"message\">\n                    <a-icon type=\"exclamation-circle\" theme=\"filled\"\n                        :style=\"{ color: '#FFA031' }\"></a-icon>\n                    <span>{{ i18n \"pages.xray.blockConnectionsConfigsDesc\"\n                        }}</span>\n                </template>\n            </a-alert>\n        </a-row>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.blockips\" }}</template>\n            <template #control>\n                <a-select mode=\"tags\" v-model=\"blockedIPs\"\n                    :style=\"{ width: '100%' }\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                    <a-select-option :value=\"p.value\" :label=\"p.label\"\n                        v-for=\"p in settingsData.IPsOptions\">\n                        <span>[[ p.label ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.blockdomains\" }}</template>\n            <template #control>\n                <a-select mode=\"tags\" v-model=\"blockedDomains\"\n                    :style=\"{ width: '100%' }\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                    <a-select-option :value=\"p.value\" :label=\"p.label\"\n                        v-for=\"p in settingsData.BlockDomainsOptions\">\n                        <span>[[ p.label ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-row :xs=\"24\" :sm=\"24\" :lg=\"12\">\n            <a-alert type=\"warning\"\n                :style=\"{ textAlign: 'center', marginTop: '20px' }\">\n                <template slot=\"message\">\n                    <a-icon type=\"exclamation-circle\" theme=\"filled\"\n                        :style=\"{ color: '#FFA031' }\"></a-icon>\n                    <span>{{ i18n \"pages.xray.directConnectionsConfigsDesc\"\n                        }}</span>\n                </template>\n            </a-alert>\n        </a-row>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.directips\" }}</template>\n            <template #control>\n                <a-select mode=\"tags\" :style=\"{ width: '100%' }\"\n                    v-model=\"directIPs\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                    <a-select-option :value=\"p.value\" :label=\"p.label\"\n                        v-for=\"p in settingsData.IPsOptions\">\n                        <span>[[ p.label ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.directdomains\" }}</template>\n            <template #control>\n                <a-select mode=\"tags\" :style=\"{ width: '100%' }\"\n                    v-model=\"directDomains\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                    <a-select-option :value=\"p.value\" :label=\"p.label\"\n                        v-for=\"p in settingsData.DomainsOptions\">\n                        <span>[[ p.label ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-row :xs=\"24\" :sm=\"24\" :lg=\"12\">\n            <a-alert type=\"warning\"\n                :style=\"{ textAlign: 'center', marginTop: '20px' }\">\n                <template slot=\"message\">\n                    <a-icon type=\"exclamation-circle\" theme=\"filled\"\n                        :style=\"{ color: '#FFA031' }\"></a-icon>\n                    <span>{{ i18n \"pages.xray.ipv4RoutingDesc\" }}</span>\n                </template>\n            </a-alert>\n        </a-row>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.ipv4Routing\" }}</template>\n            <template #control>\n                <a-select mode=\"tags\" :style=\"{ width: '100%' }\"\n                    v-model=\"ipv4Domains\"\n                    :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                    <a-select-option :value=\"p.value\" :label=\"p.label\"\n                        v-for=\"p in settingsData.ServicesOptions\">\n                        <span>[[ p.label ]]</span>\n                    </a-select-option>\n                </a-select>\n            </template>\n        </a-setting-list-item>\n        <a-row :xs=\"24\" :sm=\"24\" :lg=\"12\">\n            <a-alert type=\"warning\"\n                :style=\"{ textAlign: 'center', marginTop: '20px' }\">\n                <template slot=\"message\">\n                    <a-icon type=\"exclamation-circle\" theme=\"filled\"\n                        :style=\"{ color: '#FFA031' }\"></a-icon>\n                    {{ i18n \"pages.xray.warpRoutingDesc\" }}\n                </template>\n            </a-alert>\n        </a-row>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.warpRouting\" }}</template>\n            <template #control>\n                <template v-if=\"WarpExist\">\n                    <a-select mode=\"tags\" :style=\"{ width: '100%' }\"\n                        v-model=\"warpDomains\"\n                        :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                        <a-select-option :value=\"p.value\" :label=\"p.label\"\n                            v-for=\"p in settingsData.ServicesOptions\">\n                            <span>[[ p.label ]]</span>\n                        </a-select-option>\n                    </a-select>\n                </template>\n                <template v-else>\n                    <a-button type=\"primary\" icon=\"cloud\"\n                        @click=\"showWarp()\">WARP</a-button>\n                </template>\n            </template>\n        </a-setting-list-item>\n    </a-collapse-panel>\n    <a-collapse-panel key=\"6\"\n        header='{{ i18n \"pages.settings.resetDefaultConfig\"}}'>\n        <a-space direction=\"horizontal\" :style=\"{ padding: '0 20px' }\">\n            <a-button type=\"danger\" @click=\"resetXrayConfigToDefault\">\n                <span>{{ i18n \"pages.settings.resetDefaultConfig\" }}</span>\n            </a-button>\n        </a-space>\n    </a-collapse-panel>\n</a-collapse>\n{{end}}"
  },
  {
    "path": "web/html/settings/xray/dns.html",
    "content": "{{define \"settings/xray/dns\"}}\n<a-collapse default-active-key=\"1\">\n    <a-collapse-panel key=\"1\" header='{{ i18n \"pages.xray.generalConfigs\"}}'>\n        <a-setting-list-item paddings=\"small\">\n            <template #title>{{ i18n \"pages.xray.dns.enable\" }}</template>\n            <template #description>{{ i18n \"pages.xray.dns.enableDesc\" }}</template>\n            <template #control>\n                <a-switch v-model=\"enableDNS\"></a-switch>\n            </template>\n        </a-setting-list-item>\n        <template v-if=\"enableDNS\">\n            <a-setting-list-item paddings=\"small\">\n                <template #title>{{ i18n \"pages.xray.dns.tag\" }}</template>\n                <template #description>{{ i18n \"pages.xray.dns.tagDesc\" }}</template>\n                <template #control>\n                    <a-input type=\"text\" v-model=\"dnsTag\"></a-input>\n                </template>\n            </a-setting-list-item>\n            <a-setting-list-item paddings=\"small\">\n                <template #title>{{ i18n \"pages.xray.dns.clientIp\" }}</template>\n                <template #description>{{ i18n \"pages.xray.dns.clientIpDesc\" }}</template>\n                <template #control>\n                    <a-input type=\"text\" v-model=\"dnsClientIp\"></a-input>\n                </template>\n            </a-setting-list-item>\n            <a-setting-list-item paddings=\"small\">\n                <template #title>{{ i18n \"pages.xray.dns.strategy\" }}</template>\n                <template #description>{{ i18n \"pages.xray.dns.strategyDesc\" }}</template>\n                <template #control>\n                    <a-select v-model=\"dnsStrategy\" :style=\"{ width: '100%' }\"\n                        :dropdown-class-name=\"themeSwitcher.currentTheme\">\n                        <a-select-option :value=\"l\" :label=\"l\" v-for=\"l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']\">\n                            <span>[[ l ]]</span>\n                        </a-select-option>\n                    </a-select>\n                </template>\n            </a-setting-list-item>\n            <a-setting-list-item paddings=\"small\">\n                <template #title>{{ i18n \"pages.xray.dns.disableCache\" }}</template>\n                <template #description>{{ i18n \"pages.xray.dns.disableCacheDesc\" }}</template>\n                <template #control>\n                    <a-switch v-model=\"dnsDisableCache\"></a-switch>\n                </template>\n            </a-setting-list-item>\n            <a-setting-list-item paddings=\"small\">\n                <template #title>{{ i18n \"pages.xray.dns.disableFallback\" }}</template>\n                <template #description>{{ i18n \"pages.xray.dns.disableFallbackDesc\" }}</template>\n                <template #control>\n                    <a-switch v-model=\"dnsDisableFallback\"></a-switch>\n                </template>\n            </a-setting-list-item>\n            <a-setting-list-item paddings=\"small\">\n                <template #title>{{ i18n \"pages.xray.dns.disableFallbackIfMatch\" }}</template>\n                <template #description>{{ i18n \"pages.xray.dns.disableFallbackIfMatchDesc\" }}</template>\n                <template #control>\n                    <a-switch v-model=\"dnsDisableFallbackIfMatch\"></a-switch>\n                </template>\n            </a-setting-list-item>\n            <a-setting-list-item paddings=\"small\">\n                <template #title>{{ i18n \"pages.xray.dns.enableParallelQuery\" }}</template>\n                <template #description>{{ i18n \"pages.xray.dns.enableParallelQueryDesc\" }}</template>\n                <template #control>\n                    <a-switch v-model=\"dnsEnableParallelQuery\"></a-switch>\n                </template>\n            </a-setting-list-item>\n\n            <a-setting-list-item paddings=\"small\">\n                <template #title>{{ i18n \"pages.xray.dns.useSystemHosts\" }}</template>\n                <template #description>{{ i18n \"pages.xray.dns.useSystemHostsDesc\" }}</template>\n                <template #control>\n                    <a-switch v-model=\"dnsUseSystemHosts\"></a-switch>\n                </template>\n            </a-setting-list-item>\n        </template>\n    </a-collapse-panel>\n    <template v-if=\"enableDNS\">\n        <a-collapse-panel key=\"2\" header='DNS'>\n            <template v-if=\"dnsServers.length > 0\">\n                <a-space direction=\"vertical\" size=\"middle\">\n                    <a-button type=\"primary\" icon=\"plus\" @click=\"addDNSServer()\">\n                        <span>{{ i18n \"pages.xray.dns.add\" }}</span>\n                    </a-button>\n                    <a-table :columns=\"dnsColumns\" bordered :row-key=\"r => r.key\" :data-source=\"dnsServers\"\n                        :scroll=\"isMobile ? {} : { x: 200 }\" :pagination=\"false\" :indent-size=\"0\"\n                        :locale='{ filterConfirm: `{{ i18n \"confirm\" }}`, filterReset: `{{ i18n \"reset\" }}` }'>\n                        <template slot=\"action\" slot-scope=\"text,dns,index\">\n                            <span>[[ index+1 ]]</span>\n                            <a-dropdown :trigger=\"['click']\">\n                                <a-icon @click=\"e => e.preventDefault()\" type=\"more\"\n                                    :style=\"{ fontSize: '16px', textDecoration: 'bold' }\"></a-icon>\n                                <a-menu slot=\"overlay\" :theme=\"themeSwitcher.currentTheme\">\n                                    <a-menu-item @click=\"editDNSServer(index)\">\n                                        <a-icon type=\"edit\"></a-icon>\n                                        <span>{{ i18n \"edit\" }}</span>\n                                    </a-menu-item>\n                                    <a-menu-item @click=\"deleteDNSServer(index)\">\n                                        <span :style=\"{ color: '#FF4D4F' }\">\n                                            <a-icon type=\"delete\"></a-icon>\n                                            <span>{{ i18n \"delete\"}}</span>\n                                        </span>\n                                    </a-menu-item>\n                                </a-menu>\n                            </a-dropdown>\n                        </template>\n                        <template slot=\"address\" slot-scope=\"dns,index\">\n                            <span v-if=\"typeof dns == 'object'\">[[ dns.address ]]</span>\n                            <span v-else>[[ dns ]]</span>\n                        </template>\n                        <template slot=\"domain\" slot-scope=\"dns,index\">\n                            <span v-if=\"typeof dns == 'object'\">[[ dns.domains.join(\",\") ]]</span>\n                        </template>\n                        <template slot=\"expectIPs\" slot-scope=\"dns,index\">\n                            <span v-if=\"typeof dns == 'object'\">[[ dns.expectIPs.join(\",\") ]]</span>\n                        </template>\n                    </a-table>\n                </a-space>\n            </template>\n            <template v-else>\n                <a-empty description='{{ i18n \"emptyDnsDesc\" }}' :style=\"{ margin: '10px' }\">\n                    <a-button-group>\n                        <a-button type=\"primary\" icon=\"plus\" @click=\"addDNSServer()\">\n                            <span>{{ i18n \"pages.xray.dns.add\" }}</span>\n                        </a-button>\n                        <a-button type=\"primary\" icon=\"menu\" @click=\"openDNSPresets()\"></a-button>\n                    </a-button-group>\n                </a-empty>\n            </template>\n        </a-collapse-panel>\n        <a-collapse-panel key=\"3\" header='Fake DNS'>\n            <template v-if=\"fakeDns && fakeDns.length > 0\">\n                <a-space direction=\"vertical\" size=\"middle\">\n                    <a-button type=\"primary\" icon=\"plus\" @click=\"addFakedns()\">{{ i18n \"pages.xray.fakedns.add\"\n                        }}</a-button>\n                    <a-table :columns=\"fakednsColumns\" bordered :row-key=\"r => r.key\" :data-source=\"fakeDns\"\n                        :scroll=\"isMobile ? {} : { x: 200 }\" :pagination=\"false\" :indent-size=\"0\"\n                        :locale='{ filterConfirm: `{{ i18n \"confirm\" }}`, filterReset: `{{ i18n \"reset\" }}` }'>\n                        <template slot=\"action\" slot-scope=\"text,fakedns,index\">\n                            <span>[[ index+1 ]]</span>\n                            <a-dropdown :trigger=\"['click']\">\n                                <a-icon @click=\"e => e.preventDefault()\" type=\"more\"\n                                    :style=\"{ fontSize: '16px', textDecoration: 'bold' }\"></a-icon>\n                                <a-menu slot=\"overlay\" :theme=\"themeSwitcher.currentTheme\">\n                                    <a-menu-item @click=\"editFakedns(index)\">\n                                        <a-icon type=\"edit\"></a-icon>\n                                        <span>{{ i18n \"edit\" }}</span>\n                                    </a-menu-item>\n                                    <a-menu-item @click=\"deleteFakedns(index)\">\n                                        <span :style=\"{ color: '#FF4D4F' }\">\n                                            <a-icon type=\"delete\"></a-icon>\n                                            <span>{{ i18n \"delete\"}}</span>\n                                        </span>\n                                    </a-menu-item>\n                                </a-menu>\n                            </a-dropdown>\n                        </template>\n                    </a-table>\n                </a-space>\n            </template>\n            <template v-else>\n                <a-empty description='{{ i18n \"emptyFakeDnsDesc\" }}' :style=\"{ margin: '20px' }\">\n                    <a-button type=\"primary\" icon=\"plus\" @click=\"addFakedns()\" :style=\"{ marginTop: '10px' }\">\n                        <span>{{ i18n \"pages.xray.fakedns.add\" }}</span>\n                    </a-button>\n                </a-empty>\n            </template>\n        </a-collapse-panel>\n    </template>\n</a-collapse>\n{{end}}"
  },
  {
    "path": "web/html/settings/xray/outbounds.html",
    "content": "{{define \"settings/xray/outbounds\"}}\n<a-space direction=\"vertical\" size=\"middle\">\n    <a-row>\n        <a-col :xs=\"12\" :sm=\"12\" :lg=\"12\">\n            <a-space direction=\"horizontal\" size=\"small\">\n                <a-button type=\"primary\" icon=\"plus\" @click=\"addOutbound\">\n                    <span v-if=\"!isMobile\">{{ i18n\n                        \"pages.xray.outbound.addOutbound\" }}</span>\n                </a-button>\n                <a-button type=\"primary\" icon=\"cloud\"\n                    @click=\"showWarp()\">WARP</a-button>\n            </a-space>\n        </a-col>\n        <a-col :xs=\"12\" :sm=\"12\" :lg=\"12\" :style=\"{ textAlign: 'right' }\">\n            <a-button-group>\n                <a-button icon=\"sync\" @click=\"refreshOutboundTraffic()\"\n                    :loading=\"refreshing\"></a-button>\n                <a-popconfirm placement=\"topRight\"\n                    @confirm=\"resetOutboundTraffic(-1)\"\n                    title='{{ i18n \"pages.inbounds.resetTrafficContent\"}}'\n                    :overlay-class-name=\"themeSwitcher.currentTheme\"\n                    ok-text='{{ i18n \"reset\"}}'\n                    cancel-text='{{ i18n \"cancel\"}}'>\n                    <a-icon slot=\"icon\" type=\"question-circle-o\"\n                        :style=\"{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }\"></a-icon>\n                    <a-button icon=\"retweet\"></a-button>\n                </a-popconfirm>\n            </a-button-group>\n        </a-col>\n    </a-row>\n    <a-table :columns=\"outboundColumns\" bordered :row-key=\"r => r.key\"\n        :data-source=\"outboundData\"\n        :scroll=\"isMobile ? {} : { x: 800 }\" :pagination=\"false\"\n        :indent-size=\"0\"\n        :locale='{ filterConfirm: `{{ i18n \"confirm\" }}`, filterReset: `{{ i18n \"reset\" }}` }'>\n        <template slot=\"action\" slot-scope=\"text, outbound, index\">\n            <span>[[ index+1 ]]</span>\n            <a-dropdown :trigger=\"['click']\">\n                <a-icon @click=\"e => e.preventDefault()\" type=\"more\"\n                    :style=\"{ fontSize: '16px', textDecoration: 'bold' }\"></a-icon>\n                <a-menu slot=\"overlay\" :theme=\"themeSwitcher.currentTheme\">\n                    <a-menu-item v-if=\"index>0\"\n                        @click=\"setFirstOutbound(index)\">\n                        <a-icon type=\"vertical-align-top\"></a-icon>\n                        <span>{{ i18n \"pages.xray.rules.first\"}}</span>\n                    </a-menu-item>\n                    <a-menu-item @click=\"editOutbound(index)\">\n                        <a-icon type=\"edit\"></a-icon>\n                        <span>{{ i18n \"edit\" }}</span>\n                    </a-menu-item>\n                    <a-menu-item @click=\"resetOutboundTraffic(index)\">\n                        <span>\n                            <a-icon type=\"retweet\"></a-icon>\n                            <span>{{ i18n \"pages.inbounds.resetTraffic\"}}</span>\n                        </span>\n                    </a-menu-item>\n                    <a-menu-item @click=\"deleteOutbound(index)\">\n                        <span :style=\"{ color: '#FF4D4F' }\">\n                            <a-icon type=\"delete\"></a-icon>\n                            <span>{{ i18n \"delete\"}}</span>\n                        </span>\n                    </a-menu-item>\n                </a-menu>\n            </a-dropdown>\n        </template>\n        <template slot=\"address\" slot-scope=\"text, outbound, index\">\n            <p :style=\"{ margin: '0 5px' }\"\n                v-for=\"addr in findOutboundAddress(outbound)\">[[ addr ]]</p>\n        </template>\n        <template slot=\"protocol\" slot-scope=\"text, outbound, index\">\n            <a-tag :style=\"{ margin: '0' }\" color=\"purple\">[[ outbound.protocol\n                ]]</a-tag>\n            <template\n                v-if=\"[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)\">\n                <a-tag :style=\"{ margin: '0' }\" color=\"blue\">[[\n                    outbound.streamSettings.network ]]</a-tag>\n                <a-tag :style=\"{ margin: '0' }\"\n                    v-if=\"outbound.streamSettings.security=='tls'\"\n                    color=\"green\">tls</a-tag>\n                <a-tag :style=\"{ margin: '0' }\"\n                    v-if=\"outbound.streamSettings.security=='reality'\"\n                    color=\"green\">reality</a-tag>\n            </template>\n        </template>\n        <template slot=\"traffic\" slot-scope=\"text, outbound, index\">\n            <a-tag color=\"green\">[[ findOutboundTraffic(outbound) ]]</a-tag>\n        </template>\n        <template slot=\"test\" slot-scope=\"text, outbound, index\">\n            <a-tooltip>\n                <template slot=\"title\">{{ i18n \"pages.xray.outbound.test\"\n                    }}</template>\n                <a-button\n                    type=\"primary\"\n                    shape=\"circle\"\n                    icon=\"thunderbolt\"\n                    :loading=\"outboundTestStates[index] && outboundTestStates[index].testing\"\n                    @click=\"testOutbound(index)\"\n                    :disabled=\"(outbound.protocol === 'blackhole' || outbound.tag === 'blocked') || (outboundTestStates[index] && outboundTestStates[index].testing)\">\n                </a-button>\n            </a-tooltip>\n        </template>\n        <template slot=\"testResult\" slot-scope=\"text, outbound, index\">\n            <div\n                v-if=\"outboundTestStates[index] && outboundTestStates[index].result\">\n                <a-tag v-if=\"outboundTestStates[index].result.success\"\n                    color=\"green\">\n                    [[ outboundTestStates[index].result.delay ]]ms\n                    <span v-if=\"outboundTestStates[index].result.statusCode\">\n                        ([[ outboundTestStates[index].result.statusCode\n                        ]])</span>\n                </a-tag>\n                <a-tooltip v-else\n                    :title=\"outboundTestStates[index].result.error\">\n                    <a-tag color=\"red\">\n                        Failed\n                    </a-tag>\n                </a-tooltip>\n            </div>\n            <span\n                v-else-if=\"outboundTestStates[index] && outboundTestStates[index].testing\">\n                <a-icon type=\"loading\" />\n            </span>\n            <span v-else>-</span>\n        </template>\n    </a-table>\n</a-space>\n{{end}}"
  },
  {
    "path": "web/html/settings/xray/reverse.html",
    "content": "{{define \"settings/xray/reverse\"}}\n<template v-if=\"reverseData.length > 0\">\n    <a-space direction=\"vertical\" size=\"middle\">\n        <a-button type=\"primary\" icon=\"plus\" @click=\"addReverse()\">\n            <span>{{ i18n \"pages.xray.outbound.addReverse\" }}</span>\n        </a-button>\n        <a-table :columns=\"reverseColumns\" bordered :row-key=\"r => r.key\" :data-source=\"reverseData\"\n            :scroll=\"isMobile ? {} : { x: 200 }\" :pagination=\"false\" :indent-size=\"0\"\n            :locale='{ filterConfirm: `{{ i18n \"confirm\" }}`, filterReset: `{{ i18n \"reset\" }}` }'>\n            <template slot=\"action\" slot-scope=\"text, reverse, index\">\n                <span>[[ index+1 ]]</span>\n                <a-dropdown :trigger=\"['click']\">\n                    <a-icon @click=\"e => e.preventDefault()\" type=\"more\"\n                        :style=\"{ fontSize: '16px', textDecoration: 'bold' }\"></a-icon>\n                    <a-menu slot=\"overlay\" :theme=\"themeSwitcher.currentTheme\">\n                        <a-menu-item @click=\"editReverse(index)\">\n                            <a-icon type=\"edit\"></a-icon>\n                            <span>{{ i18n \"edit\" }}</span>\n                        </a-menu-item>\n                        <a-menu-item @click=\"deleteReverse(index)\">\n                            <span :style=\"{ color: '#FF4D4F' }\">\n                                <a-icon type=\"delete\"></a-icon>\n                                <span>{{ i18n \"delete\"}}</span>\n                            </span>\n                        </a-menu-item>\n                    </a-menu>\n                </a-dropdown>\n            </template>\n        </a-table>\n    </a-space>\n</template>\n<template v-else>\n    <a-empty description='{{ i18n \"emptyReverseDesc\" }}' :style=\"{ margin: '10px' }\">\n        <a-button type=\"primary\" icon=\"plus\" @click=\"addReverse()\" :style=\"{ marginTop: '10px' }\">\n            {{ i18n \"pages.xray.outbound.addReverse\" }}\n        </a-button>\n    </a-empty>\n</template>\n{{end}}"
  },
  {
    "path": "web/html/settings/xray/routing.html",
    "content": "{{define \"settings/xray/routing\"}}\n<a-space direction=\"vertical\" size=\"middle\">\n    <a-button type=\"primary\" icon=\"plus\" @click=\"addRule\">{{ i18n \"pages.xray.rules.add\" }}</a-button>\n    <a-table-sortable :columns=\"isMobile ? rulesMobileColumns : rulesColumns\" bordered :row-key=\"r => r.key\"\n        :data-source=\"routingRuleData\" :scroll=\"isMobile ? {} : { x: 1000 }\" :pagination=\"false\" :indent-size=\"0\"\n        v-on:onSort=\"replaceRule\">\n        <template slot=\"action\" slot-scope=\"text, rule, index\">\n            <a-table-sort-trigger :item-index=\"index\"></a-table-sort-trigger>\n            <span class=\"ant-table-row-index\"> [[ index+1 ]] </span>\n            <a-dropdown :trigger=\"['click']\">\n                <a-icon @click=\"e => e.preventDefault()\" type=\"more\"\n                    :style=\"{ fontSize: '16px', textDecoration: 'bold' }\"></a-icon>\n                <a-menu slot=\"overlay\" :theme=\"themeSwitcher.currentTheme\">\n                    <a-menu-item v-if=\"index>0\" @click=\"replaceRule(index,0)\">\n                        <a-icon type=\"vertical-align-top\"></a-icon>\n                        {{ i18n \"pages.xray.rules.first\"}}\n                    </a-menu-item>\n                    <a-menu-item v-if=\"index>0\" @click=\"replaceRule(index,index-1)\">\n                        <a-icon type=\"arrow-up\"></a-icon>\n                        {{ i18n \"pages.xray.rules.up\"}}\n                    </a-menu-item>\n                    <a-menu-item v-if=\"index<routingRuleData.length-1\" @click=\"replaceRule(index,index+1)\">\n                        <a-icon type=\"arrow-down\"></a-icon>\n                        {{ i18n \"pages.xray.rules.down\"}}\n                    </a-menu-item>\n                    <a-menu-item v-if=\"index<routingRuleData.length-1\"\n                        @click=\"replaceRule(index,routingRuleData.length-1)\">\n                        <a-icon type=\"vertical-align-bottom\"></a-icon>\n                        {{ i18n \"pages.xray.rules.last\"}}\n                    </a-menu-item>\n                    <a-menu-item @click=\"editRule(index)\">\n                        <a-icon type=\"edit\"></a-icon>\n                        {{ i18n \"edit\" }}\n                    </a-menu-item>\n                    <a-menu-item @click=\"deleteRule(index)\">\n                        <span :style=\"{ color: '#FF4D4F' }\">\n                            <a-icon type=\"delete\"></a-icon> {{ i18n \"delete\"}}\n                        </span>\n                    </a-menu-item>\n                </a-menu>\n            </a-dropdown>\n        </template>\n        <template slot=\"inbound\" slot-scope=\"text, rule, index\">\n            <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n                <template slot=\"content\">\n                    <p v-if=\"rule.inboundTag\">Inbound Tag: [[ rule.inboundTag ]]</p>\n                    <p v-if=\"rule.user\">User email: [[ rule.user ]]</p>\n                </template>\n                [[ [rule.inboundTag,rule.user].join('\\n') ]]\n            </a-popover>\n        </template>\n        <template slot=\"outbound\" slot-scope=\"text, rule, index\">\n            <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n                <template slot=\"content\">\n                    <p v-if=\"rule.outboundTag\">Outbound Tag: [[ rule.outboundTag ]]</p>\n                </template>\n                [[ rule.outboundTag ]]\n            </a-popover>\n        </template>\n        <template slot=\"balancer\" slot-scope=\"text, rule, index\">\n            <a-popover :overlay-class-name=\"themeSwitcher.currentTheme\">\n                <template slot=\"content\">\n                    <p v-if=\"rule.balancerTag\">Balancer Tag: [[ rule.balancerTag ]]</p>\n                </template>\n                [[ rule.balancerTag ]]\n            </a-popover>\n        </template>\n        <template slot=\"info\" slot-scope=\"text, rule, index\">\n            <a-popover placement=\"bottomRight\"\n                v-if=\"(rule.sourceIP+rule.sourcePort+rule.vlessRoute+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0\"\n                :overlay-class-name=\"themeSwitcher.currentTheme\" trigger=\"click\">\n                <template slot=\"content\">\n                    <table cellpadding=\"2\" :style=\"{ maxWidth: '300px' }\">\n                        <tr v-if=\"rule.sourceIP\">\n                            <td>Source IP</td>\n                            <td><a-tag color=\"blue\" v-for=\"r in rule.sourceIP.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.sourcePort\">\n                            <td>Source Port</td>\n                            <td><a-tag color=\"green\" v-for=\"r in rule.sourcePort.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.vlessRoute\">\n                            <td>VLESS Route</td>\n                            <td><a-tag color=\"geekblue\" v-for=\"r in rule.vlessRoute.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.network\">\n                            <td>Network</td>\n                            <td><a-tag color=\"blue\" v-for=\"r in rule.network.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.protocol\">\n                            <td>Protocol</td>\n                            <td><a-tag color=\"green\" v-for=\"r in rule.protocol.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.attrs\">\n                            <td>Attrs</td>\n                            <td><a-tag color=\"blue\" v-for=\"r in rule.attrs.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.ip\">\n                            <td>IP</td>\n                            <td><a-tag color=\"green\" v-for=\"r in rule.ip.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.domain\">\n                            <td>Domain</td>\n                            <td><a-tag color=\"blue\" v-for=\"r in rule.domain.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.port\">\n                            <td>Port</td>\n                            <td><a-tag color=\"green\" v-for=\"r in rule.port.split(',')\">[[ r ]]</a-tag></td>\n                        </tr>\n                        <tr v-if=\"rule.balancerTag\">\n                            <td>Balancer Tag</td>\n                            <td><a-tag color=\"blue\">[[ rule.balancerTag ]]</a-tag></td>\n                        </tr>\n                    </table>\n                </template>\n                <a-button shape=\"round\" size=\"small\" :style=\"{ fontSize: '14px', padding: '0 10px' }\">\n                    <a-icon type=\"info\"></a-icon>\n                </a-button>\n            </a-popover>\n        </template>\n    </a-table-sortable>\n</a-space>\n{{end}}"
  },
  {
    "path": "web/html/settings.html",
    "content": "{{ template \"page/head_start\" .}}\n{{ template \"page/head_end\" .}}\n\n{{ template \"page/body_start\" .}}\n<a-layout id=\"app\" v-cloak :class=\"themeSwitcher.currentTheme + ' settings-page'\">\n  <a-sidebar></a-sidebar>\n  <a-layout id=\"content-layout\">\n    <a-layout-content>\n      <a-spin :spinning=\"loadingStates.spinning\" :delay=\"500\" tip='{{ i18n \"loading\"}}'>\n        <transition name=\"list\" appear>\n          <a-alert type=\"error\" v-if=\"confAlerts.length>0 && loadingStates.fetched\" :style=\"{ marginBottom: '10px' }\"\n            message='{{ i18n \"secAlertTitle\" }}' color=\"red\" show-icon closable>\n            <template slot=\"description\">\n              <b>{{ i18n \"secAlertConf\" }}</b>\n              <ul>\n                <li v-for=\"a in confAlerts\">[[ a ]]</li>\n              </ul>\n            </template>\n          </a-alert>\n        </transition>\n        <transition name=\"list\" appear>\n          <template>\n            <a-row v-if=\"!loadingStates.fetched\">\n              <a-card\n                :style=\"{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }\">\n                <a-spin tip='{{ i18n \"loading\" }}'></a-spin>\n              </a-card>\n            </a-row>\n            <a-row :gutter=\"[isMobile ? 8 : 16, isMobile ? 0 : 12]\" v-else>\n              <a-col>\n                <a-card hoverable>\n                  <a-row :style=\"{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }\">\n                    <a-col :xs=\"24\" :sm=\"10\" :style=\"{ padding: '4px' }\">\n                      <a-space direction=\"horizontal\">\n                        <a-button type=\"primary\" :disabled=\"saveBtnDisable\" @click=\"updateAllSetting\">{{ i18n\n                          \"pages.settings.save\" }}</a-button>\n                        <a-button type=\"danger\" :disabled=\"!saveBtnDisable\" @click=\"restartPanel\">{{ i18n\n                          \"pages.settings.restartPanel\" }}</a-button>\n                      </a-space>\n                    </a-col>\n                    <a-col :xs=\"24\" :sm=\"14\">\n                      <template>\n                        <div>\n                          <a-back-top :target=\"() => document.getElementById('content-layout')\"\n                            visibility-height=\"200\"></a-back-top>\n                          <a-alert type=\"warning\" :style=\"{ float: 'right', width: 'fit-content' }\"\n                            message='{{ i18n \"pages.settings.infoDesc\" }}' show-icon>\n                          </a-alert>\n                        </div>\n                      </template>\n                    </a-col>\n                  </a-row>\n                </a-card>\n              </a-col>\n              <a-col>\n                <a-tabs default-active-key=\"1\">\n                  <a-tab-pane key=\"1\" :style=\"{ paddingTop: '20px' }\">\n                    <template #tab>\n                      <a-icon type=\"setting\"></a-icon>\n                      <span>{{ i18n \"pages.settings.panelSettings\" }}</span>\n                    </template>\n                    {{ template \"settings/panel/general\" . }}\n                  </a-tab-pane>\n                  <a-tab-pane key=\"2\" :style=\"{ paddingTop: '20px' }\">\n                    <template #tab>\n                      <a-icon type=\"safety\"></a-icon>\n                      <span>{{ i18n \"pages.settings.securitySettings\" }}</span>\n                    </template>\n                    {{ template \"settings/panel/security\" . }}\n                  </a-tab-pane>\n                  <a-tab-pane key=\"3\" :style=\"{ paddingTop: '20px' }\">\n                    <template #tab>\n                      <a-icon type=\"message\"></a-icon>\n                      <span>{{ i18n \"pages.settings.TGBotSettings\" }}</span>\n                    </template>\n                    {{ template \"settings/panel/telegram\" . }}\n                  </a-tab-pane>\n                  <a-tab-pane key=\"4\" :style=\"{ paddingTop: '20px' }\">\n                    <template #tab>\n                      <a-icon type=\"cloud-server\"></a-icon>\n                      <span>{{ i18n \"pages.settings.subSettings\" }}</span>\n                    </template>\n                    {{ template \"settings/panel/subscription/general\" . }}\n                  </a-tab-pane>\n                  <a-tab-pane key=\"5\" v-if=\"allSetting.subJsonEnable\" :style=\"{ paddingTop: '20px' }\">\n                    <template #tab>\n                      <a-icon type=\"code\"></a-icon>\n                      <span>{{ i18n \"pages.settings.subSettings\" }} (JSON)</span>\n                    </template>\n                    {{ template \"settings/panel/subscription/json\" . }}\n                  </a-tab-pane>\n                </a-tabs>\n              </a-col>\n            </a-row>\n          </template>\n        </transition>\n      </a-spin>\n    </a-layout-content>\n  </a-layout>\n</a-layout>\n{{template \"page/body_scripts\" .}}\n<script src=\"{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/otpauth/otpauth.umd.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}\"></script>\n{{template \"component/aSidebar\" .}}\n{{template \"component/aThemeSwitch\" .}}\n{{template \"component/aSettingListItem\" .}}\n{{template \"modals/twoFactorModal\"}}\n<script>\n  const app = new Vue({\n    delimiters: ['[[', ']]'],\n    mixins: [MediaQueryMixin],\n    el: '#app',\n    data: {\n      themeSwitcher,\n      loadingStates: {\n        fetched: false,\n        spinning: false\n      },\n      oldAllSetting: new AllSetting(),\n      allSetting: new AllSetting(),\n      saveBtnDisable: true,\n      entryHost: null,\n      entryPort: null,\n      entryProtocol: null,\n      entryIsIP: false,\n      user: {},\n      lang: LanguageManager.getLanguage(),\n      inboundOptions: [],\n      remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' },\n      remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'],\n      datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }],\n      remarkSample: '',\n      defaultFragment: {\n        tag: \"fragment\",\n        protocol: \"freedom\",\n        settings: {\n          domainStrategy: \"AsIs\",\n          fragment: {\n            packets: \"tlshello\",\n            length: \"100-200\",\n            interval: \"10-20\",\n            maxSplit: \"300-400\"\n          }\n        },\n        streamSettings: {\n          sockopt: {\n            tcpKeepAliveIdle: 100,\n            tcpMptcp: true,\n            penetrate: true\n          }\n        }\n      },\n      defaultNoises: {\n        tag: \"noises\",\n        protocol: \"freedom\",\n        settings: {\n          domainStrategy: \"AsIs\",\n          noises: [\n            { type: \"rand\", packet: \"10-20\", delay: \"10-16\", applyTo: \"ip\" },\n          ],\n        },\n      },\n      defaultMux: {\n        enabled: true,\n        concurrency: 8,\n        xudpConcurrency: 16,\n        xudpProxyUDP443: \"reject\"\n      },\n      defaultRules: [\n        {\n          type: \"field\",\n          outboundTag: \"direct\",\n          domain: [\n            \"geosite:category-ir\"\n          ]\n        },\n        {\n          type: \"field\",\n          outboundTag: \"direct\",\n          ip: [\n            \"geoip:private\",\n            \"geoip:ir\"\n          ]\n        },\n      ],\n      directIPsOptions: [\n        { label: 'Private IP', value: 'geoip:private' },\n        { label: '🇮🇷 Iran', value: 'geoip:ir' },\n        { label: '🇨🇳 China', value: 'geoip:cn' },\n        { label: '🇷🇺 Russia', value: 'geoip:ru' },\n        { label: '🇻🇳 Vietnam', value: 'geoip:vn' },\n        { label: '🇪🇸 Spain', value: 'geoip:es' },\n        { label: '🇮🇩 Indonesia', value: 'geoip:id' },\n        { label: '🇺🇦 Ukraine', value: 'geoip:ua' },\n        { label: '🇹🇷 Türkiye', value: 'geoip:tr' },\n        { label: '🇧🇷 Brazil', value: 'geoip:br' },\n      ],\n      diretDomainsOptions: [\n        { label: 'Private DNS', value: 'geosite:private' },\n        { label: '🇮🇷 Iran', value: 'geosite:category-ir' },\n        { label: '🇨🇳 China', value: 'geosite:cn' },\n        { label: '🇷🇺 Russia', value: 'geosite:category-ru' },\n        { label: 'Apple', value: 'geosite:apple' },\n        { label: 'Meta', value: 'geosite:meta' },\n        { label: 'Google', value: 'geosite:google' },\n      ],\n      get remarkModel() {\n        rm = this.allSetting.remarkModel;\n        return rm.length > 1 ? rm.substring(1).split('') : [];\n      },\n      set remarkModel(value) {\n        rs = this.allSetting.remarkModel[0];\n        this.allSetting.remarkModel = rs + value.join('');\n        this.changeRemarkSample();\n      },\n      get remarkSeparator() {\n        return this.allSetting.remarkModel.length > 1 ? this.allSetting.remarkModel.charAt(0) : '-';\n      },\n      set remarkSeparator(value) {\n        this.allSetting.remarkModel = value + this.allSetting.remarkModel.substring(1);\n        this.changeRemarkSample();\n      },\n      get datepicker() {\n        return this.allSetting.datepicker ? this.allSetting.datepicker : 'gregorian';\n      },\n      set datepicker(value) {\n        this.allSetting.datepicker = value;\n      },\n      changeRemarkSample() {\n        sample = []\n        this.remarkModel.forEach(r => sample.push(this.remarkModels[r]));\n        this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator);\n      }\n    },\n    methods: {\n      loading(spinning = true) {\n        this.loadingStates.spinning = spinning;\n      },\n      _isIp(h) {\n        if (typeof h !== \"string\") return false;\n\n        // IPv4: four dot-separated octets 0-255\n        const v4 = h.split(\".\");\n        if (\n          v4.length === 4 &&\n          v4.every(p => /^\\d{1,3}$/.test(p) && Number(p) <= 255)\n        ) return true;\n\n        // IPv6: hex groups, optional single :: compression\n        if (!h.includes(\":\") || h.includes(\":::\")) return false;\n        const parts = h.split(\"::\");\n        if (parts.length > 2) return false;\n\n        const splitGroups = s => (s ? s.split(\":\").filter(Boolean) : []);\n        const head = splitGroups(parts[0]);\n        const tail = splitGroups(parts[1]);\n        const validGroup = seg => /^[0-9a-fA-F]{1,4}$/.test(seg);\n\n        if (![...head, ...tail].every(validGroup)) return false;\n        const groups = head.length + tail.length;\n\n        return parts.length === 2 ? groups < 8 : groups === 8;\n      },\n      async getAllSetting() {\n        const msg = await HttpUtil.post(\"/panel/setting/all\");\n\n        if (msg.success) {\n          if (!this.loadingStates.fetched) {\n            this.loadingStates.fetched = true\n          }\n\n          this.oldAllSetting = new AllSetting(msg.obj);\n          this.allSetting = new AllSetting(msg.obj);\n          app.changeRemarkSample();\n          this.saveBtnDisable = true;\n        }\n      },\n      async loadInboundTags() {\n        const msg = await HttpUtil.get(\"/panel/api/inbounds/list\");\n        if (msg && msg.success && Array.isArray(msg.obj)) {\n          this.inboundOptions = msg.obj.map(ib => ({\n            label: `${ib.tag} (${ib.protocol}@${ib.port})`,\n            value: ib.tag,\n          }));\n        } else {\n          this.inboundOptions = [];\n        }\n      },\n      async updateAllSetting() {\n        this.loading(true);\n        const msg = await HttpUtil.post(\"/panel/setting/update\", this.allSetting);\n        this.loading(false);\n        if (msg.success) {\n          await this.getAllSetting();\n        }\n      },\n      async updateUser() {\n        const sendUpdateUserRequest = async () => {\n          this.loading(true);\n          const msg = await HttpUtil.post(\"/panel/setting/updateUser\", this.user);\n          this.loading(false);\n          if (msg.success) {\n            this.user = {};\n            window.location.replace(basePath + \"logout\");\n          }\n        }\n\n        if (this.allSetting.twoFactorEnable) {\n          twoFactorModal.show({\n            title: '{{ i18n \"pages.settings.security.twoFactorModalChangeCredentialsTitle\" }}',\n            description: '{{ i18n \"pages.settings.security.twoFactorModalChangeCredentialsStep\" }}',\n            token: this.allSetting.twoFactorToken,\n            type: 'confirm',\n            confirm: (success) => {\n              if (success) {\n                sendUpdateUserRequest();\n              }\n            }\n          })\n        } else {\n          sendUpdateUserRequest();\n        }\n      },\n      async restartPanel() {\n        await new Promise(resolve => {\n          this.$confirm({\n            title: '{{ i18n \"pages.settings.restartPanel\" }}',\n            content: '{{ i18n \"pages.settings.restartPanelDesc\" }}',\n            class: themeSwitcher.currentTheme,\n            okText: '{{ i18n \"sure\" }}',\n            cancelText: '{{ i18n \"cancel\" }}',\n            onOk: () => resolve(),\n          });\n        });\n        this.loading(true);\n        const msg = await HttpUtil.post(\"/panel/setting/restartPanel\");\n        this.loading(false);\n        if (!msg.success) return;\n\n        this.loading(true);\n        await PromiseUtil.sleep(5000);\n\n        const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = this.allSetting;\n        const newProtocol = (webCertFile || webKeyFile) ? \"https:\" : \"http:\";\n\n        let base = webBasePath ? webBasePath.replace(/^\\//, \"\") : \"\";\n        if (base && !base.endsWith(\"/\")) base += \"/\";\n\n        if (!this.entryIsIP) {\n          const url = new URL(window.location.href);\n          url.pathname = `/${base}panel/settings`;\n          url.protocol = newProtocol;\n          window.location.replace(url.toString());\n          return;\n        }\n\n        let finalHost = this.entryHost;\n        let finalPort = this.entryPort || \"\";\n\n        if (webDomain && this._isIp(webDomain)) {\n          finalHost = webDomain;\n        }\n\n        if (webPort && Number(webPort) !== Number(this.entryPort)) {\n          finalPort = String(webPort);\n        }\n\n        const url = new URL(`${newProtocol}//${finalHost}`);\n        if (finalPort) url.port = finalPort;\n        url.pathname = `/${base}panel/settings`;\n\n        window.location.replace(url.toString());\n      },\n      toggleTwoFactor(newValue) {\n        if (newValue) {\n          const newTwoFactorToken = RandomUtil.randomBase32String()\n\n          twoFactorModal.show({\n            title: '{{ i18n \"pages.settings.security.twoFactorModalSetTitle\" }}',\n            token: newTwoFactorToken,\n            type: 'set',\n            confirm: (success) => {\n              if (success) {\n                Vue.prototype.$message['success']('{{ i18n \"pages.settings.security.twoFactorModalSetSuccess\" }}')\n\n                this.allSetting.twoFactorToken = newTwoFactorToken\n              }\n\n              this.allSetting.twoFactorEnable = success\n            }\n          })\n        } else {\n          twoFactorModal.show({\n            title: '{{ i18n \"pages.settings.security.twoFactorModalDeleteTitle\" }}',\n            description: '{{ i18n \"pages.settings.security.twoFactorModalRemoveStep\" }}',\n            token: this.allSetting.twoFactorToken,\n            type: 'confirm',\n            confirm: (success) => {\n              if (success) {\n                Vue.prototype.$message['success']('{{ i18n \"pages.settings.security.twoFactorModalDeleteSuccess\" }}')\n\n                this.allSetting.twoFactorEnable = false\n                this.allSetting.twoFactorToken = \"\"\n              }\n            }\n          })\n        }\n      },\n      addNoise() {\n        const newNoise = { type: \"rand\", packet: \"10-20\", delay: \"10-16\", applyTo: \"ip\" };\n        this.noisesArray = [...this.noisesArray, newNoise];\n      },\n      removeNoise(index) {\n        const newNoises = [...this.noisesArray];\n        newNoises.splice(index, 1);\n        this.noisesArray = newNoises;\n      },\n      updateNoiseType(index, value) {\n        const updatedNoises = [...this.noisesArray];\n        updatedNoises[index] = { ...updatedNoises[index], type: value };\n        this.noisesArray = updatedNoises;\n      },\n      updateNoisePacket(index, value) {\n        const updatedNoises = [...this.noisesArray];\n        updatedNoises[index] = { ...updatedNoises[index], packet: value };\n        this.noisesArray = updatedNoises;\n      },\n      updateNoiseDelay(index, value) {\n        const updatedNoises = [...this.noisesArray];\n        updatedNoises[index] = { ...updatedNoises[index], delay: value };\n        this.noisesArray = updatedNoises;\n      },\n      updateNoiseApplyTo(index, value) {\n        const updatedNoises = [...this.noisesArray];\n        updatedNoises[index] = { ...updatedNoises[index], applyTo: value };\n        this.noisesArray = updatedNoises;\n      },\n    },\n    computed: {\n      ldapInboundTagList: {\n        get: function () {\n          const csv = this.allSetting.ldapInboundTags || \"\";\n          return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];\n        },\n        set: function (list) {\n          this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';\n        }\n      },\n      fragment: {\n        get: function () { return this.allSetting?.subJsonFragment != \"\"; },\n        set: function (v) {\n          this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : \"\";\n        }\n      },\n      fragmentPackets: {\n        get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.packets : \"\"; },\n        set: function (v) {\n          if (v != \"\") {\n            newFragment = JSON.parse(this.allSetting.subJsonFragment);\n            newFragment.settings.fragment.packets = v;\n            this.allSetting.subJsonFragment = JSON.stringify(newFragment);\n          }\n        }\n      },\n      fragmentLength: {\n        get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.length : \"\"; },\n        set: function (v) {\n          if (v != \"\") {\n            newFragment = JSON.parse(this.allSetting.subJsonFragment);\n            newFragment.settings.fragment.length = v;\n            this.allSetting.subJsonFragment = JSON.stringify(newFragment);\n          }\n        }\n      },\n      fragmentInterval: {\n        get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.interval : \"\"; },\n        set: function (v) {\n          if (v != \"\") {\n            newFragment = JSON.parse(this.allSetting.subJsonFragment);\n            newFragment.settings.fragment.interval = v;\n            this.allSetting.subJsonFragment = JSON.stringify(newFragment);\n          }\n        }\n      },\n      fragmentMaxSplit: {\n        get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.maxSplit : \"\"; },\n        set: function (v) {\n          if (v != \"\") {\n            newFragment = JSON.parse(this.allSetting.subJsonFragment);\n            newFragment.settings.fragment.maxSplit = v;\n            this.allSetting.subJsonFragment = JSON.stringify(newFragment);\n          }\n        }\n      },\n      noises: {\n        get() {\n          return this.allSetting?.subJsonNoises != \"\";\n        },\n        set(v) {\n          if (v) {\n            this.allSetting.subJsonNoises = JSON.stringify(this.defaultNoises);\n          } else {\n            this.allSetting.subJsonNoises = \"\";\n          }\n        }\n      },\n      noisesArray: {\n        get() {\n          return this.noises ? JSON.parse(this.allSetting.subJsonNoises).settings.noises : [];\n        },\n        set(value) {\n          if (this.noises) {\n            const newNoises = JSON.parse(this.allSetting.subJsonNoises);\n            newNoises.settings.noises = value;\n            this.allSetting.subJsonNoises = JSON.stringify(newNoises);\n          }\n        }\n      },\n      enableMux: {\n        get: function () { return this.allSetting?.subJsonMux != \"\"; },\n        set: function (v) {\n          this.allSetting.subJsonMux = v ? JSON.stringify(this.defaultMux) : \"\";\n        }\n      },\n      muxConcurrency: {\n        get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1; },\n        set: function (v) {\n          newMux = JSON.parse(this.allSetting.subJsonMux);\n          newMux.concurrency = v;\n          this.allSetting.subJsonMux = JSON.stringify(newMux);\n        }\n      },\n      muxXudpConcurrency: {\n        get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1; },\n        set: function (v) {\n          newMux = JSON.parse(this.allSetting.subJsonMux);\n          newMux.xudpConcurrency = v;\n          this.allSetting.subJsonMux = JSON.stringify(newMux);\n        }\n      },\n      muxXudpProxyUDP443: {\n        get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : \"reject\"; },\n        set: function (v) {\n          newMux = JSON.parse(this.allSetting.subJsonMux);\n          newMux.xudpProxyUDP443 = v;\n          this.allSetting.subJsonMux = JSON.stringify(newMux);\n        }\n      },\n      enableDirect: {\n        get: function () { return this.allSetting?.subJsonRules != \"\"; },\n        set: function (v) {\n          this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : \"\";\n        }\n      },\n      directIPs: {\n        get: function () {\n          if (!this.enableDirect) return [];\n          const rules = JSON.parse(this.allSetting.subJsonRules);\n          if (!Array.isArray(rules)) return [];\n          const ipRule = rules.find(r => r.ip);\n          return ipRule?.ip ?? [];\n        },\n        set: function (v) {\n          let rules = JSON.parse(this.allSetting.subJsonRules);\n          if (!Array.isArray(rules)) return;\n\n          if (v.length == 0) {\n            rules = rules.filter(r => !r.ip);\n          } else {\n            let ruleIndex = rules.findIndex(r => r.ip);\n            if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[1]) - 1;\n\n            rules[ruleIndex].ip = [];\n            v.forEach(d => {\n              rules[ruleIndex].ip.push(d);\n            });\n          }\n          this.allSetting.subJsonRules = JSON.stringify(rules);\n        }\n      },\n      directDomains: {\n        get: function () {\n          if (!this.enableDirect) return [];\n          const rules = JSON.parse(this.allSetting.subJsonRules);\n          if (!Array.isArray(rules)) return [];\n          const domainRule = rules.find(r => r.domain);\n          return domainRule?.domain ?? [];\n        },\n        set: function (v) {\n          let rules = JSON.parse(this.allSetting.subJsonRules);\n          if (!Array.isArray(rules)) return;\n          if (v.length == 0) {\n            rules = rules.filter(r => !r.domain);\n          } else {\n            let ruleIndex = rules.findIndex(r => r.domain);\n            if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[0]) - 1;\n\n            rules[ruleIndex].domain = v;\n          }\n          this.allSetting.subJsonRules = JSON.stringify(rules);\n        }\n      },\n      confAlerts: {\n        get: function () {\n          if (!this.allSetting) return [];\n          var alerts = []\n          if (window.location.protocol !== \"https:\") alerts.push('{{ i18n \"secAlertSSL\" }}');\n          if (this.allSetting.webPort === 2053) alerts.push('{{ i18n \"secAlertPanelPort\" }}');\n          panelPath = window.location.pathname.split('/').length < 4\n          if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n \"secAlertPanelURI\" }}');\n          if (this.allSetting.subEnable) {\n            subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;\n            if (subPath == '/sub/') alerts.push('{{ i18n \"secAlertSubURI\" }}');\n          }\n          if (this.allSetting.subJsonEnable) {\n            subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;\n            if (subJsonPath == '/json/') alerts.push('{{ i18n \"secAlertSubJsonURI\" }}');\n          }\n          return alerts\n        }\n      }\n    },\n    async mounted() {\n      this.entryHost = window.location.hostname;\n      this.entryPort = window.location.port;\n      this.entryProtocol = window.location.protocol;\n      this.entryIsIP = this._isIp(this.entryHost);\n      await this.getAllSetting();\n      await this.loadInboundTags();\n      while (true) {\n        await PromiseUtil.sleep(1000);\n        this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);\n      }\n    }\n  });\n</script>\n{{ template \"page/body_end\" .}}"
  },
  {
    "path": "web/html/xray.html",
    "content": "{{ template \"page/head_start\" .}}\n<link rel=\"stylesheet\"\n  href=\"{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}\">\n<link rel=\"stylesheet\"\n  href=\"{{ .base_path }}assets/codemirror/fold/foldgutter.css\">\n<link rel=\"stylesheet\"\n  href=\"{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}\">\n<link rel=\"stylesheet\" href=\"{{ .base_path }}assets/codemirror/lint/lint.css\">\n{{ template \"page/head_end\" .}}\n\n{{ template \"page/body_start\" .}}\n<a-layout id=\"app\" v-cloak :class=\"themeSwitcher.currentTheme + ' xray-page'\">\n  <a-sidebar></a-sidebar>\n  <a-layout id=\"content-layout\">\n    <a-layout-content>\n      <a-spin :spinning=\"loadingStates.spinning\" :delay=\"500\"\n        tip='{{ i18n \"loading\"}}'>\n        <transition name=\"list\" appear>\n          <a-alert type=\"error\" v-if=\"showAlert && loadingStates.fetched\"\n            :style=\"{ marginBottom: '10px' }\"\n            message='{{ i18n \"secAlertTitle\" }}' color=\"red\"\n            description='{{ i18n \"secAlertSsl\" }}' show-icon closable>\n          </a-alert>\n        </transition>\n        <transition name=\"list\" appear>\n          <a-row v-if=\"!loadingStates.fetched\">\n            <a-card\n              :style=\"{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }\">\n              <a-spin tip='{{ i18n \"loading\" }}'></a-spin>\n            </a-card>\n          </a-row>\n          <a-row :gutter=\"[isMobile ? 8 : 16, isMobile ? 0 : 12]\" v-else>\n            <a-col>\n              <a-card hoverable>\n                <a-row\n                  :style=\"{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }\">\n                  <a-col :xs=\"24\" :sm=\"10\" :style=\"{ padding: '4px' }\">\n                    <a-space direction=\"horizontal\">\n                      <a-button type=\"primary\" :disabled=\"saveBtnDisable\"\n                        @click=\"updateXraySetting\">\n                        {{ i18n \"pages.xray.save\" }}\n                      </a-button>\n                      <a-button type=\"danger\" :disabled=\"!saveBtnDisable\"\n                        @click=\"restartXray\">\n                        {{ i18n \"pages.xray.restart\" }}\n                      </a-button>\n                      <a-popover v-if=\"restartResult\"\n                        :overlay-class-name=\"themeSwitcher.currentTheme\">\n                        <span slot=\"title\">{{ i18n\n                          \"pages.index.xrayErrorPopoverTitle\" }}</span>\n                        <template slot=\"content\">\n                          <span :style=\"{ maxWidth: '400px' }\"\n                            v-for=\"line in restartResult.split('\\n')\">[[ line\n                            ]]</span>\n                        </template>\n                        <a-icon type=\"question-circle\"></a-icon>\n                      </a-popover>\n                    </a-space>\n                  </a-col>\n                  <a-col :xs=\"24\" :sm=\"14\">\n                    <template>\n                      <div>\n                        <a-back-top\n                          :target=\"() => document.getElementById('content-layout')\"\n                          visibility-height=\"200\"></a-back-top>\n                        <a-alert type=\"warning\"\n                          :style=\"{ float: 'right', width: 'fit-content' }\"\n                          message='{{ i18n \"pages.settings.infoDesc\" }}'\n                          show-icon>\n                        </a-alert>\n                      </div>\n                    </template>\n                  </a-col>\n                </a-row>\n              </a-card>\n            </a-col>\n            <a-col>\n              <a-tabs default-active-key=\"tpl-basic\"\n                @change=\"(activeKey) => { this.changePage(activeKey); }\"\n                :class=\"themeSwitcher.currentTheme\">\n                <a-tab-pane key=\"tpl-basic\" :style=\"{ paddingTop: '20px' }\">\n                  <template #tab>\n                    <a-icon type=\"setting\"></a-icon>\n                    <span>{{ i18n \"pages.xray.basicTemplate\"}}</span>\n                  </template>\n                  {{ template \"settings/xray/basics\" . }}\n                </a-tab-pane>\n                <a-tab-pane key=\"tpl-routing\" :style=\"{ paddingTop: '20px' }\">\n                  <template #tab>\n                    <a-icon type=\"swap\"></a-icon>\n                    <span>{{ i18n \"pages.xray.Routings\"}}</span>\n                  </template>\n                  {{ template \"settings/xray/routing\" . }}\n                </a-tab-pane>\n                <a-tab-pane key=\"tpl-outbound\" force-render=\"true\">\n                  <template #tab>\n                    <a-icon type=\"upload\"></a-icon>\n                    <span>{{ i18n \"pages.xray.Outbounds\"}}</span>\n                  </template>\n                  {{ template \"settings/xray/outbounds\" . }}\n                </a-tab-pane>\n                <a-tab-pane key=\"tpl-reverse\" :style=\"{ paddingTop: '20px' }\"\n                  force-render=\"true\">\n                  <template #tab>\n                    <a-icon type=\"import\"></a-icon>\n                    <span>{{ i18n \"pages.xray.outbound.reverse\"}}</span>\n                  </template>\n                  {{ template \"settings/xray/reverse\" . }}\n                </a-tab-pane>\n                <a-tab-pane key=\"tpl-balancer\" :style=\"{ paddingTop: '20px' }\"\n                  force-render=\"true\">\n                  <template #tab>\n                    <a-icon type=\"cluster\"></a-icon>\n                    <span>{{ i18n \"pages.xray.Balancers\"}}</span>\n                  </template>\n                  {{ template \"settings/xray/balancers\" . }}\n                </a-tab-pane>\n                <a-tab-pane key=\"tpl-dns\" :style=\"{ paddingTop: '20px' }\"\n                  force-render=\"true\">\n                  <template #tab>\n                    <a-icon type=\"database\"></a-icon>\n                    <span>DNS</span>\n                  </template>\n                  {{ template \"settings/xray/dns\" . }}\n                </a-tab-pane>\n                <a-tab-pane key=\"tpl-advanced\" force-render=\"true\">\n                  <template #tab>\n                    <a-icon type=\"code\"></a-icon>\n                    <span>{{ i18n \"pages.xray.advancedTemplate\"}}</span>\n                  </template>\n                  {{ template \"settings/xray/advanced\" . }}\n                </a-tab-pane>\n              </a-tabs>\n            </a-col>\n          </a-row>\n        </transition>\n      </a-spin>\n    </a-layout-content>\n  </a-layout>\n</a-layout>\n{{template \"page/body_scripts\" .}}\n<script\n  src=\"{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}\"></script>\n<script\n  src=\"{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}\"></script>\n<script src=\"{{ .base_path }}assets/codemirror/javascript.js\"></script>\n<script src=\"{{ .base_path }}assets/codemirror/jshint.js\"></script>\n<script src=\"{{ .base_path }}assets/codemirror/jsonlint.js\"></script>\n<script src=\"{{ .base_path }}assets/codemirror/lint/lint.js\"></script>\n<script\n  src=\"{{ .base_path }}assets/codemirror/lint/javascript-lint.js\"></script>\n<script\n  src=\"{{ .base_path }}assets/codemirror/hint/javascript-hint.js\"></script>\n<script src=\"{{ .base_path }}assets/codemirror/fold/foldcode.js\"></script>\n<script src=\"{{ .base_path }}assets/codemirror/fold/foldgutter.js\"></script>\n<script src=\"{{ .base_path }}assets/codemirror/fold/brace-fold.js\"></script>\n{{template \"component/aSidebar\" .}}\n{{template \"component/aThemeSwitch\" .}}\n{{template \"component/aTableSortable\" .}}\n{{template \"component/aSettingListItem\" .}}\n{{template \"modals/ruleModal\"}}\n{{template \"modals/outModal\"}}\n{{template \"modals/reverseModal\"}}\n{{template \"modals/balancerModal\"}}\n{{template \"modals/dnsModal\"}}\n{{template \"modals/dnsPresetsModal\"}}\n{{template \"modals/fakednsModal\"}}\n{{template \"modals/warpModal\"}}\n<script>\n  const rulesColumns = [\n    { title: \"#\", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },\n    {\n      title: '{{ i18n \"pages.xray.rules.source\"}}', children: [\n        { title: 'IP', dataIndex: \"sourceIP\", align: 'center', width: 20, ellipsis: true },\n        { title: '{{ i18n \"pages.inbounds.port\" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true },\n        { title: 'VLESS Route', dataIndex: 'vlessRoute', align: 'center', width: 15, ellipsis: true }]\n    },\n    {\n      title: '{{ i18n \"pages.inbounds.network\"}}', children: [\n        { title: 'L4', dataIndex: 'network', align: 'center', width: 10 },\n        { title: '{{ i18n \"protocol\" }}', dataIndex: 'protocol', align: 'center', width: 15, ellipsis: true },\n        { title: 'Attrs', dataIndex: 'attrs', align: 'center', width: 10, ellipsis: true }]\n    },\n    {\n      title: '{{ i18n \"pages.xray.rules.dest\"}}', children: [\n        { title: 'IP', dataIndex: 'ip', align: 'center', width: 20, ellipsis: true },\n        { title: '{{ i18n \"pages.xray.outbound.domain\" }}', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true },\n        { title: '{{ i18n \"pages.inbounds.port\" }}', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]\n    },\n    {\n      title: '{{ i18n \"pages.xray.rules.inbound\"}}', children: [\n        { title: '{{ i18n \"pages.xray.outbound.tag\" }}', dataIndex: 'inboundTag', align: 'center', width: 15, ellipsis: true },\n        { title: '{{ i18n \"pages.inbounds.client\" }}', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]\n    },\n    { title: '{{ i18n \"pages.xray.rules.outbound\"}}', dataIndex: 'outboundTag', align: 'center', width: 17 },\n    { title: '{{ i18n \"pages.xray.rules.balancer\"}}', dataIndex: 'balancerTag', align: 'center', width: 15 },\n  ];\n\n  const rulesMobileColumns = [\n    { title: \"#\", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },\n    { title: '{{ i18n \"pages.xray.rules.inbound\"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'inbound' } },\n    { title: '{{ i18n \"pages.xray.rules.outbound\"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'outbound' } },\n    { title: '{{ i18n \"pages.xray.rules.info\"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'info' } },\n  ];\n\n  const outboundColumns = [\n    { title: \"#\", align: 'center', width: 60, scopedSlots: { customRender: 'action' } },\n    { title: '{{ i18n \"pages.xray.outbound.tag\"}}', dataIndex: 'tag', align: 'center', width: 50 },\n    { title: '{{ i18n \"protocol\"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },\n    { title: '{{ i18n \"pages.xray.outbound.address\"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },\n    { title: '{{ i18n \"pages.inbounds.traffic\" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } },\n    { title: '{{ i18n \"pages.xray.outbound.testResult\" }}', align: 'center', width: 120, scopedSlots: { customRender: 'testResult' } },\n    { title: '{{ i18n \"pages.xray.outbound.test\" }}', align: 'center', width: 60, scopedSlots: { customRender: 'test' } },\n  ];\n\n  const reverseColumns = [\n    { title: \"#\", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },\n    { title: '{{ i18n \"pages.xray.outbound.type\"}}', dataIndex: 'type', align: 'center', width: 50 },\n    { title: '{{ i18n \"pages.xray.outbound.tag\"}}', dataIndex: 'tag', align: 'center', width: 50 },\n    { title: '{{ i18n \"pages.xray.outbound.domain\"}}', dataIndex: 'domain', align: 'center', width: 50 },\n  ];\n\n  const balancerColumns = [\n    { title: \"#\", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },\n    { title: '{{ i18n \"pages.xray.balancer.tag\"}}', dataIndex: 'tag', align: 'center', width: 50 },\n    { title: '{{ i18n \"pages.xray.balancer.balancerStrategy\"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' } },\n    { title: '{{ i18n \"pages.xray.balancer.balancerSelectors\"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' } },\n  ];\n\n  const dnsColumns = [\n    { title: \"#\", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },\n    { title: '{{ i18n \"pages.xray.outbound.address\"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },\n    { title: '{{ i18n \"pages.xray.dns.domains\"}}', align: 'center', width: 50, scopedSlots: { customRender: 'domain' } },\n    { title: '{{ i18n \"pages.xray.dns.expectIPs\"}}', align: 'center', width: 50, scopedSlots: { customRender: 'expectIPs' } },\n  ];\n\n  const fakednsColumns = [\n    { title: \"#\", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },\n    { title: '{{ i18n \"pages.xray.fakedns.ipPool\"}}', dataIndex: 'ipPool', align: 'center', width: 50 },\n    { title: '{{ i18n \"pages.xray.fakedns.poolSize\"}}', dataIndex: 'poolSize', align: 'center', width: 50 },\n  ];\n\n  const app = new Vue({\n    delimiters: ['[[', ']]'],\n    mixins: [MediaQueryMixin],\n    el: '#app',\n    data: {\n      themeSwitcher,\n      isDarkTheme: themeSwitcher.isDarkTheme,\n      loadingStates: {\n        fetched: false,\n        spinning: false\n      },\n      oldXraySetting: '',\n      xraySetting: '',\n      outboundTestUrl: 'https://www.google.com/generate_204',\n      oldOutboundTestUrl: 'https://www.google.com/generate_204',\n      inboundTags: [],\n      outboundsTraffic: [],\n      outboundTestStates: {}, // Track testing state and results for each outbound\n      saveBtnDisable: true,\n      refreshing: false,\n      restartResult: '',\n      showAlert: false,\n      advSettings: 'xraySetting',\n      obsSettings: '',\n      cm: null,\n      cmOptions: {\n        lineNumbers: true,\n        mode: \"application/json\",\n        lint: true,\n        styleActiveLine: true,\n        matchBrackets: true,\n        theme: \"xq\",\n        autoCloseTags: true,\n        lineWrapping: true,\n        indentUnit: 2,\n        indentWithTabs: true,\n        smartIndent: true,\n        tabSize: 2,\n        lineWiseCopyCut: false,\n        foldGutter: true,\n        gutters: [\n          \"CodeMirror-lint-markers\",\n          \"CodeMirror-linenumbers\",\n          \"CodeMirror-foldgutter\",\n        ],\n      },\n      ipv4Settings: {\n        tag: \"IPv4\",\n        protocol: \"freedom\",\n        settings: {\n          domainStrategy: \"UseIPv4\"\n        }\n      },\n      directSettings: {\n        tag: \"direct\",\n        protocol: \"freedom\"\n      },\n      routingDomainStrategies: [\"AsIs\", \"IPIfNonMatch\", \"IPOnDemand\"],\n      log: {\n        loglevel: [\"none\", \"debug\", \"info\", \"warning\", \"error\"],\n        access: [\"none\", \"./access.log\"],\n        error: [\"none\", \"./error.log\"],\n        dnsLog: false,\n        maskAddress: [\"quarter\", \"half\", \"full\"],\n      },\n      settingsData: {\n        protocols: {\n          bittorrent: [\"bittorrent\"],\n        },\n        IPsOptions: [\n          { label: 'Private IPs', value: 'geoip:private' },\n          { label: '🇮🇷 Iran', value: 'ext:geoip_IR.dat:ir' },\n          { label: '🇨🇳 China', value: 'geoip:cn' },\n          { label: '🇷🇺 Russia', value: 'ext:geoip_RU.dat:ru' },\n          { label: '🇻🇳 Vietnam', value: 'geoip:vn' },\n          { label: '🇪🇸 Spain', value: 'geoip:es' },\n          { label: '🇮🇩 Indonesia', value: 'geoip:id' },\n          { label: '🇺🇦 Ukraine', value: 'geoip:ua' },\n          { label: '🇹🇷 Türkiye', value: 'geoip:tr' },\n          { label: '🇧🇷 Brazil', value: 'geoip:br' },\n        ],\n        DomainsOptions: [\n          { label: '🇮🇷 Iran', value: 'ext:geosite_IR.dat:ir' },\n          { label: '🇮🇷 .ir', value: 'regexp:.*\\\\.ir$' },\n          { label: '🇮🇷 .ایران', value: 'regexp:.*\\\\.xn--mgba3a4f16a$' },\n          { label: '🇨🇳 China', value: 'geosite:cn' },\n          { label: '🇨🇳 .cn', value: 'regexp:.*\\\\.cn$' },\n          { label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },\n          { label: '🇷🇺 .ru', value: 'regexp:.*\\\\.ru$' },\n          { label: '🇷🇺 .su', value: 'regexp:.*\\\\.su$' },\n          { label: '🇷🇺 .рф', value: 'regexp:.*\\\\.xn--p1ai$' },\n          { label: '🇻🇳 .vn', value: 'regexp:.*\\\\.vn$' },\n        ],\n        BlockDomainsOptions: [\n          { label: 'Ads All', value: 'geosite:category-ads-all' },\n          { label: 'Ads IR 🇮🇷', value: 'ext:geosite_IR.dat:category-ads-all' },\n          { label: 'Ads RU 🇷🇺', value: 'ext:geosite_RU.dat:category-ads-all' },\n          { label: 'Malware 🇮🇷', value: 'ext:geosite_IR.dat:malware' },\n          { label: 'Phishing 🇮🇷', value: 'ext:geosite_IR.dat:phishing' },\n          { label: 'Cryptominers 🇮🇷', value: 'ext:geosite_IR.dat:cryptominers' },\n          { label: 'Adult +18', value: 'geosite:category-porn' },\n          { label: '🇮🇷 Iran', value: 'ext:geosite_IR.dat:ir' },\n          { label: '🇮🇷 .ir', value: 'regexp:.*\\\\.ir$' },\n          { label: '🇮🇷 .ایران', value: 'regexp:.*\\\\.xn--mgba3a4f16a$' },\n          { label: '🇨🇳 China', value: 'geosite:cn' },\n          { label: '🇨🇳 .cn', value: 'regexp:.*\\\\.cn$' },\n          { label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },\n          { label: '🇷🇺 .ru', value: 'regexp:.*\\\\.ru$' },\n          { label: '🇷🇺 .su', value: 'regexp:.*\\\\.su$' },\n          { label: '🇷🇺 .рф', value: 'regexp:.*\\\\.xn--p1ai$' },\n          { label: '🇻🇳 .vn', value: 'regexp:.*\\\\.vn$' },\n        ],\n        ServicesOptions: [\n          { label: 'Apple', value: 'geosite:apple' },\n          { label: 'Meta', value: 'geosite:meta' },\n          { label: 'Google', value: 'geosite:google' },\n          { label: 'OpenAI', value: 'geosite:openai' },\n          { label: 'Spotify', value: 'geosite:spotify' },\n          { label: 'Netflix', value: 'geosite:netflix' },\n          { label: 'Reddit', value: 'geosite:reddit' },\n          { label: 'Speedtest', value: 'geosite:speedtest' },\n        ]\n      },\n      defaultObservatory: {\n        subjectSelector: [],\n        probeURL: \"https://www.google.com/generate_204\",\n        probeInterval: \"10m\",\n        enableConcurrency: true\n      },\n      defaultBurstObservatory: {\n        subjectSelector: [],\n        pingConfig: {\n          destination: \"https://www.google.com/generate_204\",\n          interval: \"30m\",\n          connectivity: \"http://connectivitycheck.platform.hicloud.com/generate_204\",\n          timeout: \"10s\",\n          sampling: 2\n        }\n      }\n    },\n    methods: {\n      loading(spinning = true) {\n        this.loadingStates.spinning = spinning;\n      },\n      async getOutboundsTraffic() {\n        const msg = await HttpUtil.get(\"/panel/xray/getOutboundsTraffic\");\n        if (msg.success) {\n          this.outboundsTraffic = msg.obj;\n        }\n      },\n      async getXraySetting() {\n        const msg = await HttpUtil.post(\"/panel/xray/\");\n\n        if (msg.success) {\n          if (!this.loadingStates.fetched) {\n            this.loadingStates.fetched = true\n          }\n\n          result = JSON.parse(msg.obj);\n          xs = JSON.stringify(result.xraySetting, null, 2);\n          this.oldXraySetting = xs;\n          this.xraySetting = xs;\n          this.inboundTags = result.inboundTags;\n          this.outboundTestUrl = result.outboundTestUrl || 'https://www.google.com/generate_204';\n          this.oldOutboundTestUrl = this.outboundTestUrl;\n          this.saveBtnDisable = true;\n        }\n      },\n      async updateXraySetting() {\n        this.loading(true);\n        const msg = await HttpUtil.post(\"/panel/xray/update\", {\n          xraySetting: this.xraySetting,\n          outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'\n        });\n        this.loading(false);\n        if (msg.success) {\n          await this.getXraySetting();\n        }\n      },\n      async restartXray() {\n        this.loading(true);\n        const msg = await HttpUtil.post(\"/panel/api/server/restartXrayService\");\n        this.loading(false);\n        if (msg.success) {\n          await PromiseUtil.sleep(500);\n          await this.getXrayResult();\n        }\n        this.loading(false);\n      },\n      async getXrayResult() {\n        const msg = await HttpUtil.get(\"/panel/xray/getXrayResult\");\n        if (msg.success) {\n          this.restartResult = msg.obj;\n          if (msg.obj.length > 1) Vue.prototype.$message.error(msg.obj);\n        }\n      },\n      async resetXrayConfigToDefault() {\n        this.loading(true);\n        const msg = await HttpUtil.get(\"/panel/setting/getDefaultJsonConfig\");\n        this.loading(false);\n        if (msg.success) {\n          this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));\n          this.saveBtnDisable = true;\n        }\n      },\n      changePage(pageKey) {\n        if (pageKey == 'tpl-advanced') this.changeCode();\n        if (pageKey == 'tpl-balancer') this.changeObsCode();\n      },\n      syncRulesWithOutbound(tag, setting) {\n        const newTemplateSettings = this.templateSettings;\n        const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);\n        const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);\n        if (!haveRules && outboundIndex > 0) {\n          newTemplateSettings.outbounds.splice(outboundIndex);\n        }\n        if (haveRules && outboundIndex < 0) {\n          newTemplateSettings.outbounds.push(setting);\n        }\n        this.templateSettings = newTemplateSettings;\n      },\n      templateRuleGetter(routeSettings) {\n        const { property, outboundTag } = routeSettings;\n        let result = [];\n        if (this.templateSettings != null) {\n          this.templateSettings.routing.rules.forEach(\n            (routingRule) => {\n              if (\n                routingRule.hasOwnProperty(property) &&\n                routingRule.hasOwnProperty(\"outboundTag\") &&\n                routingRule.outboundTag === outboundTag\n              ) {\n                result.push(...routingRule[property]);\n              }\n            }\n          );\n        }\n        return result;\n      },\n      templateRuleSetter(routeSettings) {\n        const { data, property, outboundTag } = routeSettings;\n        const oldTemplateSettings = this.templateSettings;\n        const newTemplateSettings = oldTemplateSettings;\n        currentProperty = this.templateRuleGetter({ outboundTag, property })\n        if (currentProperty.length == 0) {\n          const propertyRule = {\n            type: \"field\",\n            outboundTag,\n            [property]: data\n          };\n          newTemplateSettings.routing.rules.push(propertyRule);\n        }\n        else {\n          const newRules = [];\n          insertedOnce = false;\n          newTemplateSettings.routing.rules.forEach(\n            (routingRule) => {\n              if (\n                routingRule.hasOwnProperty(property) &&\n                routingRule.hasOwnProperty(\"outboundTag\") &&\n                routingRule.outboundTag === outboundTag\n              ) {\n                if (!insertedOnce && data.length > 0) {\n                  insertedOnce = true;\n                  routingRule[property] = data;\n                  newRules.push(routingRule);\n                }\n              }\n              else {\n                newRules.push(routingRule);\n              }\n            }\n          );\n          newTemplateSettings.routing.rules = newRules;\n        }\n        this.templateSettings = newTemplateSettings;\n      },\n      changeCode() {\n        if (this.cm != null) {\n          this.cm.toTextArea();\n        }\n        textAreaObj = document.getElementById('xraySetting');\n        textAreaObj.value = this[this.advSettings];\n        this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);\n        this.cm.on('change', editor => {\n          value = editor.getValue();\n          if (this.isJsonString(value)) {\n            this[this.advSettings] = value;\n          }\n        });\n      },\n      changeObsCode() {\n        if (this.cm != null) {\n          this.cm.toTextArea();\n        }\n        if (this.obsSettings == '') {\n          this.cm = null;\n          return\n        }\n        textAreaObj = document.getElementById('obsSetting');\n        textAreaObj.value = this[this.obsSettings];\n        this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);\n        this.cm.on('change', editor => {\n          value = editor.getValue();\n          if (this.isJsonString(value)) {\n            this[this.obsSettings] = value;\n          }\n        });\n      },\n      isJsonString(str) {\n        try {\n          JSON.parse(str);\n        } catch (e) {\n          return false;\n        }\n        return true;\n      },\n      findOutboundTraffic(o) {\n        for (const otraffic of this.outboundsTraffic) {\n          if (otraffic.tag == o.tag) {\n            return `↑ ${SizeFormatter.sizeFormat(otraffic.up)} / ${SizeFormatter.sizeFormat(otraffic.down)} ↓`\n          }\n        }\n        return `${SizeFormatter.sizeFormat(0)} / ${SizeFormatter.sizeFormat(0)}`\n      },\n      findOutboundAddress(o) {\n        serverObj = null;\n        switch (o.protocol) {\n          case Protocols.VMess:\n            serverObj = o.settings.vnext;\n            break;\n          case Protocols.VLESS:\n            return [o.settings?.address + ':' + o.settings?.port];\n          case Protocols.HTTP:\n          case Protocols.Socks:\n          case Protocols.Shadowsocks:\n          case Protocols.Trojan:\n            serverObj = o.settings.servers;\n            break;\n          case Protocols.DNS:\n            return [o.settings?.address + ':' + o.settings?.port];\n          case Protocols.Wireguard:\n            return o.settings.peers.map(peer => peer.endpoint);\n          default:\n            return null;\n        }\n        return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : null;\n      },\n      addOutbound() {\n        outModal.show({\n          title: '{{ i18n \"pages.xray.outbound.addOutbound\"}}',\n          okText: '{{ i18n \"pages.xray.outbound.addOutbound\" }}',\n          confirm: (outbound) => {\n            outModal.loading();\n            if (outbound.tag.length > 0) {\n              this.templateSettings.outbounds.push(outbound);\n              this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);\n            }\n            outModal.close();\n          },\n          isEdit: false,\n          tags: this.templateSettings.outbounds.map(obj => obj.tag)\n        });\n      },\n      editOutbound(index) {\n        outModal.show({\n          title: '{{ i18n \"pages.xray.outbound.editOutbound\"}} ' + (index + 1),\n          outbound: app.templateSettings.outbounds[index],\n          confirm: (outbound) => {\n            outModal.loading();\n            this.templateSettings.outbounds[index] = outbound;\n            this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);\n            outModal.close();\n          },\n          isEdit: true,\n          tags: this.outboundData.filter((o) => o.key != index).map(obj => obj.tag)\n        });\n      },\n      deleteOutbound(index) {\n        outbounds = this.templateSettings.outbounds;\n        outbounds.splice(index, 1);\n        this.outboundSettings = JSON.stringify(outbounds);\n      },\n      setFirstOutbound(index) {\n        outbounds = this.templateSettings.outbounds;\n        outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);\n        this.outboundSettings = JSON.stringify(outbounds);\n      },\n      async testOutbound(index) {\n        const outbound = this.templateSettings.outbounds[index];\n        if (!outbound) {\n          Vue.prototype.$message.error('{{ i18n \"pages.xray.outbound.testError\" }}');\n          return;\n        }\n\n        if (outbound.protocol === 'blackhole' || outbound.tag === 'blocked') {\n          Vue.prototype.$message.warning('{{ i18n \"pages.xray.outbound.testError\" }}: blocked/blackhole outbound');\n          return;\n        }\n\n        // Initialize test state for this outbound if not exists\n        if (!this.outboundTestStates[index]) {\n          this.$set(this.outboundTestStates, index, {\n            testing: false,\n            result: null\n          });\n        }\n\n        // Set testing state\n        this.$set(this.outboundTestStates[index], 'testing', true);\n        this.$set(this.outboundTestStates[index], 'result', null);\n\n        try {\n          const outboundJSON = JSON.stringify(outbound);\n          const allOutboundsJSON = JSON.stringify(this.templateSettings.outbounds || []);\n          \n          const msg = await HttpUtil.post(\"/panel/xray/testOutbound\", {\n            outbound: outboundJSON,\n            allOutbounds: allOutboundsJSON\n          });\n\n          // Update test state\n          this.$set(this.outboundTestStates[index], 'testing', false);\n\n          if (msg.success && msg.obj) {\n            const result = msg.obj;\n            this.$set(this.outboundTestStates[index], 'result', result);\n            \n            if (result.success) {\n              Vue.prototype.$message.success(\n                `{{ i18n \"pages.xray.outbound.testSuccess\" }}: ${result.delay}ms (${result.statusCode})`\n              );\n            } else {\n              Vue.prototype.$message.error(\n                `{{ i18n \"pages.xray.outbound.testFailed\" }}: ${result.error || 'Unknown error'}`\n              );\n            }\n          } else {\n            this.$set(this.outboundTestStates[index], 'result', {\n              success: false,\n              error: msg.msg || '{{ i18n \"pages.xray.outbound.testError\" }}'\n            });\n            Vue.prototype.$message.error(msg.msg || '{{ i18n \"pages.xray.outbound.testError\" }}');\n          }\n        } catch (error) {\n          this.$set(this.outboundTestStates[index], 'testing', false);\n          this.$set(this.outboundTestStates[index], 'result', {\n            success: false,\n            error: error.message || '{{ i18n \"pages.xray.outbound.testError\" }}'\n          });\n          Vue.prototype.$message.error('{{ i18n \"pages.xray.outbound.testError\" }}: ' + error.message);\n        }\n      },\n      addReverse() {\n        reverseModal.show({\n          title: '{{ i18n \"pages.xray.outbound.addReverse\"}}',\n          okText: '{{ i18n \"pages.xray.outbound.addReverse\" }}',\n          confirm: (reverse, rules) => {\n            reverseModal.loading();\n            if (reverse.tag.length > 0) {\n              newTemplateSettings = this.templateSettings;\n              if (newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {};\n              if (newTemplateSettings.reverse[reverse.type + 's'] == undefined) newTemplateSettings.reverse[reverse.type + 's'] = [];\n              newTemplateSettings.reverse[reverse.type + 's'].push({ tag: reverse.tag, domain: reverse.domain });\n              this.templateSettings = newTemplateSettings;\n\n              // Add related rules\n              this.templateSettings.routing.rules.push(...rules);\n              this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);\n            }\n            reverseModal.close();\n          },\n          isEdit: false\n        });\n      },\n      editReverse(index) {\n        if (this.reverseData[index].type == \"bridge\") {\n          oldRules = this.templateSettings.routing.rules.filter(r => r.inboundTag && r.inboundTag[0] == this.reverseData[index].tag);\n        } else {\n          oldRules = this.templateSettings.routing.rules.filter(r => r.outboundTag && r.outboundTag == this.reverseData[index].tag);\n        }\n        reverseModal.show({\n          title: '{{ i18n \"pages.xray.outbound.editReverse\"}} ' + (index + 1),\n          reverse: this.reverseData[index],\n          rules: oldRules,\n          confirm: (reverse, rules) => {\n            reverseModal.loading();\n            if (reverse.tag.length > 0) {\n              oldData = this.reverseData[index];\n              newTemplateSettings = this.templateSettings;\n              oldReverseIndex = newTemplateSettings.reverse[oldData.type + 's'].findIndex(rs => rs.tag == oldData.tag);\n              oldRuleIndex0 = oldRules.length > 0 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[0])) : -1;\n              oldRuleIndex1 = oldRules.length == 2 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[1])) : -1;\n              if (oldData.type == reverse.type) {\n                newTemplateSettings.reverse[oldData.type + 's'][oldReverseIndex] = { tag: reverse.tag, domain: reverse.domain };\n              } else {\n                newTemplateSettings.reverse[oldData.type + 's'].splice(oldReverseIndex, 1);\n                // delete empty object\n                if (newTemplateSettings.reverse[oldData.type + 's'].length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type + 's');\n                // add other type of reverse if it is not exist\n                if (!newTemplateSettings.reverse[reverse.type + 's']) newTemplateSettings.reverse[reverse.type + 's'] = [];\n                newTemplateSettings.reverse[reverse.type + 's'].push({ tag: reverse.tag, domain: reverse.domain });\n              }\n              this.templateSettings = newTemplateSettings;\n\n              // Adjust Rules\n              newRules = this.templateSettings.routing.rules;\n              oldRuleIndex0 != -1 ? newRules[oldRuleIndex0] = rules[0] : newRules.push(rules[0]);\n              oldRuleIndex1 != -1 ? newRules[oldRuleIndex1] = rules[1] : newRules.push(rules[1]);\n              this.routingRuleSettings = JSON.stringify(newRules);\n            }\n            reverseModal.close();\n          },\n          isEdit: true\n        });\n      },\n      deleteReverse(index) {\n        oldData = this.reverseData[index];\n        newTemplateSettings = this.templateSettings;\n        reverseTypeObj = newTemplateSettings.reverse[oldData.type + 's'];\n        realIndex = reverseTypeObj.findIndex(r => r.tag == oldData.tag && r.domain == oldData.domain);\n        newTemplateSettings.reverse[oldData.type + 's'].splice(realIndex, 1);\n\n        // delete empty objects\n        if (reverseTypeObj.length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type + 's');\n        if (Object.keys(newTemplateSettings.reverse).length === 0) Reflect.deleteProperty(newTemplateSettings, 'reverse');\n\n        // delete related routing rules\n        newRules = newTemplateSettings.routing.rules;\n        if (oldData.type == \"bridge\") {\n          newRules = newTemplateSettings.routing.rules.filter(r => !(r.inboundTag && r.inboundTag.length == 1 && r.inboundTag[0] == oldData.tag));\n        } else if (oldData.type == \"portal\") {\n          newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);\n        }\n        newTemplateSettings.routing.rules = newRules;\n\n        this.templateSettings = newTemplateSettings;\n      },\n      async refreshOutboundTraffic() {\n        if (!this.refreshing) {\n          this.refreshing = true;\n          await this.getOutboundsTraffic();\n\n          data = []\n          if (this.templateSettings != null) {\n            this.templateSettings.outbounds.forEach((o, index) => {\n              data.push({ 'key': index, ...o });\n            });\n          }\n\n          this.outboundData = data;\n          this.refreshing = false;\n        }\n      },\n      async resetOutboundTraffic(index) {\n        let tag = \"-alltags-\";\n        if (index >= 0) {\n          tag = this.outboundData[index].tag ? this.outboundData[index].tag : \"\"\n        }\n        const msg = await HttpUtil.post(\"/panel/xray/resetOutboundsTraffic\", { tag: tag });\n        if (msg.success) {\n          await this.refreshOutboundTraffic();\n        }\n      },\n      addBalancer() {\n        balancerModal.show({\n          title: '{{ i18n \"pages.xray.balancer.addBalancer\"}}',\n          okText: '{{ i18n \"pages.xray.balancer.addBalancer\"}}',\n          balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),\n          balancer: {\n            tag: '',\n            strategy: 'random',\n            selector: [],\n            fallbackTag: ''\n          },\n          confirm: (balancer) => {\n            balancerModal.loading();\n            newTemplateSettings = this.templateSettings;\n            if (newTemplateSettings.routing.balancers == undefined) {\n              newTemplateSettings.routing.balancers = [];\n            }\n            let tmpBalancer = {\n              'tag': balancer.tag,\n              'selector': balancer.selector,\n              'fallbackTag': balancer.fallbackTag\n            };\n            if (balancer.strategy && balancer.strategy != 'random') {\n              tmpBalancer.strategy = {\n                'type': balancer.strategy\n              };\n            }\n            newTemplateSettings.routing.balancers.push(tmpBalancer);\n            this.templateSettings = newTemplateSettings;\n            this.updateObservatorySelectors();\n            balancerModal.close();\n            this.changeObsCode();\n          },\n          isEdit: false\n        });\n      },\n      editBalancer(index) {\n        const oldTag = this.balancersData[index].tag;\n        balancerModal.show({\n          title: '{{ i18n \"pages.xray.balancer.editBalancer\"}}',\n          okText: '{{ i18n \"sure\" }}',\n          balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),\n          balancer: this.balancersData[index],\n          confirm: (balancer) => {\n            balancerModal.loading();\n            newTemplateSettings = this.templateSettings;\n\n            let tmpBalancer = {\n              'tag': balancer.tag,\n              'selector': balancer.selector,\n              'fallbackTag': balancer.fallbackTag\n            };\n\n            // Remove old tag\n            if (newTemplateSettings.observatory) {\n              newTemplateSettings.observatory.subjectSelector = newTemplateSettings.observatory.subjectSelector.filter(s => s != oldTag);\n            }\n            if (newTemplateSettings.burstObservatory) {\n              newTemplateSettings.burstObservatory.subjectSelector = newTemplateSettings.burstObservatory.subjectSelector.filter(s => s != oldTag);\n            }\n\n            if (balancer.strategy && balancer.strategy != 'random') {\n              tmpBalancer.strategy = {\n                'type': balancer.strategy\n              };\n            }\n\n            newTemplateSettings.routing.balancers[index] = tmpBalancer;\n            // change edited tag if used in rule section\n            if (oldTag != balancer.tag) {\n              newTemplateSettings.routing.rules.forEach((rule) => {\n                if (rule.balancerTag && rule.balancerTag == oldTag) {\n                  rule.balancerTag = balancer.tag;\n                }\n              });\n            }\n            this.templateSettings = newTemplateSettings;\n            this.updateObservatorySelectors();\n            balancerModal.close();\n            this.changeObsCode();\n          },\n          isEdit: true\n        });\n      },\n      updateObservatorySelectors() {\n        newTemplateSettings = this.templateSettings;\n        const leastPings = this.balancersData.filter((b) => b.strategy == 'leastPing');\n        const leastLoads = this.balancersData.filter((b) =>\n          b.strategy === 'leastLoad' ||\n          b.strategy === 'roundRobin' ||\n          b.strategy === 'random'\n        );\n        if (leastPings.length > 0) {\n          if (!newTemplateSettings.observatory)\n            newTemplateSettings.observatory = this.defaultObservatory;\n          newTemplateSettings.observatory.subjectSelector = [];\n          leastPings.forEach((b) => {\n            b.selector.forEach((s) => {\n              if (!newTemplateSettings.observatory.subjectSelector.includes(s))\n                newTemplateSettings.observatory.subjectSelector.push(s);\n            });\n          });\n        } else {\n          delete newTemplateSettings.observatory\n        }\n        if (leastLoads.length > 0) {\n          if (!newTemplateSettings.burstObservatory)\n            newTemplateSettings.burstObservatory = this.defaultBurstObservatory;\n          newTemplateSettings.burstObservatory.subjectSelector = [];\n          leastLoads.forEach((b) => {\n            b.selector.forEach((s) => {\n              if (!newTemplateSettings.burstObservatory.subjectSelector.includes(s))\n                newTemplateSettings.burstObservatory.subjectSelector.push(s);\n            });\n          });\n        } else {\n          delete newTemplateSettings.burstObservatory\n        }\n        this.templateSettings = newTemplateSettings;\n        this.changeObsCode();\n      },\n      deleteBalancer(index) {\n        newTemplateSettings = this.templateSettings;\n\n        // Remove from balancers\n        const removedBalancer = this.balancersData.splice(index, 1)[0];\n\n        // Remove from settings\n        let realIndex = newTemplateSettings.routing.balancers.findIndex((b) => b.tag === removedBalancer.tag);\n        newTemplateSettings.routing.balancers.splice(realIndex, 1);\n\n        // Update balancers property to an empty array if there are no more balancers\n        if (newTemplateSettings.routing.balancers.length === 0) {\n          delete newTemplateSettings.routing.balancers;\n        }\n        this.templateSettings = newTemplateSettings;\n        this.updateObservatorySelectors();\n        this.obsSettings = '';\n        this.changeObsCode()\n      },\n      openDNSPresets() {\n        dnsPresetsModal.show({\n          title: '{{ i18n \"pages.xray.dns.dnsPresetTitle\" }}',\n          selected: (selectedPreset) => {\n            this.dnsServers = selectedPreset;\n\n            dnsPresetsModal.close();\n          }\n        });\n      },\n      addDNSServer() {\n        dnsModal.show({\n          title: '{{ i18n \"pages.xray.dns.add\" }}',\n          confirm: (dnsServer) => {\n            dnsServers = this.dnsServers;\n            dnsServers.push(dnsServer);\n            this.dnsServers = dnsServers;\n            dnsModal.close();\n          },\n          isEdit: false\n        });\n      },\n      editDNSServer(index) {\n        dnsModal.show({\n          title: '{{ i18n \"pages.xray.dns.edit\" }} #' + (index + 1),\n          dnsServer: this.dnsServers[index],\n          confirm: (dnsServer) => {\n            dnsServers = this.dnsServers;\n            dnsServers[index] = dnsServer;\n            this.dnsServers = dnsServers;\n            dnsModal.close();\n          },\n          isEdit: true\n        });\n      },\n      deleteDNSServer(index) {\n        newDnsServers = this.dnsServers;\n        newDnsServers.splice(index, 1);\n        this.dnsServers = newDnsServers;\n      },\n      addFakedns() {\n        fakednsModal.show({\n          title: '{{ i18n \"pages.xray.fakedns.add\" }}',\n          confirm: (item) => {\n            fakeDns = this.fakeDns ?? [];\n            fakeDns.push(item);\n            this.fakeDns = fakeDns;\n            fakednsModal.close();\n          },\n          isEdit: false\n        });\n      },\n      editFakedns(index) {\n        fakednsModal.show({\n          title: '{{ i18n \"pages.xray.fakedns.edit\" }} #' + (index + 1),\n          fakeDns: this.fakeDns[index],\n          confirm: (item) => {\n            fakeDns = this.fakeDns;\n            fakeDns[index] = item;\n            this.fakeDns = fakeDns;\n            fakednsModal.close();\n          },\n          isEdit: true\n        });\n      },\n      deleteFakedns(index) {\n        fakeDns = this.fakeDns;\n        fakeDns.splice(index, 1);\n        this.fakeDns = fakeDns;\n      },\n      addRule() {\n        ruleModal.show({\n          title: '{{ i18n \"pages.xray.rules.add\"}}',\n          okText: '{{ i18n \"pages.xray.rules.add\" }}',\n          confirm: (rule) => {\n            ruleModal.loading();\n            if (JSON.stringify(rule).length > 3) {\n              this.templateSettings.routing.rules.push(rule);\n              this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);\n            }\n            ruleModal.close();\n          },\n          isEdit: false\n        });\n      },\n      editRule(index) {\n        ruleModal.show({\n          title: '{{ i18n \"pages.xray.rules.edit\"}} ' + (index + 1),\n          rule: app.templateSettings.routing.rules[index],\n          confirm: (rule) => {\n            ruleModal.loading();\n            if (JSON.stringify(rule).length > 3) {\n              this.templateSettings.routing.rules[index] = rule;\n              this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);\n            }\n            ruleModal.close();\n          },\n          isEdit: true\n        });\n      },\n      replaceRule(old_index, new_index) {\n        rules = this.templateSettings.routing.rules;\n        if (new_index >= rules.length) rules.push(undefined);\n        rules.splice(new_index, 0, rules.splice(old_index, 1)[0]);\n        this.routingRuleSettings = JSON.stringify(rules);\n      },\n      deleteRule(index) {\n        rules = this.templateSettings.routing.rules;\n        rules.splice(index, 1);\n        this.routingRuleSettings = JSON.stringify(rules);\n      },\n      showWarp() {\n        warpModal.show();\n      }\n    },\n    async mounted() {\n      if (window.location.protocol !== \"https:\") {\n        this.showAlert = true;\n      }\n      await this.getXraySetting();\n      await this.getXrayResult();\n      await this.getOutboundsTraffic();\n\n      if (window.wsClient) {\n          window.wsClient.connect();\n          window.wsClient.on('outbounds', (payload) => {\n            if (payload) {\n              this.outboundsTraffic = payload;\n              this.$forceUpdate();\n            }\n          });\n      }\n\n      while (true) {\n        await PromiseUtil.sleep(800);\n        this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this.outboundTestUrl;\n      }\n    },\n    computed: {\n      templateSettings: {\n        get: function () {\n          const parsedSettings = this.xraySetting ? JSON.parse(this.xraySetting) : null;\n          return parsedSettings;\n        },\n        set: function (newValue) {\n          if (newValue) {\n            this.xraySetting = JSON.stringify(newValue, null, 2);\n          }\n        },\n      },\n      inboundSettings: {\n        get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.inbounds = JSON.parse(newValue);\n          this.templateSettings = newTemplateSettings;\n        },\n      },\n      outboundSettings: {\n        get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.outbounds = JSON.parse(newValue);\n          this.templateSettings = newTemplateSettings;\n        },\n      },\n      outboundData: {\n        get: function () {\n          data = []\n          if (this.templateSettings != null) {\n            this.templateSettings.outbounds.forEach((o, index) => {\n              data.push({ 'key': index, ...o });\n            });\n          }\n          return data;\n        },\n      },\n      reverseData: {\n        get: function () {\n          data = []\n          if (this.templateSettings != null && this.templateSettings.reverse != null) {\n            if (this.templateSettings.reverse.bridges) {\n              this.templateSettings.reverse.bridges.forEach((o, index) => {\n                data.push({ 'key': index, 'type': 'bridge', ...o });\n              });\n            }\n            if (this.templateSettings.reverse.portals) {\n              this.templateSettings.reverse.portals.forEach((o, index) => {\n                data.push({ 'key': index, 'type': 'portal', ...o });\n              });\n            }\n          }\n          return data;\n        },\n      },\n      routingRuleSettings: {\n        get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.routing.rules = JSON.parse(newValue);\n          this.templateSettings = newTemplateSettings;\n        },\n      },\n      routingRuleData: {\n        get: function () {\n          data = [];\n          if (this.templateSettings != null) {\n            this.templateSettings.routing.rules.forEach((r, index) => {\n              data.push({ 'key': index, ...r });\n            });\n            // Make rules readable\n            data.forEach(r => {\n              if (r.domain) r.domain = r.domain.join(',')\n              if (r.ip) r.ip = r.ip.join(',')\n              if (r.source) r.source = r.source.join(',');\n              if (r.user) r.user = r.user.join(',')\n              if (r.inboundTag) r.inboundTag = r.inboundTag.join(',')\n              if (r.protocol) r.protocol = r.protocol.join(',')\n              if (r.attrs) r.attrs = JSON.stringify(r.attrs, null, 2)\n            });\n          }\n          return data;\n        }\n      },\n      balancersData: {\n        get: function () {\n          data = []\n          if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings.routing.balancers != null) {\n            this.templateSettings.routing.balancers.forEach((o, index) => {\n              data.push({\n                'key': index,\n                'tag': o.tag ? o.tag : \"\",\n                'strategy': o.strategy?.type ?? \"random\",\n                'selector': o.selector ? o.selector : [],\n                'fallbackTag': o.fallbackTag ?? '',\n              });\n            });\n          }\n          return data;\n        }\n      },\n      observatory: {\n        get: function () {\n          return this.templateSettings?.observatory ? JSON.stringify(this.templateSettings.observatory, null, 2) : null;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.observatory = JSON.parse(newValue);\n          this.templateSettings = newTemplateSettings;\n        },\n      },\n      burstObservatory: {\n        get: function () {\n          return this.templateSettings?.burstObservatory ? JSON.stringify(this.templateSettings.burstObservatory, null, 2) : null;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.burstObservatory = JSON.parse(newValue);\n          this.templateSettings = newTemplateSettings;\n        },\n      },\n      observatoryEnable: function () { return this.templateSettings != null && this.templateSettings.observatory != undefined },\n      burstObservatoryEnable: function () { return this.templateSettings != null && this.templateSettings.burstObservatory != undefined },\n      freedomStrategy: {\n        get: function () {\n          if (!this.templateSettings) return \"AsIs\";\n          freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === \"freedom\" && o.tag == \"direct\");\n          if (!freedomOutbound) return \"AsIs\";\n          if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return \"AsIs\";\n          return freedomOutbound.settings.domainStrategy;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === \"freedom\" && o.tag == \"direct\");\n          if (freedomOutboundIndex == -1) {\n            newTemplateSettings.outbounds.push({ protocol: \"freedom\", tag: \"direct\", settings: { \"domainStrategy\": newValue } });\n          } else if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {\n            newTemplateSettings.outbounds[freedomOutboundIndex].settings = { \"domainStrategy\": newValue };\n          } else {\n            newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      routingStrategy: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return \"AsIs\";\n          return this.templateSettings.routing.domainStrategy;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.routing.domainStrategy = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      logLevel: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.loglevel) return \"warning\";\n          return this.templateSettings.log.loglevel;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.log.loglevel = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      accessLog: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.access) return \"\";\n          return this.templateSettings.log.access;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.log.access = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      errorLog: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.error) return \"\";\n          return this.templateSettings.log.error;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.log.error = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnslog: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.dnsLog) return false;\n          return this.templateSettings.log.dnsLog;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.log.dnsLog = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      statsInboundUplink: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy.system.statsInboundUplink) return false;\n          return this.templateSettings.policy.system.statsInboundUplink;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.policy.system.statsInboundUplink = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      statsInboundDownlink: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy.system.statsInboundDownlink) return false;\n          return this.templateSettings.policy.system.statsInboundDownlink;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.policy.system.statsInboundDownlink = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      statsOutboundUplink: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy.system.statsOutboundUplink) return false;\n          return this.templateSettings.policy.system.statsOutboundUplink;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.policy.system.statsOutboundUplink = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      statsOutboundDownlink: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy.system.statsOutboundDownlink) return false;\n          return this.templateSettings.policy.system.statsOutboundDownlink;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.policy.system.statsOutboundDownlink = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      maskAddressLog: {\n        get: function () {\n          if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.maskAddress) return \"\";\n          return this.templateSettings.log.maskAddress;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.log.maskAddress = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      blockedIPs: {\n        get: function () {\n          return this.templateRuleGetter({ outboundTag: \"blocked\", property: \"ip\" });\n        },\n        set: function (newValue) {\n          this.templateRuleSetter({ outboundTag: \"blocked\", property: \"ip\", data: newValue });\n        }\n      },\n      blockedDomains: {\n        get: function () {\n          return this.templateRuleGetter({ outboundTag: \"blocked\", property: \"domain\" });\n        },\n        set: function (newValue) {\n          this.templateRuleSetter({ outboundTag: \"blocked\", property: \"domain\", data: newValue });\n        }\n      },\n      blockedProtocols: {\n        get: function () {\n          return this.templateRuleGetter({ outboundTag: \"blocked\", property: \"protocol\" });\n        },\n        set: function (newValue) {\n          this.templateRuleSetter({ outboundTag: \"blocked\", property: \"protocol\", data: newValue });\n        }\n      },\n      directIPs: {\n        get: function () {\n          return this.templateRuleGetter({ outboundTag: \"direct\", property: \"ip\" });\n        },\n        set: function (newValue) {\n          this.templateRuleSetter({ outboundTag: \"direct\", property: \"ip\", data: newValue });\n          this.syncRulesWithOutbound(\"direct\", this.directSettings);\n        }\n      },\n      directDomains: {\n        get: function () {\n          return this.templateRuleGetter({ outboundTag: \"direct\", property: \"domain\" });\n        },\n        set: function (newValue) {\n          this.templateRuleSetter({ outboundTag: \"direct\", property: \"domain\", data: newValue });\n          this.syncRulesWithOutbound(\"direct\", this.directSettings);\n        }\n      },\n      ipv4Domains: {\n        get: function () {\n          return this.templateRuleGetter({ outboundTag: \"IPv4\", property: \"domain\" });\n        },\n        set: function (newValue) {\n          this.templateRuleSetter({ outboundTag: \"IPv4\", property: \"domain\", data: newValue });\n          this.syncRulesWithOutbound(\"IPv4\", this.ipv4Settings);\n        }\n      },\n      warpDomains: {\n        get: function () {\n          return this.templateRuleGetter({ outboundTag: \"warp\", property: \"domain\" });\n        },\n        set: function (newValue) {\n          this.templateRuleSetter({ outboundTag: \"warp\", property: \"domain\", data: newValue });\n        }\n      },\n      torrentSettings: {\n        get: function () {\n          return ArrayUtils.doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);\n        },\n        set: function (newValue) {\n          if (newValue) {\n            this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];\n          } else {\n            this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data));\n          }\n        },\n      },\n      WarpExist: {\n        get: function () {\n          return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == \"warp\") >= 0 : false;\n        },\n      },\n      enableDNS: {\n        get: function () {\n          return this.templateSettings ? this.templateSettings.dns != null : false;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          if (newValue) {\n            newTemplateSettings.dns = {\n              servers: [],\n              queryStrategy: \"UseIP\",\n              tag: \"dns_inbound\",\n              enableParallelQuery: false\n            };\n            newTemplateSettings.fakedns = null;\n          } else {\n            delete newTemplateSettings.dns;\n            delete newTemplateSettings.fakedns;\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsTag: {\n        get: function () {\n          return this.enableDNS ? this.templateSettings.dns.tag : \"\";\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.dns.tag = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsClientIp: {\n        get: function () {\n          return this.enableDNS ? this.templateSettings.dns.clientIp : null;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          if (newValue) {\n            newTemplateSettings.dns.clientIp = newValue;\n          } else {\n            delete newTemplateSettings.dns.clientIp;\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsDisableCache: {\n        get: function () {\n          return this.enableDNS ? this.templateSettings.dns.disableCache : false;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          if (newValue) {\n            newTemplateSettings.dns.disableCache = newValue;\n          } else {\n            delete newTemplateSettings.dns.disableCache\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsDisableFallback: {\n        get: function () {\n          return this.enableDNS ? this.templateSettings.dns.disableFallback : false;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          if (newValue) {\n            newTemplateSettings.dns.disableFallback = newValue;\n          } else {\n            delete newTemplateSettings.dns.disableFallback\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsDisableFallbackIfMatch: {\n        get: function () {\n          return this.enableDNS ? this.templateSettings.dns.disableFallbackIfMatch : false;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          if (newValue) {\n            newTemplateSettings.dns.disableFallbackIfMatch = newValue;\n          } else {\n            delete newTemplateSettings.dns.disableFallbackIfMatch\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsEnableParallelQuery: {\n        get: function () {\n          return this.enableDNS ? (this.templateSettings.dns.enableParallelQuery || false) : false;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          if (newValue) {\n            newTemplateSettings.dns.enableParallelQuery = newValue;\n          } else {\n            delete newTemplateSettings.dns.enableParallelQuery\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsUseSystemHosts: {\n        get: function () {\n          return this.enableDNS ? this.templateSettings.dns.useSystemHosts : false;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          if (newValue) {\n            newTemplateSettings.dns.useSystemHosts = newValue;\n          } else {\n            delete newTemplateSettings.dns.useSystemHosts\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsStrategy: {\n        get: function () {\n          return this.enableDNS ? this.templateSettings.dns.queryStrategy : null;\n        },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.dns.queryStrategy = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      dnsServers: {\n        get: function () { return this.enableDNS ? this.templateSettings.dns.servers : []; },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          newTemplateSettings.dns.servers = newValue;\n          this.templateSettings = newTemplateSettings;\n        }\n      },\n      fakeDns: {\n        get: function () { return this.templateSettings && this.templateSettings.fakedns ? this.templateSettings.fakedns : []; },\n        set: function (newValue) {\n          newTemplateSettings = this.templateSettings;\n          if (this.enableDNS) {\n            newTemplateSettings.fakedns = newValue.length > 0 ? newValue : null;\n          } else {\n            delete newTemplateSettings.fakedns;\n          }\n          this.templateSettings = newTemplateSettings;\n        }\n      }\n    },\n  });\n</script>\n{{ template \"page/body_end\" .}}"
  },
  {
    "path": "web/job/check_client_ip_job.go",
    "content": "package job\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n)\n\n// IPWithTimestamp tracks an IP address with its last seen timestamp\ntype IPWithTimestamp struct {\n\tIP        string `json:\"ip\"`\n\tTimestamp int64  `json:\"timestamp\"`\n}\n\n// CheckClientIpJob monitors client IP addresses from access logs and manages IP blocking based on configured limits.\ntype CheckClientIpJob struct {\n\tlastClear     int64\n\tdisAllowedIps []string\n}\n\nvar job *CheckClientIpJob\n\n// NewCheckClientIpJob creates a new client IP monitoring job instance.\nfunc NewCheckClientIpJob() *CheckClientIpJob {\n\tjob = new(CheckClientIpJob)\n\treturn job\n}\n\nfunc (j *CheckClientIpJob) Run() {\n\tif j.lastClear == 0 {\n\t\tj.lastClear = time.Now().Unix()\n\t}\n\n\tshouldClearAccessLog := false\n\tiplimitActive := j.hasLimitIp()\n\tf2bInstalled := j.checkFail2BanInstalled()\n\tisAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)\n\n\tif isAccessLogAvailable {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tif iplimitActive {\n\t\t\t\tshouldClearAccessLog = j.processLogFile()\n\t\t\t}\n\t\t} else {\n\t\t\tif iplimitActive {\n\t\t\t\tif f2bInstalled {\n\t\t\t\t\tshouldClearAccessLog = j.processLogFile()\n\t\t\t\t} else {\n\t\t\t\t\tif !f2bInstalled {\n\t\t\t\t\t\tlogger.Warning(\"[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif shouldClearAccessLog || (isAccessLogAvailable && time.Now().Unix()-j.lastClear > 3600) {\n\t\tj.clearAccessLog()\n\t}\n}\n\nfunc (j *CheckClientIpJob) clearAccessLog() {\n\tlogAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)\n\tj.checkError(err)\n\tdefer logAccessP.Close()\n\n\taccessLogPath, err := xray.GetAccessLogPath()\n\tj.checkError(err)\n\n\tfile, err := os.Open(accessLogPath)\n\tj.checkError(err)\n\tdefer file.Close()\n\n\t_, err = io.Copy(logAccessP, file)\n\tj.checkError(err)\n\n\terr = os.Truncate(accessLogPath, 0)\n\tj.checkError(err)\n\n\tj.lastClear = time.Now().Unix()\n}\n\nfunc (j *CheckClientIpJob) hasLimitIp() bool {\n\tdb := database.GetDB()\n\tvar inbounds []*model.Inbound\n\n\terr := db.Model(model.Inbound{}).Find(&inbounds).Error\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tfor _, inbound := range inbounds {\n\t\tif inbound.Settings == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tsettings := map[string][]model.Client{}\n\t\tjson.Unmarshal([]byte(inbound.Settings), &settings)\n\t\tclients := settings[\"clients\"]\n\n\t\tfor _, client := range clients {\n\t\t\tlimitIp := client.LimitIP\n\t\t\tif limitIp > 0 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (j *CheckClientIpJob) processLogFile() bool {\n\n\tipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\\[?([0-9a-fA-F\\.:]+)\\]?:\\d+ accepted`)\n\temailRegex := regexp.MustCompile(`email: (.+)$`)\n\ttimestampRegex := regexp.MustCompile(`^(\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2})`)\n\n\taccessLogPath, _ := xray.GetAccessLogPath()\n\tfile, _ := os.Open(accessLogPath)\n\tdefer file.Close()\n\n\t// Track IPs with their last seen timestamp\n\tinboundClientIps := make(map[string]map[string]int64, 100)\n\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tipMatches := ipRegex.FindStringSubmatch(line)\n\t\tif len(ipMatches) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tip := ipMatches[1]\n\n\t\tif ip == \"127.0.0.1\" || ip == \"::1\" {\n\t\t\tcontinue\n\t\t}\n\n\t\temailMatches := emailRegex.FindStringSubmatch(line)\n\t\tif len(emailMatches) < 2 {\n\t\t\tcontinue\n\t\t}\n\t\temail := emailMatches[1]\n\n\t\t// Extract timestamp from log line\n\t\tvar timestamp int64\n\t\ttimestampMatches := timestampRegex.FindStringSubmatch(line)\n\t\tif len(timestampMatches) >= 2 {\n\t\t\tt, err := time.Parse(\"2006/01/02 15:04:05\", timestampMatches[1])\n\t\t\tif err == nil {\n\t\t\t\ttimestamp = t.Unix()\n\t\t\t} else {\n\t\t\t\ttimestamp = time.Now().Unix()\n\t\t\t}\n\t\t} else {\n\t\t\ttimestamp = time.Now().Unix()\n\t\t}\n\n\t\tif _, exists := inboundClientIps[email]; !exists {\n\t\t\tinboundClientIps[email] = make(map[string]int64)\n\t\t}\n\t\t// Update timestamp - keep the latest\n\t\tif existingTime, ok := inboundClientIps[email][ip]; !ok || timestamp > existingTime {\n\t\t\tinboundClientIps[email][ip] = timestamp\n\t\t}\n\t}\n\n\tshouldCleanLog := false\n\tfor email, ipTimestamps := range inboundClientIps {\n\n\t\t// Convert to IPWithTimestamp slice\n\t\tipsWithTime := make([]IPWithTimestamp, 0, len(ipTimestamps))\n\t\tfor ip, timestamp := range ipTimestamps {\n\t\t\tipsWithTime = append(ipsWithTime, IPWithTimestamp{IP: ip, Timestamp: timestamp})\n\t\t}\n\n\t\tclientIpsRecord, err := j.getInboundClientIps(email)\n\t\tif err != nil {\n\t\t\tj.addInboundClientIps(email, ipsWithTime)\n\t\t\tcontinue\n\t\t}\n\n\t\tshouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ipsWithTime) || shouldCleanLog\n\t}\n\n\treturn shouldCleanLog\n}\n\nfunc (j *CheckClientIpJob) checkFail2BanInstalled() bool {\n\tcmd := \"fail2ban-client\"\n\targs := []string{\"-h\"}\n\terr := exec.Command(cmd, args...).Run()\n\treturn err == nil\n}\n\nfunc (j *CheckClientIpJob) checkAccessLogAvailable(iplimitActive bool) bool {\n\taccessLogPath, err := xray.GetAccessLogPath()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tif accessLogPath == \"none\" || accessLogPath == \"\" {\n\t\tif iplimitActive {\n\t\t\tlogger.Warning(\"[LimitIP] Access log path is not set, Please configure the access log path in Xray configs.\")\n\t\t}\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (j *CheckClientIpJob) checkError(e error) {\n\tif e != nil {\n\t\tlogger.Warning(\"client ip job err:\", e)\n\t}\n}\n\nfunc (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {\n\tdb := database.GetDB()\n\tInboundClientIps := &model.InboundClientIps{}\n\terr := db.Model(model.InboundClientIps{}).Where(\"client_email = ?\", clientEmail).First(InboundClientIps).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn InboundClientIps, nil\n}\n\nfunc (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ipsWithTime []IPWithTimestamp) error {\n\tinboundClientIps := &model.InboundClientIps{}\n\tjsonIps, err := json.Marshal(ipsWithTime)\n\tj.checkError(err)\n\n\tinboundClientIps.ClientEmail = clientEmail\n\tinboundClientIps.Ips = string(jsonIps)\n\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\n\tdefer func() {\n\t\tif err == nil {\n\t\t\ttx.Commit()\n\t\t} else {\n\t\t\ttx.Rollback()\n\t\t}\n\t}()\n\n\terr = tx.Save(inboundClientIps).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, newIpsWithTime []IPWithTimestamp) bool {\n\t// Get the inbound configuration\n\tinbound, err := j.getInboundByEmail(clientEmail)\n\tif err != nil {\n\t\tlogger.Errorf(\"failed to fetch inbound settings for email %s: %s\", clientEmail, err)\n\t\treturn false\n\t}\n\n\tif inbound.Settings == \"\" {\n\t\tlogger.Debug(\"wrong data:\", inbound)\n\t\treturn false\n\t}\n\n\tsettings := map[string][]model.Client{}\n\tjson.Unmarshal([]byte(inbound.Settings), &settings)\n\tclients := settings[\"clients\"]\n\n\t// Find the client's IP limit\n\tvar limitIp int\n\tvar clientFound bool\n\tfor _, client := range clients {\n\t\tif client.Email == clientEmail {\n\t\t\tlimitIp = client.LimitIP\n\t\t\tclientFound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !clientFound || limitIp <= 0 || !inbound.Enable {\n\t\t// No limit or inbound disabled, just update and return\n\t\tjsonIps, _ := json.Marshal(newIpsWithTime)\n\t\tinboundClientIps.Ips = string(jsonIps)\n\t\tdb := database.GetDB()\n\t\tdb.Save(inboundClientIps)\n\t\treturn false\n\t}\n\n\t// Parse old IPs from database\n\tvar oldIpsWithTime []IPWithTimestamp\n\tif inboundClientIps.Ips != \"\" {\n\t\tjson.Unmarshal([]byte(inboundClientIps.Ips), &oldIpsWithTime)\n\t}\n\n\t// Merge old and new IPs, keeping the latest timestamp for each IP\n\tipMap := make(map[string]int64)\n\tfor _, ipTime := range oldIpsWithTime {\n\t\tipMap[ipTime.IP] = ipTime.Timestamp\n\t}\n\tfor _, ipTime := range newIpsWithTime {\n\t\tif existingTime, ok := ipMap[ipTime.IP]; !ok || ipTime.Timestamp > existingTime {\n\t\t\tipMap[ipTime.IP] = ipTime.Timestamp\n\t\t}\n\t}\n\n\t// Convert back to slice and sort by timestamp (oldest first)\n\t// This ensures we always protect the original/current connections and ban new excess ones.\n\tallIps := make([]IPWithTimestamp, 0, len(ipMap))\n\tfor ip, timestamp := range ipMap {\n\t\tallIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp})\n\t}\n\tsort.Slice(allIps, func(i, j int) bool {\n\t\treturn allIps[i].Timestamp < allIps[j].Timestamp // Ascending order (oldest first)\n\t})\n\n\tshouldCleanLog := false\n\tj.disAllowedIps = []string{}\n\n\t// Open log file\n\tlogIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\tlogger.Errorf(\"failed to open IP limit log file: %s\", err)\n\t\treturn false\n\t}\n\tdefer logIpFile.Close()\n\tlog.SetOutput(logIpFile)\n\tlog.SetFlags(log.LstdFlags)\n\n\t// Check if we exceed the limit\n\tif len(allIps) > limitIp {\n\t\tshouldCleanLog = true\n\n\t\t// Keep the oldest IPs (currently active connections) and ban the new excess ones.\n\t\tkeptIps := allIps[:limitIp]\n\t\tbannedIps := allIps[limitIp:]\n\n\t\t// Log banned IPs in the format fail2ban filters expect: [LIMIT_IP] Email = X || Disconnecting OLD IP = Y || Timestamp = Z\n\t\tfor _, ipTime := range bannedIps {\n\t\t\tj.disAllowedIps = append(j.disAllowedIps, ipTime.IP)\n\t\t\tlog.Printf(\"[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d\", clientEmail, ipTime.IP, ipTime.Timestamp)\n\t\t}\n\n\t\t// Update database with only the currently active (kept) IPs\n\t\tjsonIps, _ := json.Marshal(keptIps)\n\t\tinboundClientIps.Ips = string(jsonIps)\n\t} else {\n\t\t// Under limit, save all IPs\n\t\tjsonIps, _ := json.Marshal(allIps)\n\t\tinboundClientIps.Ips = string(jsonIps)\n\t}\n\n\tdb := database.GetDB()\n\terr = db.Save(inboundClientIps).Error\n\tif err != nil {\n\t\tlogger.Error(\"failed to save inboundClientIps:\", err)\n\t\treturn false\n\t}\n\n\tif len(j.disAllowedIps) > 0 {\n\t\tlogger.Infof(\"[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban\", clientEmail, limitIp, len(j.disAllowedIps))\n\t}\n\n\treturn shouldCleanLog\n}\n\nfunc (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {\n\tdb := database.GetDB()\n\tinbound := &model.Inbound{}\n\n\terr := db.Model(&model.Inbound{}).Where(\"settings LIKE ?\", \"%\"+clientEmail+\"%\").First(inbound).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn inbound, nil\n}\n"
  },
  {
    "path": "web/job/check_cpu_usage.go",
    "content": "package job\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\n\t\"github.com/shirou/gopsutil/v4/cpu\"\n)\n\n// CheckCpuJob monitors CPU usage and sends Telegram notifications when usage exceeds the configured threshold.\ntype CheckCpuJob struct {\n\ttgbotService   service.Tgbot\n\tsettingService service.SettingService\n}\n\n// NewCheckCpuJob creates a new CPU monitoring job instance.\nfunc NewCheckCpuJob() *CheckCpuJob {\n\treturn new(CheckCpuJob)\n}\n\n// Run checks CPU usage over the last minute and sends a Telegram alert if it exceeds the threshold.\nfunc (j *CheckCpuJob) Run() {\n\tthreshold, err := j.settingService.GetTgCpu()\n\tif err != nil || threshold <= 0 {\n\t\t// If threshold cannot be retrieved or is not set, skip sending notifications\n\t\treturn\n\t}\n\n\t// get latest status of server\n\tpercent, err := cpu.Percent(1*time.Minute, false)\n\tif err == nil && percent[0] > float64(threshold) {\n\t\tmsg := j.tgbotService.I18nBot(\"tgbot.messages.cpuThreshold\",\n\t\t\t\"Percent==\"+strconv.FormatFloat(percent[0], 'f', 2, 64),\n\t\t\t\"Threshold==\"+strconv.Itoa(threshold))\n\n\t\tj.tgbotService.SendMsgToTgbotAdmins(msg)\n\t}\n}\n"
  },
  {
    "path": "web/job/check_hash_storage.go",
    "content": "package job\n\nimport (\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n)\n\n// CheckHashStorageJob periodically cleans up expired hash entries from the Telegram bot's hash storage.\ntype CheckHashStorageJob struct {\n\ttgbotService service.Tgbot\n}\n\n// NewCheckHashStorageJob creates a new hash storage cleanup job instance.\nfunc NewCheckHashStorageJob() *CheckHashStorageJob {\n\treturn new(CheckHashStorageJob)\n}\n\n// Run removes expired hash entries from the Telegram bot's hash storage.\nfunc (j *CheckHashStorageJob) Run() {\n\t// Remove expired hashes from storage\n\tj.tgbotService.GetHashStorage().RemoveExpiredHashes()\n}\n"
  },
  {
    "path": "web/job/check_xray_running_job.go",
    "content": "// Package job provides background job implementations for the 3x-ui web panel,\n// including traffic monitoring, system checks, and periodic maintenance tasks.\npackage job\n\nimport (\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n)\n\n// CheckXrayRunningJob monitors Xray process health and restarts it if it crashes.\ntype CheckXrayRunningJob struct {\n\txrayService service.XrayService\n\tcheckTime   int\n}\n\n// NewCheckXrayRunningJob creates a new Xray health check job instance.\nfunc NewCheckXrayRunningJob() *CheckXrayRunningJob {\n\treturn new(CheckXrayRunningJob)\n}\n\n// Run checks if Xray has crashed and restarts it after confirming it's down for 2 consecutive checks.\nfunc (j *CheckXrayRunningJob) Run() {\n\tif !j.xrayService.DidXrayCrash() {\n\t\tj.checkTime = 0\n\t} else {\n\t\tj.checkTime++\n\t\t// only restart if it's down 2 times in a row\n\t\tif j.checkTime > 1 {\n\t\t\terr := j.xrayService.RestartXray(false)\n\t\t\tj.checkTime = 0\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(\"Restart xray failed:\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "web/job/clear_logs_job.go",
    "content": "package job\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n)\n\n// ClearLogsJob clears old log files to prevent disk space issues.\ntype ClearLogsJob struct{}\n\n// NewClearLogsJob creates a new log cleanup job instance.\nfunc NewClearLogsJob() *ClearLogsJob {\n\treturn new(ClearLogsJob)\n}\n\n// ensureFileExists creates the necessary directories and file if they don't exist\nfunc ensureFileExists(path string) error {\n\tdir := filepath.Dir(path)\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn err\n\t}\n\n\tfile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfile.Close()\n\treturn nil\n}\n\n// Here Run is an interface method of the Job interface\nfunc (j *ClearLogsJob) Run() {\n\tlogFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}\n\tlogFilesPrev := []string{xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}\n\n\t// Ensure all log files and their paths exist\n\tfor _, path := range append(logFiles, logFilesPrev...) {\n\t\tif err := ensureFileExists(path); err != nil {\n\t\t\tlogger.Warning(\"Failed to ensure log file exists:\", path, \"-\", err)\n\t\t}\n\t}\n\n\t// Clear log files and copy to previous logs\n\tfor i := range len(logFiles) {\n\t\tif i > 0 {\n\t\t\t// Copy to previous logs\n\t\t\tlogFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warning(\"Failed to open previous log file for writing:\", logFilesPrev[i-1], \"-\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlogFile, err := os.OpenFile(logFiles[i], os.O_RDONLY, 0644)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warning(\"Failed to open current log file for reading:\", logFiles[i], \"-\", err)\n\t\t\t\tlogFilePrev.Close()\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t_, err = io.Copy(logFilePrev, logFile)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warning(\"Failed to copy log file:\", logFiles[i], \"to\", logFilesPrev[i-1], \"-\", err)\n\t\t\t}\n\n\t\t\tlogFile.Close()\n\t\t\tlogFilePrev.Close()\n\t\t}\n\n\t\terr := os.Truncate(logFiles[i], 0)\n\t\tif err != nil {\n\t\t\tlogger.Warning(\"Failed to truncate log file:\", logFiles[i], \"-\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "web/job/ldap_sync_job.go",
    "content": "package job\n\nimport (\n\t\"time\"\n\n\t\"strings\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\tldaputil \"github.com/mhsanaei/3x-ui/v2/util/ldap\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\n\t\"strconv\"\n\n\t\"github.com/google/uuid\"\n)\n\nvar DefaultTruthyValues = []string{\"true\", \"1\", \"yes\", \"on\"}\n\ntype LdapSyncJob struct {\n\tsettingService service.SettingService\n\tinboundService service.InboundService\n\txrayService    service.XrayService\n}\n\n// --- Helper functions for mustGet ---\nfunc mustGetString(fn func() (string, error)) string {\n\tv, err := fn()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn v\n}\n\nfunc mustGetInt(fn func() (int, error)) int {\n\tv, err := fn()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn v\n}\n\nfunc mustGetBool(fn func() (bool, error)) bool {\n\tv, err := fn()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn v\n}\n\nfunc mustGetStringOr(fn func() (string, error), fallback string) string {\n\tv, err := fn()\n\tif err != nil || v == \"\" {\n\t\treturn fallback\n\t}\n\treturn v\n}\n\nfunc NewLdapSyncJob() *LdapSyncJob {\n\treturn new(LdapSyncJob)\n}\n\nfunc (j *LdapSyncJob) Run() {\n\tlogger.Info(\"LDAP sync job started\")\n\n\tenabled, err := j.settingService.GetLdapEnable()\n\tif err != nil || !enabled {\n\t\tlogger.Warning(\"LDAP disabled or failed to fetch flag\")\n\t\treturn\n\t}\n\n\t// --- LDAP fetch ---\n\tcfg := ldaputil.Config{\n\t\tHost:       mustGetString(j.settingService.GetLdapHost),\n\t\tPort:       mustGetInt(j.settingService.GetLdapPort),\n\t\tUseTLS:     mustGetBool(j.settingService.GetLdapUseTLS),\n\t\tBindDN:     mustGetString(j.settingService.GetLdapBindDN),\n\t\tPassword:   mustGetString(j.settingService.GetLdapPassword),\n\t\tBaseDN:     mustGetString(j.settingService.GetLdapBaseDN),\n\t\tUserFilter: mustGetString(j.settingService.GetLdapUserFilter),\n\t\tUserAttr:   mustGetString(j.settingService.GetLdapUserAttr),\n\t\tFlagField:  mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),\n\t\tTruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)),\n\t\tInvert:     mustGetBool(j.settingService.GetLdapInvertFlag),\n\t}\n\n\tflags, err := ldaputil.FetchVlessFlags(cfg)\n\tif err != nil {\n\t\tlogger.Warning(\"LDAP fetch failed:\", err)\n\t\treturn\n\t}\n\tlogger.Infof(\"Fetched %d LDAP flags\", len(flags))\n\n\t// --- Load all inbounds and all clients once ---\n\tinboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))\n\tinbounds, err := j.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"Failed to get inbounds:\", err)\n\t\treturn\n\t}\n\n\tallClients := map[string]*model.Client{}  // email -> client\n\tinboundMap := map[string]*model.Inbound{} // tag -> inbound\n\tfor _, ib := range inbounds {\n\t\tinboundMap[ib.Tag] = ib\n\t\tclients, _ := j.inboundService.GetClients(ib)\n\t\tfor i := range clients {\n\t\t\tallClients[clients[i].Email] = &clients[i]\n\t\t}\n\t}\n\n\t// --- Prepare batch operations ---\n\tautoCreate := mustGetBool(j.settingService.GetLdapAutoCreate)\n\tdefGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB)\n\tdefExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)\n\tdefLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)\n\n\tclientsToCreate := map[string][]model.Client{} // tag -> []new clients\n\tclientsToEnable := map[string][]string{}       // tag -> []email\n\tclientsToDisable := map[string][]string{}      // tag -> []email\n\n\tfor email, allowed := range flags {\n\t\texists := allClients[email] != nil\n\t\tfor _, tag := range inboundTags {\n\t\t\tif !exists && allowed && autoCreate {\n\t\t\t\tnewClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)\n\t\t\t\tclientsToCreate[tag] = append(clientsToCreate[tag], newClient)\n\t\t\t} else if exists {\n\t\t\t\tif allowed && !allClients[email].Enable {\n\t\t\t\t\tclientsToEnable[tag] = append(clientsToEnable[tag], email)\n\t\t\t\t} else if !allowed && allClients[email].Enable {\n\t\t\t\t\tclientsToDisable[tag] = append(clientsToDisable[tag], email)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// --- Execute batch create ---\n\tfor tag, newClients := range clientsToCreate {\n\t\tif len(newClients) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tpayload := &model.Inbound{Id: inboundMap[tag].Id}\n\t\tpayload.Settings = j.clientsToJSON(newClients)\n\t\tif _, err := j.inboundService.AddInboundClient(payload); err != nil {\n\t\t\tlogger.Warningf(\"Failed to add clients for tag %s: %v\", tag, err)\n\t\t} else {\n\t\t\tlogger.Infof(\"LDAP auto-create: %d clients for %s\", len(newClients), tag)\n\t\t\tj.xrayService.SetToNeedRestart()\n\t\t}\n\t}\n\n\t// --- Execute enable/disable batch ---\n\tfor tag, emails := range clientsToEnable {\n\t\tj.batchSetEnable(inboundMap[tag], emails, true)\n\t}\n\tfor tag, emails := range clientsToDisable {\n\t\tj.batchSetEnable(inboundMap[tag], emails, false)\n\t}\n\n\t// --- Auto delete clients not in LDAP ---\n\tautoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)\n\tif autoDelete {\n\t\tldapEmailSet := map[string]struct{}{}\n\t\tfor e := range flags {\n\t\t\tldapEmailSet[e] = struct{}{}\n\t\t}\n\t\tfor _, tag := range inboundTags {\n\t\t\tj.deleteClientsNotInLDAP(tag, ldapEmailSet)\n\t\t}\n\t}\n}\n\nfunc splitCsv(s string) []string {\n\tif s == \"\" {\n\t\treturn DefaultTruthyValues\n\t}\n\tparts := strings.Split(s, \",\")\n\tout := make([]string, 0, len(parts))\n\tfor _, p := range parts {\n\t\tv := strings.TrimSpace(p)\n\t\tif v != \"\" {\n\t\t\tout = append(out, v)\n\t\t}\n\t}\n\treturn out\n}\n\n// buildClient creates a new client for auto-create\nfunc (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {\n\tc := model.Client{\n\t\tEmail:   email,\n\t\tEnable:  true,\n\t\tLimitIP: defLimitIP,\n\t\tTotalGB: int64(defGB),\n\t}\n\tif defExpiryDays > 0 {\n\t\tc.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()\n\t}\n\tswitch ib.Protocol {\n\tcase model.Trojan, model.Shadowsocks:\n\t\tc.Password = uuid.NewString()\n\tdefault:\n\t\tc.ID = uuid.NewString()\n\t}\n\treturn c\n}\n\n// batchSetEnable enables/disables clients in batch through a single call\nfunc (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {\n\tif len(emails) == 0 {\n\t\treturn\n\t}\n\n\t// Prepare JSON for mass update\n\tclients := make([]model.Client, 0, len(emails))\n\tfor _, email := range emails {\n\t\tclients = append(clients, model.Client{\n\t\t\tEmail:  email,\n\t\t\tEnable: enable,\n\t\t})\n\t}\n\n\tpayload := &model.Inbound{\n\t\tId:       ib.Id,\n\t\tSettings: j.clientsToJSON(clients),\n\t}\n\n\t// Use a single AddInboundClient call to update enable\n\tif _, err := j.inboundService.AddInboundClient(payload); err != nil {\n\t\tlogger.Warningf(\"Batch set enable failed for inbound %s: %v\", ib.Tag, err)\n\t\treturn\n\t}\n\n\tlogger.Infof(\"Batch set enable=%v for %d clients in inbound %s\", enable, len(emails), ib.Tag)\n\tj.xrayService.SetToNeedRestart()\n}\n\n// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart\nfunc (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {\n\tinbounds, err := j.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"Failed to get inbounds for deletion:\", err)\n\t\treturn\n\t}\n\n\tbatchSize := 50 //  clients in 1 batch\n\trestartNeeded := false\n\n\tfor _, ib := range inbounds {\n\t\tif ib.Tag != inboundTag {\n\t\t\tcontinue\n\t\t}\n\t\tclients, err := j.inboundService.GetClients(ib)\n\t\tif err != nil {\n\t\t\tlogger.Warningf(\"Failed to get clients for inbound %s: %v\", ib.Tag, err)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Collect clients for deletion\n\t\ttoDelete := []model.Client{}\n\t\tfor _, c := range clients {\n\t\t\tif _, ok := ldapEmails[c.Email]; !ok {\n\t\t\t\ttoDelete = append(toDelete, c)\n\t\t\t}\n\t\t}\n\n\t\tif len(toDelete) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Delete in batches\n\t\tfor i := 0; i < len(toDelete); i += batchSize {\n\t\t\tend := min(i+batchSize, len(toDelete))\n\t\t\tbatch := toDelete[i:end]\n\n\t\t\tfor _, c := range batch {\n\t\t\t\tvar clientKey string\n\t\t\t\tswitch ib.Protocol {\n\t\t\t\tcase model.Trojan:\n\t\t\t\t\tclientKey = c.Password\n\t\t\t\tcase model.Shadowsocks:\n\t\t\t\t\tclientKey = c.Email\n\t\t\t\tdefault: // vless/vmess\n\t\t\t\t\tclientKey = c.ID\n\t\t\t\t}\n\n\t\t\t\tif _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil {\n\t\t\t\t\tlogger.Warningf(\"Failed to delete client %s from inbound id=%d(tag=%s): %v\",\n\t\t\t\t\t\tc.Email, ib.Id, ib.Tag, err)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Infof(\"Deleted client %s from inbound id=%d(tag=%s)\",\n\t\t\t\t\t\tc.Email, ib.Id, ib.Tag)\n\t\t\t\t\t// do not restart here\n\t\t\t\t\trestartNeeded = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// One time after all batches\n\tif restartNeeded {\n\t\tj.xrayService.SetToNeedRestart()\n\t\tlogger.Info(\"Xray restart scheduled after batch deletion\")\n\t}\n}\n\n// clientsToJSON serializes an array of clients to JSON\nfunc (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {\n\tb := strings.Builder{}\n\tb.WriteString(\"{\\\"clients\\\":[\")\n\tfor i, c := range clients {\n\t\tif i > 0 {\n\t\t\tb.WriteString(\",\")\n\t\t}\n\t\tb.WriteString(j.clientToJSON(c))\n\t}\n\tb.WriteString(\"]}\")\n\treturn b.String()\n}\n\n// clientToJSON serializes minimal client fields to JSON object string without extra deps\nfunc (j *LdapSyncJob) clientToJSON(c model.Client) string {\n\t// construct minimal JSON manually to avoid importing json for simple case\n\tb := strings.Builder{}\n\tb.WriteString(\"{\")\n\tif c.ID != \"\" {\n\t\tb.WriteString(\"\\\"id\\\":\\\"\")\n\t\tb.WriteString(c.ID)\n\t\tb.WriteString(\"\\\",\")\n\t}\n\tif c.Password != \"\" {\n\t\tb.WriteString(\"\\\"password\\\":\\\"\")\n\t\tb.WriteString(c.Password)\n\t\tb.WriteString(\"\\\",\")\n\t}\n\tb.WriteString(\"\\\"email\\\":\\\"\")\n\tb.WriteString(c.Email)\n\tb.WriteString(\"\\\",\")\n\tb.WriteString(\"\\\"enable\\\":\")\n\tif c.Enable {\n\t\tb.WriteString(\"true\")\n\t} else {\n\t\tb.WriteString(\"false\")\n\t}\n\tb.WriteString(\",\")\n\tb.WriteString(\"\\\"limitIp\\\":\")\n\tb.WriteString(strconv.Itoa(c.LimitIP))\n\tb.WriteString(\",\")\n\tb.WriteString(\"\\\"totalGB\\\":\")\n\tb.WriteString(strconv.FormatInt(c.TotalGB, 10))\n\tif c.ExpiryTime > 0 {\n\t\tb.WriteString(\",\\\"expiryTime\\\":\")\n\t\tb.WriteString(strconv.FormatInt(c.ExpiryTime, 10))\n\t}\n\tb.WriteString(\"}\")\n\treturn b.String()\n}\n"
  },
  {
    "path": "web/job/periodic_traffic_reset_job.go",
    "content": "package job\n\nimport (\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n)\n\n// Period represents the time period for traffic resets.\ntype Period string\n\n// PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period.\ntype PeriodicTrafficResetJob struct {\n\tinboundService service.InboundService\n\tperiod         Period\n}\n\n// NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period.\nfunc NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {\n\treturn &PeriodicTrafficResetJob{\n\t\tperiod: period,\n\t}\n}\n\n// Run resets traffic statistics for all inbounds that match the configured reset period.\nfunc (j *PeriodicTrafficResetJob) Run() {\n\tinbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))\n\tif err != nil {\n\t\tlogger.Warning(\"Failed to get inbounds for traffic reset:\", err)\n\t\treturn\n\t}\n\n\tif len(inbounds) == 0 {\n\t\treturn\n\t}\n\tlogger.Infof(\"Running periodic traffic reset job for period: %s (%d matching inbounds)\", j.period, len(inbounds))\n\n\tresetCount := 0\n\n\tfor _, inbound := range inbounds {\n\t\tresetInboundErr := j.inboundService.ResetAllTraffics()\n\t\tif resetInboundErr != nil {\n\t\t\tlogger.Warning(\"Failed to reset traffic for inbound\", inbound.Id, \":\", resetInboundErr)\n\t\t}\n\n\t\tresetClientErr := j.inboundService.ResetAllClientTraffics(inbound.Id)\n\t\tif resetClientErr != nil {\n\t\t\tlogger.Warning(\"Failed to reset traffic for all users of inbound\", inbound.Id, \":\", resetClientErr)\n\t\t}\n\n\t\tif resetInboundErr == nil && resetClientErr == nil {\n\t\t\tresetCount++\n\t\t}\n\t}\n\n\tif resetCount > 0 {\n\t\tlogger.Infof(\"Periodic traffic reset completed: %d inbounds reset\", resetCount)\n\t}\n}\n"
  },
  {
    "path": "web/job/stats_notify_job.go",
    "content": "package job\n\nimport (\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n)\n\n// LoginStatus represents the status of a login attempt.\ntype LoginStatus byte\n\nconst (\n\tLoginSuccess LoginStatus = 1 // Successful login\n\tLoginFail    LoginStatus = 0 // Failed login attempt\n)\n\n// StatsNotifyJob sends periodic statistics reports via Telegram bot.\ntype StatsNotifyJob struct {\n\txrayService  service.XrayService\n\ttgbotService service.Tgbot\n}\n\n// NewStatsNotifyJob creates a new statistics notification job instance.\nfunc NewStatsNotifyJob() *StatsNotifyJob {\n\treturn new(StatsNotifyJob)\n}\n\n// Run sends a statistics report via Telegram bot if Xray is running.\nfunc (j *StatsNotifyJob) Run() {\n\tif !j.xrayService.IsXrayRunning() {\n\t\treturn\n\t}\n\tj.tgbotService.SendReport()\n}\n"
  },
  {
    "path": "web/job/xray_traffic_job.go",
    "content": "package job\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/websocket\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n\n\t\"github.com/valyala/fasthttp\"\n)\n\n// XrayTrafficJob collects and processes traffic statistics from Xray, updating the database and optionally informing external APIs.\ntype XrayTrafficJob struct {\n\tsettingService  service.SettingService\n\txrayService     service.XrayService\n\tinboundService  service.InboundService\n\toutboundService service.OutboundService\n}\n\n// NewXrayTrafficJob creates a new traffic collection job instance.\nfunc NewXrayTrafficJob() *XrayTrafficJob {\n\treturn new(XrayTrafficJob)\n}\n\n// Run collects traffic statistics from Xray and updates the database, triggering restart if needed.\nfunc (j *XrayTrafficJob) Run() {\n\tif !j.xrayService.IsXrayRunning() {\n\t\treturn\n\t}\n\ttraffics, clientTraffics, err := j.xrayService.GetXrayTraffic()\n\tif err != nil {\n\t\treturn\n\t}\n\terr, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics)\n\tif err != nil {\n\t\tlogger.Warning(\"add inbound traffic failed:\", err)\n\t}\n\terr, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics)\n\tif err != nil {\n\t\tlogger.Warning(\"add outbound traffic failed:\", err)\n\t}\n\tif ExternalTrafficInformEnable, err := j.settingService.GetExternalTrafficInformEnable(); ExternalTrafficInformEnable {\n\t\tj.informTrafficToExternalAPI(traffics, clientTraffics)\n\t} else if err != nil {\n\t\tlogger.Warning(\"get ExternalTrafficInformEnable failed:\", err)\n\t}\n\tif needRestart0 || needRestart1 {\n\t\tj.xrayService.SetToNeedRestart()\n\t}\n\n\t// Get online clients and last online map for real-time status updates\n\tonlineClients := j.inboundService.GetOnlineClients()\n\tlastOnlineMap, err := j.inboundService.GetClientsLastOnline()\n\tif err != nil {\n\t\tlogger.Warning(\"get clients last online failed:\", err)\n\t\tlastOnlineMap = make(map[string]int64)\n\t}\n\n\t// Fetch updated inbounds from database with accumulated traffic values\n\t// This ensures frontend receives the actual total traffic, not just delta values\n\tupdatedInbounds, err := j.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"get all inbounds for websocket failed:\", err)\n\t}\n\n\tupdatedOutbounds, err := j.outboundService.GetOutboundsTraffic()\n\tif err != nil {\n\t\tlogger.Warning(\"get all outbounds for websocket failed:\", err)\n\t}\n\n\t// Broadcast traffic update via WebSocket with accumulated values from database\n\ttrafficUpdate := map[string]any{\n\t\t\"traffics\":       traffics,\n\t\t\"clientTraffics\": clientTraffics,\n\t\t\"onlineClients\":  onlineClients,\n\t\t\"lastOnlineMap\":  lastOnlineMap,\n\t}\n\twebsocket.BroadcastTraffic(trafficUpdate)\n\n\t// Broadcast full inbounds update for real-time UI refresh\n\tif updatedInbounds != nil {\n\t\twebsocket.BroadcastInbounds(updatedInbounds)\n\t}\n\n\tif updatedOutbounds != nil {\n\t\twebsocket.BroadcastOutbounds(updatedOutbounds)\n\t}\n\n}\n\nfunc (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) {\n\tinformURL, err := j.settingService.GetExternalTrafficInformURI()\n\tif err != nil {\n\t\tlogger.Warning(\"get ExternalTrafficInformURI failed:\", err)\n\t\treturn\n\t}\n\trequestBody, err := json.Marshal(map[string]any{\"clientTraffics\": clientTraffics, \"inboundTraffics\": inboundTraffics})\n\tif err != nil {\n\t\tlogger.Warning(\"parse client/inbound traffic failed:\", err)\n\t\treturn\n\t}\n\trequest := fasthttp.AcquireRequest()\n\tdefer fasthttp.ReleaseRequest(request)\n\trequest.Header.SetMethod(\"POST\")\n\trequest.Header.SetContentType(\"application/json; charset=UTF-8\")\n\trequest.SetBody([]byte(requestBody))\n\trequest.SetRequestURI(informURL)\n\tresponse := fasthttp.AcquireResponse()\n\tdefer fasthttp.ReleaseResponse(response)\n\tif err := fasthttp.Do(request, response); err != nil {\n\t\tlogger.Warning(\"POST ExternalTrafficInformURI failed:\", err)\n\t}\n}\n"
  },
  {
    "path": "web/locale/locale.go",
    "content": "// Package locale provides internationalization (i18n) support for the 3x-ui web panel,\n// including translation loading, localization, and middleware for web and bot interfaces.\npackage locale\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/nicksnyder/go-i18n/v2/i18n\"\n\t\"github.com/pelletier/go-toml/v2\"\n\t\"golang.org/x/text/language\"\n)\n\nvar (\n\ti18nBundle   *i18n.Bundle\n\tLocalizerWeb *i18n.Localizer\n\tLocalizerBot *i18n.Localizer\n)\n\n// I18nType represents the type of interface for internationalization.\ntype I18nType string\n\nconst (\n\tBot I18nType = \"bot\" // Bot interface type\n\tWeb I18nType = \"web\" // Web interface type\n)\n\n// SettingService interface defines methods for accessing locale settings.\ntype SettingService interface {\n\tGetTgLang() (string, error)\n}\n\n// InitLocalizer initializes the internationalization system with embedded translation files.\nfunc InitLocalizer(i18nFS embed.FS, settingService SettingService) error {\n\t// set default bundle to English\n\ti18nBundle = i18n.NewBundle(language.MustParse(\"en-US\"))\n\ti18nBundle.RegisterUnmarshalFunc(\"toml\", toml.Unmarshal)\n\n\t// parse files\n\tif err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {\n\t\treturn err\n\t}\n\n\t// setup bot locale\n\tif err := initTGBotLocalizer(settingService); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// createTemplateData creates a template data map from parameters with optional separator.\nfunc createTemplateData(params []string, separator ...string) map[string]any {\n\tvar sep string = \"==\"\n\tif len(separator) > 0 {\n\t\tsep = separator[0]\n\t}\n\n\ttemplateData := make(map[string]any)\n\tfor _, param := range params {\n\t\tparts := strings.SplitN(param, sep, 2)\n\t\ttemplateData[parts[0]] = parts[1]\n\t}\n\n\treturn templateData\n}\n\n// I18n retrieves a localized message for the given key and type.\n// It supports both bot and web contexts, with optional template parameters.\n// Returns the localized message or an empty string if localization fails.\nfunc I18n(i18nType I18nType, key string, params ...string) string {\n\tvar localizer *i18n.Localizer\n\n\tswitch i18nType {\n\tcase \"bot\":\n\t\tlocalizer = LocalizerBot\n\tcase \"web\":\n\t\tlocalizer = LocalizerWeb\n\tdefault:\n\t\tlogger.Errorf(\"Invalid type for I18n: %s\", i18nType)\n\t\treturn \"\"\n\t}\n\n\ttemplateData := createTemplateData(params)\n\n\tif localizer == nil {\n\t\t// Fallback to key if localizer not ready; prevents nil panic on pages like sub\n\t\treturn key\n\t}\n\n\tmsg, err := localizer.Localize(&i18n.LocalizeConfig{\n\t\tMessageID:    key,\n\t\tTemplateData: templateData,\n\t})\n\tif err != nil {\n\t\tlogger.Errorf(\"Failed to localize message: %v\", err)\n\t\treturn \"\"\n\t}\n\n\treturn msg\n}\n\n// initTGBotLocalizer initializes the bot localizer with the configured language.\nfunc initTGBotLocalizer(settingService SettingService) error {\n\tbotLang, err := settingService.GetTgLang()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tLocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)\n\treturn nil\n}\n\n// LocalizerMiddleware returns a Gin middleware that sets up localization for web requests.\n// It determines the user's language from cookies or Accept-Language header,\n// creates a localizer instance, and stores it in the Gin context for use in handlers.\n// Also provides the I18n function in the context for template rendering.\nfunc LocalizerMiddleware() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\t// Ensure bundle is initialized so creating a Localizer won't panic\n\t\tif i18nBundle == nil {\n\t\t\ti18nBundle = i18n.NewBundle(language.MustParse(\"en-US\"))\n\t\t\ti18nBundle.RegisterUnmarshalFunc(\"toml\", toml.Unmarshal)\n\t\t\t// Try lazy-load from disk when running sub server without InitLocalizer\n\t\t\tif err := loadTranslationsFromDisk(i18nBundle); err != nil {\n\t\t\t\tlogger.Warning(\"i18n lazy load failed:\", err)\n\t\t\t}\n\t\t}\n\t\tvar lang string\n\n\t\tif cookie, err := c.Request.Cookie(\"lang\"); err == nil {\n\t\t\tlang = cookie.Value\n\t\t} else {\n\t\t\tlang = c.GetHeader(\"Accept-Language\")\n\t\t}\n\n\t\tLocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)\n\n\t\tc.Set(\"localizer\", LocalizerWeb)\n\t\tc.Set(\"I18n\", I18n)\n\t\tc.Next()\n\t}\n}\n\n// loadTranslationsFromDisk attempts to load translation files from \"web/translation\" using the local filesystem.\nfunc loadTranslationsFromDisk(bundle *i18n.Bundle) error {\n\troot := os.DirFS(\"web\")\n\treturn fs.WalkDir(root, \"translation\", func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tdata, err := fs.ReadFile(root, path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = bundle.ParseMessageFileBytes(data, path)\n\t\treturn err\n\t})\n}\n\n// parseTranslationFiles parses embedded translation files and adds them to the i18n bundle.\nfunc parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {\n\terr := fs.WalkDir(i18nFS, \"translation\",\n\t\tfunc(path string, d fs.DirEntry, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif d.IsDir() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tdata, err := i18nFS.ReadFile(path)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t_, err = i18nBundle.ParseMessageFileBytes(data, path)\n\t\t\treturn err\n\t\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "web/middleware/domainValidator.go",
    "content": "// Package middleware provides HTTP middleware functions for the 3x-ui web panel,\n// including domain validation and URL redirection utilities.\npackage middleware\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// DomainValidatorMiddleware returns a Gin middleware that validates the request domain.\n// It extracts the host from the request, strips any port number, and compares it\n// against the configured domain. Requests from unauthorized domains are rejected\n// with HTTP 403 Forbidden status.\nfunc DomainValidatorMiddleware(domain string) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\thost := c.Request.Host\n\t\tif colonIndex := strings.LastIndex(host, \":\"); colonIndex != -1 {\n\t\t\thost, _, _ = net.SplitHostPort(c.Request.Host)\n\t\t}\n\n\t\tif host != domain {\n\t\t\tc.AbortWithStatus(http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "web/middleware/redirect.go",
    "content": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// RedirectMiddleware returns a Gin middleware that handles URL redirections.\n// It provides backward compatibility by redirecting old '/xui' paths to new '/panel' paths,\n// including API endpoints. The middleware performs permanent redirects (301) for SEO purposes.\nfunc RedirectMiddleware(basePath string) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\t// Redirect from old '/xui' path to '/panel'\n\t\tredirects := map[string]string{\n\t\t\t\"panel/API\": \"panel/api\",\n\t\t\t\"xui/API\":   \"panel/api\",\n\t\t\t\"xui\":       \"panel\",\n\t\t}\n\n\t\tpath := c.Request.URL.Path\n\t\tfor from, to := range redirects {\n\t\t\tfrom, to = basePath+from, basePath+to\n\n\t\t\tif strings.HasPrefix(path, from) {\n\t\t\t\tnewPath := to + path[len(from):]\n\n\t\t\t\tc.Redirect(http.StatusMovedPermanently, newPath)\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "web/network/auto_https_conn.go",
    "content": "// Package network provides network utilities for the 3x-ui web panel,\n// including automatic HTTP to HTTPS redirection functionality.\npackage network\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n)\n\n// AutoHttpsConn wraps a net.Conn to provide automatic HTTP to HTTPS redirection.\n// It intercepts the first read to detect HTTP requests and responds with a 307 redirect\n// to the HTTPS equivalent URL. Subsequent reads work normally for HTTPS connections.\ntype AutoHttpsConn struct {\n\tnet.Conn\n\n\tfirstBuf []byte\n\tbufStart int\n\n\treadRequestOnce sync.Once\n}\n\n// NewAutoHttpsConn creates a new AutoHttpsConn that wraps the given connection.\n// It enables automatic redirection of HTTP requests to HTTPS.\nfunc NewAutoHttpsConn(conn net.Conn) net.Conn {\n\treturn &AutoHttpsConn{\n\t\tConn: conn,\n\t}\n}\n\nfunc (c *AutoHttpsConn) readRequest() bool {\n\tc.firstBuf = make([]byte, 2048)\n\tn, err := c.Conn.Read(c.firstBuf)\n\tc.firstBuf = c.firstBuf[:n]\n\tif err != nil {\n\t\treturn false\n\t}\n\treader := bytes.NewReader(c.firstBuf)\n\tbufReader := bufio.NewReader(reader)\n\trequest, err := http.ReadRequest(bufReader)\n\tif err != nil {\n\t\treturn false\n\t}\n\tresp := http.Response{\n\t\tHeader: http.Header{},\n\t}\n\tresp.StatusCode = http.StatusTemporaryRedirect\n\tlocation := fmt.Sprintf(\"https://%v%v\", request.Host, request.RequestURI)\n\tresp.Header.Set(\"Location\", location)\n\tresp.Write(c.Conn)\n\tc.Close()\n\tc.firstBuf = nil\n\treturn true\n}\n\n// Read implements the net.Conn Read method with automatic HTTPS redirection.\n// On the first read, it checks if the request is HTTP and redirects to HTTPS if so.\n// Subsequent reads work normally.\nfunc (c *AutoHttpsConn) Read(buf []byte) (int, error) {\n\tc.readRequestOnce.Do(func() {\n\t\tc.readRequest()\n\t})\n\n\tif c.firstBuf != nil {\n\t\tn := copy(buf, c.firstBuf[c.bufStart:])\n\t\tc.bufStart += n\n\t\tif c.bufStart >= len(c.firstBuf) {\n\t\t\tc.firstBuf = nil\n\t\t}\n\t\treturn n, nil\n\t}\n\n\treturn c.Conn.Read(buf)\n}\n"
  },
  {
    "path": "web/network/auto_https_listener.go",
    "content": "package network\n\nimport \"net\"\n\n// AutoHttpsListener wraps a net.Listener to provide automatic HTTPS redirection.\n// It returns AutoHttpsConn connections that handle HTTP to HTTPS redirection.\ntype AutoHttpsListener struct {\n\tnet.Listener\n}\n\n// NewAutoHttpsListener creates a new AutoHttpsListener that wraps the given listener.\n// It enables automatic redirection of HTTP requests to HTTPS for all accepted connections.\nfunc NewAutoHttpsListener(listener net.Listener) net.Listener {\n\treturn &AutoHttpsListener{\n\t\tListener: listener,\n\t}\n}\n\n// Accept implements the net.Listener Accept method.\n// It accepts connections and wraps them with AutoHttpsConn for HTTPS redirection.\nfunc (l *AutoHttpsListener) Accept() (net.Conn, error) {\n\tconn, err := l.Listener.Accept()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn NewAutoHttpsConn(conn), nil\n}\n"
  },
  {
    "path": "web/service/config.json",
    "content": "{\n  \"log\": {\n    \"access\": \"none\",\n    \"dnsLog\": false,\n    \"error\": \"\",\n    \"loglevel\": \"warning\",\n    \"maskAddress\": \"\"\n  },\n  \"api\": {\n    \"tag\": \"api\",\n    \"services\": [\n      \"HandlerService\",\n      \"LoggerService\",\n      \"StatsService\"\n    ]\n  },\n  \"inbounds\": [\n    {\n      \"tag\": \"api\",\n      \"listen\": \"127.0.0.1\",\n      \"port\": 62789,\n      \"protocol\": \"tunnel\",\n      \"settings\": {\n        \"address\": \"127.0.0.1\"\n      }\n    }\n  ],\n  \"outbounds\": [\n    {\n      \"tag\": \"direct\",\n      \"protocol\": \"freedom\",\n      \"settings\": {\n        \"domainStrategy\": \"AsIs\",\n        \"redirect\": \"\",\n        \"noises\": []\n      }\n    },\n    {\n      \"tag\": \"blocked\",\n      \"protocol\": \"blackhole\",\n      \"settings\": {}\n    }\n  ],\n  \"policy\": {\n    \"levels\": {\n      \"0\": {\n        \"statsUserDownlink\": true,\n        \"statsUserUplink\": true\n      }\n    },\n    \"system\": {\n      \"statsInboundDownlink\": true,\n      \"statsInboundUplink\": true,\n      \"statsOutboundDownlink\": false,\n      \"statsOutboundUplink\": false\n    }\n  },\n  \"routing\": {\n    \"domainStrategy\": \"AsIs\",\n    \"rules\": [\n      {\n        \"type\": \"field\",\n        \"inboundTag\": [\n          \"api\"\n        ],\n        \"outboundTag\": \"api\"\n      },\n      {\n        \"type\": \"field\",\n        \"outboundTag\": \"blocked\",\n        \"ip\": [\n          \"geoip:private\"\n        ]\n      },\n      {\n        \"type\": \"field\",\n        \"outboundTag\": \"blocked\",\n        \"protocol\": [\n          \"bittorrent\"\n        ]\n      }\n    ]\n  },\n  \"stats\": {},\n  \"metrics\": {\n    \"tag\": \"metrics_out\",\n    \"listen\": \"127.0.0.1:11111\"\n  }\n}\n"
  },
  {
    "path": "web/service/inbound.go",
    "content": "// Package service provides business logic services for the 3x-ui web panel,\n// including inbound/outbound management, user administration, settings, and Xray integration.\npackage service\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n\n\t\"gorm.io/gorm\"\n)\n\n// InboundService provides business logic for managing Xray inbound configurations.\n// It handles CRUD operations for inbounds, client management, traffic monitoring,\n// and integration with the Xray API for real-time updates.\ntype InboundService struct {\n\txrayApi xray.XrayAPI\n}\n\n// GetInbounds retrieves all inbounds for a specific user.\n// Returns a slice of inbound models with their associated client statistics.\nfunc (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {\n\tdb := database.GetDB()\n\tvar inbounds []*model.Inbound\n\terr := db.Model(model.Inbound{}).Preload(\"ClientStats\").Where(\"user_id = ?\", userId).Find(&inbounds).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn nil, err\n\t}\n\t// Enrich client stats with UUID/SubId from inbound settings\n\tfor _, inbound := range inbounds {\n\t\tclients, _ := s.GetClients(inbound)\n\t\tif len(clients) == 0 || len(inbound.ClientStats) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\t// Build a map email -> client\n\t\tcMap := make(map[string]model.Client, len(clients))\n\t\tfor _, c := range clients {\n\t\t\tcMap[strings.ToLower(c.Email)] = c\n\t\t}\n\t\tfor i := range inbound.ClientStats {\n\t\t\temail := strings.ToLower(inbound.ClientStats[i].Email)\n\t\t\tif c, ok := cMap[email]; ok {\n\t\t\t\tinbound.ClientStats[i].UUID = c.ID\n\t\t\t\tinbound.ClientStats[i].SubId = c.SubID\n\t\t\t}\n\t\t}\n\t}\n\treturn inbounds, nil\n}\n\n// GetAllInbounds retrieves all inbounds from the database.\n// Returns a slice of all inbound models with their associated client statistics.\nfunc (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {\n\tdb := database.GetDB()\n\tvar inbounds []*model.Inbound\n\terr := db.Model(model.Inbound{}).Preload(\"ClientStats\").Find(&inbounds).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn nil, err\n\t}\n\t// Enrich client stats with UUID/SubId from inbound settings\n\tfor _, inbound := range inbounds {\n\t\tclients, _ := s.GetClients(inbound)\n\t\tif len(clients) == 0 || len(inbound.ClientStats) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tcMap := make(map[string]model.Client, len(clients))\n\t\tfor _, c := range clients {\n\t\t\tcMap[strings.ToLower(c.Email)] = c\n\t\t}\n\t\tfor i := range inbound.ClientStats {\n\t\t\temail := strings.ToLower(inbound.ClientStats[i].Email)\n\t\t\tif c, ok := cMap[email]; ok {\n\t\t\t\tinbound.ClientStats[i].UUID = c.ID\n\t\t\t\tinbound.ClientStats[i].SubId = c.SubID\n\t\t\t}\n\t\t}\n\t}\n\treturn inbounds, nil\n}\n\nfunc (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbound, error) {\n\tdb := database.GetDB()\n\tvar inbounds []*model.Inbound\n\terr := db.Model(model.Inbound{}).Where(\"traffic_reset = ?\", period).Find(&inbounds).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn nil, err\n\t}\n\treturn inbounds, nil\n}\n\nfunc (s *InboundService) checkPortExist(listen string, port int, ignoreId int) (bool, error) {\n\tdb := database.GetDB()\n\tif listen == \"\" || listen == \"0.0.0.0\" || listen == \"::\" || listen == \"::0\" {\n\t\tdb = db.Model(model.Inbound{}).Where(\"port = ?\", port)\n\t} else {\n\t\tdb = db.Model(model.Inbound{}).\n\t\t\tWhere(\"port = ?\", port).\n\t\t\tWhere(\n\t\t\t\tdb.Model(model.Inbound{}).Where(\n\t\t\t\t\t\"listen = ?\", listen,\n\t\t\t\t).Or(\n\t\t\t\t\t\"listen = \\\"\\\"\",\n\t\t\t\t).Or(\n\t\t\t\t\t\"listen = \\\"0.0.0.0\\\"\",\n\t\t\t\t).Or(\n\t\t\t\t\t\"listen = \\\"::\\\"\",\n\t\t\t\t).Or(\n\t\t\t\t\t\"listen = \\\"::0\\\"\"))\n\t}\n\tif ignoreId > 0 {\n\t\tdb = db.Where(\"id != ?\", ignoreId)\n\t}\n\tvar count int64\n\terr := db.Count(&count).Error\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn count > 0, nil\n}\n\nfunc (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) {\n\tsettings := map[string][]model.Client{}\n\tjson.Unmarshal([]byte(inbound.Settings), &settings)\n\tif settings == nil {\n\t\treturn nil, fmt.Errorf(\"setting is null\")\n\t}\n\n\tclients := settings[\"clients\"]\n\tif clients == nil {\n\t\treturn nil, nil\n\t}\n\treturn clients, nil\n}\n\nfunc (s *InboundService) getAllEmails() ([]string, error) {\n\tdb := database.GetDB()\n\tvar emails []string\n\terr := db.Raw(`\n\t\tSELECT JSON_EXTRACT(client.value, '$.email')\n\t\tFROM inbounds,\n\t\t\tJSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client\n\t\t`).Scan(&emails).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn emails, nil\n}\n\nfunc (s *InboundService) contains(slice []string, str string) bool {\n\tlowerStr := strings.ToLower(str)\n\tfor _, s := range slice {\n\t\tif strings.ToLower(s) == lowerStr {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) {\n\tallEmails, err := s.getAllEmails()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar emails []string\n\tfor _, client := range clients {\n\t\tif client.Email != \"\" {\n\t\t\tif s.contains(emails, client.Email) {\n\t\t\t\treturn client.Email, nil\n\t\t\t}\n\t\t\tif s.contains(allEmails, client.Email) {\n\t\t\t\treturn client.Email, nil\n\t\t\t}\n\t\t\temails = append(emails, client.Email)\n\t\t}\n\t}\n\treturn \"\", nil\n}\n\nfunc (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) {\n\tclients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tallEmails, err := s.getAllEmails()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar emails []string\n\tfor _, client := range clients {\n\t\tif client.Email != \"\" {\n\t\t\tif s.contains(emails, client.Email) {\n\t\t\t\treturn client.Email, nil\n\t\t\t}\n\t\t\tif s.contains(allEmails, client.Email) {\n\t\t\t\treturn client.Email, nil\n\t\t\t}\n\t\t\temails = append(emails, client.Email)\n\t\t}\n\t}\n\treturn \"\", nil\n}\n\n// AddInbound creates a new inbound configuration.\n// It validates port uniqueness, client email uniqueness, and required fields,\n// then saves the inbound to the database and optionally adds it to the running Xray instance.\n// Returns the created inbound, whether Xray needs restart, and any error.\nfunc (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {\n\texist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0)\n\tif err != nil {\n\t\treturn inbound, false, err\n\t}\n\tif exist {\n\t\treturn inbound, false, common.NewError(\"Port already exists:\", inbound.Port)\n\t}\n\n\texistEmail, err := s.checkEmailExistForInbound(inbound)\n\tif err != nil {\n\t\treturn inbound, false, err\n\t}\n\tif existEmail != \"\" {\n\t\treturn inbound, false, common.NewError(\"Duplicate email:\", existEmail)\n\t}\n\n\tclients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn inbound, false, err\n\t}\n\n\t// Ensure created_at and updated_at on clients in settings\n\tif len(clients) > 0 {\n\t\tvar settings map[string]any\n\t\tif err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil {\n\t\t\tnow := time.Now().Unix() * 1000\n\t\t\tupdatedClients := make([]model.Client, 0, len(clients))\n\t\t\tfor _, c := range clients {\n\t\t\t\tif c.CreatedAt == 0 {\n\t\t\t\t\tc.CreatedAt = now\n\t\t\t\t}\n\t\t\t\tc.UpdatedAt = now\n\t\t\t\tupdatedClients = append(updatedClients, c)\n\t\t\t}\n\t\t\tsettings[\"clients\"] = updatedClients\n\t\t\tif bs, err3 := json.MarshalIndent(settings, \"\", \"  \"); err3 == nil {\n\t\t\t\tinbound.Settings = string(bs)\n\t\t\t} else {\n\t\t\t\tlogger.Debug(\"Unable to marshal inbound settings with timestamps:\", err3)\n\t\t\t}\n\t\t} else if err2 != nil {\n\t\t\tlogger.Debug(\"Unable to parse inbound settings for timestamps:\", err2)\n\t\t}\n\t}\n\n\t// Secure client ID\n\tfor _, client := range clients {\n\t\tswitch inbound.Protocol {\n\t\tcase \"trojan\":\n\t\t\tif client.Password == \"\" {\n\t\t\t\treturn inbound, false, common.NewError(\"empty client ID\")\n\t\t\t}\n\t\tcase \"shadowsocks\":\n\t\t\tif client.Email == \"\" {\n\t\t\t\treturn inbound, false, common.NewError(\"empty client ID\")\n\t\t\t}\n\t\tdefault:\n\t\t\tif client.ID == \"\" {\n\t\t\t\treturn inbound, false, common.NewError(\"empty client ID\")\n\t\t\t}\n\t\t}\n\t}\n\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\tdefer func() {\n\t\tif err == nil {\n\t\t\ttx.Commit()\n\t\t} else {\n\t\t\ttx.Rollback()\n\t\t}\n\t}()\n\n\terr = tx.Save(inbound).Error\n\tif err == nil {\n\t\tif len(inbound.ClientStats) == 0 {\n\t\t\tfor _, client := range clients {\n\t\t\t\ts.AddClientStat(tx, inbound.Id, &client)\n\t\t\t}\n\t\t}\n\t} else {\n\t\treturn inbound, false, err\n\t}\n\n\tneedRestart := false\n\tif inbound.Enable {\n\t\ts.xrayApi.Init(p.GetAPIPort())\n\t\tinboundJson, err1 := json.MarshalIndent(inbound.GenXrayInboundConfig(), \"\", \"  \")\n\t\tif err1 != nil {\n\t\t\tlogger.Debug(\"Unable to marshal inbound config:\", err1)\n\t\t}\n\n\t\terr1 = s.xrayApi.AddInbound(inboundJson)\n\t\tif err1 == nil {\n\t\t\tlogger.Debug(\"New inbound added by api:\", inbound.Tag)\n\t\t} else {\n\t\t\tlogger.Debug(\"Unable to add inbound by api:\", err1)\n\t\t\tneedRestart = true\n\t\t}\n\t\ts.xrayApi.Close()\n\t}\n\n\treturn inbound, needRestart, err\n}\n\n// DelInbound deletes an inbound configuration by ID.\n// It removes the inbound from the database and the running Xray instance if active.\n// Returns whether Xray needs restart and any error.\nfunc (s *InboundService) DelInbound(id int) (bool, error) {\n\tdb := database.GetDB()\n\n\tvar tag string\n\tneedRestart := false\n\tresult := db.Model(model.Inbound{}).Select(\"tag\").Where(\"id = ? and enable = ?\", id, true).First(&tag)\n\tif result.Error == nil {\n\t\ts.xrayApi.Init(p.GetAPIPort())\n\t\terr1 := s.xrayApi.DelInbound(tag)\n\t\tif err1 == nil {\n\t\t\tlogger.Debug(\"Inbound deleted by api:\", tag)\n\t\t} else {\n\t\t\tlogger.Debug(\"Unable to delete inbound by api:\", err1)\n\t\t\tneedRestart = true\n\t\t}\n\t\ts.xrayApi.Close()\n\t} else {\n\t\tlogger.Debug(\"No enabled inbound founded to removing by api\", tag)\n\t}\n\n\t// Delete client traffics of inbounds\n\terr := db.Where(\"inbound_id = ?\", id).Delete(xray.ClientTraffic{}).Error\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tinbound, err := s.GetInbound(id)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tclients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfor _, client := range clients {\n\t\terr := s.DelClientIPs(db, client.Email)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\treturn needRestart, db.Delete(model.Inbound{}, id).Error\n}\n\nfunc (s *InboundService) GetInbound(id int) (*model.Inbound, error) {\n\tdb := database.GetDB()\n\tinbound := &model.Inbound{}\n\terr := db.Model(model.Inbound{}).First(inbound, id).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn inbound, nil\n}\n\n// UpdateInbound modifies an existing inbound configuration.\n// It validates changes, updates the database, and syncs with the running Xray instance.\n// Returns the updated inbound, whether Xray needs restart, and any error.\nfunc (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {\n\texist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id)\n\tif err != nil {\n\t\treturn inbound, false, err\n\t}\n\tif exist {\n\t\treturn inbound, false, common.NewError(\"Port already exists:\", inbound.Port)\n\t}\n\n\toldInbound, err := s.GetInbound(inbound.Id)\n\tif err != nil {\n\t\treturn inbound, false, err\n\t}\n\n\ttag := oldInbound.Tag\n\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ttx.Rollback()\n\t\t} else {\n\t\t\ttx.Commit()\n\t\t}\n\t}()\n\n\terr = s.updateClientTraffics(tx, oldInbound, inbound)\n\tif err != nil {\n\t\treturn inbound, false, err\n\t}\n\n\t// Ensure created_at and updated_at exist in inbound.Settings clients\n\t{\n\t\tvar oldSettings map[string]any\n\t\t_ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)\n\t\temailToCreated := map[string]int64{}\n\t\temailToUpdated := map[string]int64{}\n\t\tif oldSettings != nil {\n\t\t\tif oc, ok := oldSettings[\"clients\"].([]any); ok {\n\t\t\t\tfor _, it := range oc {\n\t\t\t\t\tif m, ok2 := it.(map[string]any); ok2 {\n\t\t\t\t\t\tif email, ok3 := m[\"email\"].(string); ok3 {\n\t\t\t\t\t\t\tswitch v := m[\"created_at\"].(type) {\n\t\t\t\t\t\t\tcase float64:\n\t\t\t\t\t\t\t\temailToCreated[email] = int64(v)\n\t\t\t\t\t\t\tcase int64:\n\t\t\t\t\t\t\t\temailToCreated[email] = v\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tswitch v := m[\"updated_at\"].(type) {\n\t\t\t\t\t\t\tcase float64:\n\t\t\t\t\t\t\t\temailToUpdated[email] = int64(v)\n\t\t\t\t\t\t\tcase int64:\n\t\t\t\t\t\t\t\temailToUpdated[email] = v\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tvar newSettings map[string]any\n\t\tif err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil {\n\t\t\tnow := time.Now().Unix() * 1000\n\t\t\tif nSlice, ok := newSettings[\"clients\"].([]any); ok {\n\t\t\t\tfor i := range nSlice {\n\t\t\t\t\tif m, ok2 := nSlice[i].(map[string]any); ok2 {\n\t\t\t\t\t\temail, _ := m[\"email\"].(string)\n\t\t\t\t\t\tif _, ok3 := m[\"created_at\"]; !ok3 {\n\t\t\t\t\t\t\tif v, ok4 := emailToCreated[email]; ok4 && v > 0 {\n\t\t\t\t\t\t\t\tm[\"created_at\"] = v\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tm[\"created_at\"] = now\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Preserve client's updated_at if present; do not bump on parent inbound update\n\t\t\t\t\t\tif _, hasUpdated := m[\"updated_at\"]; !hasUpdated {\n\t\t\t\t\t\t\tif v, ok4 := emailToUpdated[email]; ok4 && v > 0 {\n\t\t\t\t\t\t\t\tm[\"updated_at\"] = v\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnSlice[i] = m\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnewSettings[\"clients\"] = nSlice\n\t\t\t\tif bs, err3 := json.MarshalIndent(newSettings, \"\", \"  \"); err3 == nil {\n\t\t\t\t\tinbound.Settings = string(bs)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\toldInbound.Up = inbound.Up\n\toldInbound.Down = inbound.Down\n\toldInbound.Total = inbound.Total\n\toldInbound.Remark = inbound.Remark\n\toldInbound.Enable = inbound.Enable\n\toldInbound.ExpiryTime = inbound.ExpiryTime\n\toldInbound.TrafficReset = inbound.TrafficReset\n\toldInbound.Listen = inbound.Listen\n\toldInbound.Port = inbound.Port\n\toldInbound.Protocol = inbound.Protocol\n\toldInbound.Settings = inbound.Settings\n\toldInbound.StreamSettings = inbound.StreamSettings\n\toldInbound.Sniffing = inbound.Sniffing\n\tif inbound.Listen == \"\" || inbound.Listen == \"0.0.0.0\" || inbound.Listen == \"::\" || inbound.Listen == \"::0\" {\n\t\toldInbound.Tag = fmt.Sprintf(\"inbound-%v\", inbound.Port)\n\t} else {\n\t\toldInbound.Tag = fmt.Sprintf(\"inbound-%v:%v\", inbound.Listen, inbound.Port)\n\t}\n\n\tneedRestart := false\n\ts.xrayApi.Init(p.GetAPIPort())\n\tif s.xrayApi.DelInbound(tag) == nil {\n\t\tlogger.Debug(\"Old inbound deleted by api:\", tag)\n\t}\n\tif inbound.Enable {\n\t\tinboundJson, err2 := json.MarshalIndent(oldInbound.GenXrayInboundConfig(), \"\", \"  \")\n\t\tif err2 != nil {\n\t\t\tlogger.Debug(\"Unable to marshal updated inbound config:\", err2)\n\t\t\tneedRestart = true\n\t\t} else {\n\t\t\terr2 = s.xrayApi.AddInbound(inboundJson)\n\t\t\tif err2 == nil {\n\t\t\t\tlogger.Debug(\"Updated inbound added by api:\", oldInbound.Tag)\n\t\t\t} else {\n\t\t\t\tlogger.Debug(\"Unable to update inbound by api:\", err2)\n\t\t\t\tneedRestart = true\n\t\t\t}\n\t\t}\n\t}\n\ts.xrayApi.Close()\n\n\treturn inbound, needRestart, tx.Save(oldInbound).Error\n}\n\nfunc (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inbound, newInbound *model.Inbound) error {\n\toldClients, err := s.GetClients(oldInbound)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnewClients, err := s.GetClients(newInbound)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar emailExists bool\n\n\tfor _, oldClient := range oldClients {\n\t\temailExists = false\n\t\tfor _, newClient := range newClients {\n\t\t\tif oldClient.Email == newClient.Email {\n\t\t\t\temailExists = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !emailExists {\n\t\t\terr = s.DelClientStat(tx, oldClient.Email)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tfor _, newClient := range newClients {\n\t\temailExists = false\n\t\tfor _, oldClient := range oldClients {\n\t\t\tif newClient.Email == oldClient.Email {\n\t\t\t\temailExists = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !emailExists {\n\t\t\terr = s.AddClientStat(tx, oldInbound.Id, &newClient)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {\n\tclients, err := s.GetClients(data)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar settings map[string]any\n\terr = json.Unmarshal([]byte(data.Settings), &settings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tinterfaceClients := settings[\"clients\"].([]any)\n\t// Add timestamps for new clients being appended\n\tnowTs := time.Now().Unix() * 1000\n\tfor i := range interfaceClients {\n\t\tif cm, ok := interfaceClients[i].(map[string]any); ok {\n\t\t\tif _, ok2 := cm[\"created_at\"]; !ok2 {\n\t\t\t\tcm[\"created_at\"] = nowTs\n\t\t\t}\n\t\t\tcm[\"updated_at\"] = nowTs\n\t\t\tinterfaceClients[i] = cm\n\t\t}\n\t}\n\texistEmail, err := s.checkEmailsExistForClients(clients)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif existEmail != \"\" {\n\t\treturn false, common.NewError(\"Duplicate email:\", existEmail)\n\t}\n\n\toldInbound, err := s.GetInbound(data.Id)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Secure client ID\n\tfor _, client := range clients {\n\t\tswitch oldInbound.Protocol {\n\t\tcase \"trojan\":\n\t\t\tif client.Password == \"\" {\n\t\t\t\treturn false, common.NewError(\"empty client ID\")\n\t\t\t}\n\t\tcase \"shadowsocks\":\n\t\t\tif client.Email == \"\" {\n\t\t\t\treturn false, common.NewError(\"empty client ID\")\n\t\t\t}\n\t\tdefault:\n\t\t\tif client.ID == \"\" {\n\t\t\t\treturn false, common.NewError(\"empty client ID\")\n\t\t\t}\n\t\t}\n\t}\n\n\tvar oldSettings map[string]any\n\terr = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\toldClients := oldSettings[\"clients\"].([]any)\n\toldClients = append(oldClients, interfaceClients...)\n\n\toldSettings[\"clients\"] = oldClients\n\n\tnewSettings, err := json.MarshalIndent(oldSettings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\toldInbound.Settings = string(newSettings)\n\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ttx.Rollback()\n\t\t} else {\n\t\t\ttx.Commit()\n\t\t}\n\t}()\n\n\tneedRestart := false\n\ts.xrayApi.Init(p.GetAPIPort())\n\tfor _, client := range clients {\n\t\tif len(client.Email) > 0 {\n\t\t\ts.AddClientStat(tx, data.Id, &client)\n\t\t\tif client.Enable {\n\t\t\t\tcipher := \"\"\n\t\t\t\tif oldInbound.Protocol == \"shadowsocks\" {\n\t\t\t\t\tcipher = oldSettings[\"method\"].(string)\n\t\t\t\t}\n\t\t\t\terr1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{\n\t\t\t\t\t\"email\":    client.Email,\n\t\t\t\t\t\"id\":       client.ID,\n\t\t\t\t\t\"security\": client.Security,\n\t\t\t\t\t\"flow\":     client.Flow,\n\t\t\t\t\t\"password\": client.Password,\n\t\t\t\t\t\"cipher\":   cipher,\n\t\t\t\t})\n\t\t\t\tif err1 == nil {\n\t\t\t\t\tlogger.Debug(\"Client added by api:\", client.Email)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Debug(\"Error in adding client by api:\", err1)\n\t\t\t\t\tneedRestart = true\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tneedRestart = true\n\t\t}\n\t}\n\ts.xrayApi.Close()\n\n\treturn needRestart, tx.Save(oldInbound).Error\n}\n\nfunc (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) {\n\toldInbound, err := s.GetInbound(inboundId)\n\tif err != nil {\n\t\tlogger.Error(\"Load Old Data Error\")\n\t\treturn false, err\n\t}\n\tvar settings map[string]any\n\terr = json.Unmarshal([]byte(oldInbound.Settings), &settings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\temail := \"\"\n\tclient_key := \"id\"\n\tif oldInbound.Protocol == \"trojan\" {\n\t\tclient_key = \"password\"\n\t}\n\tif oldInbound.Protocol == \"shadowsocks\" {\n\t\tclient_key = \"email\"\n\t}\n\n\tinterfaceClients := settings[\"clients\"].([]any)\n\tvar newClients []any\n\tneedApiDel := false\n\tfor _, client := range interfaceClients {\n\t\tc := client.(map[string]any)\n\t\tc_id := c[client_key].(string)\n\t\tif c_id == clientId {\n\t\t\temail, _ = c[\"email\"].(string)\n\t\t\tneedApiDel, _ = c[\"enable\"].(bool)\n\t\t} else {\n\t\t\tnewClients = append(newClients, client)\n\t\t}\n\t}\n\n\tif len(newClients) == 0 {\n\t\treturn false, common.NewError(\"no client remained in Inbound\")\n\t}\n\n\tsettings[\"clients\"] = newClients\n\tnewSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\toldInbound.Settings = string(newSettings)\n\n\tdb := database.GetDB()\n\n\terr = s.DelClientIPs(db, email)\n\tif err != nil {\n\t\tlogger.Error(\"Error in delete client IPs\")\n\t\treturn false, err\n\t}\n\tneedRestart := false\n\n\tif len(email) > 0 {\n\t\tnotDepleted := true\n\t\terr = db.Model(xray.ClientTraffic{}).Select(\"enable\").Where(\"email = ?\", email).First(&notDepleted).Error\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Get stats error\")\n\t\t\treturn false, err\n\t\t}\n\t\terr = s.DelClientStat(db, email)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Delete stats Data Error\")\n\t\t\treturn false, err\n\t\t}\n\t\tif needApiDel && notDepleted {\n\t\t\ts.xrayApi.Init(p.GetAPIPort())\n\t\t\terr1 := s.xrayApi.RemoveUser(oldInbound.Tag, email)\n\t\t\tif err1 == nil {\n\t\t\t\tlogger.Debug(\"Client deleted by api:\", email)\n\t\t\t\tneedRestart = false\n\t\t\t} else {\n\t\t\t\tif strings.Contains(err1.Error(), fmt.Sprintf(\"User %s not found.\", email)) {\n\t\t\t\t\tlogger.Debug(\"User is already deleted. Nothing to do more...\")\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Debug(\"Error in deleting client by api:\", err1)\n\t\t\t\t\tneedRestart = true\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.xrayApi.Close()\n\t\t}\n\t}\n\treturn needRestart, db.Save(oldInbound).Error\n}\n\nfunc (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) (bool, error) {\n\t// TODO: check if TrafficReset field is updating\n\tclients, err := s.GetClients(data)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar settings map[string]any\n\terr = json.Unmarshal([]byte(data.Settings), &settings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tinterfaceClients := settings[\"clients\"].([]any)\n\n\toldInbound, err := s.GetInbound(data.Id)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\toldClients, err := s.GetClients(oldInbound)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\toldEmail := \"\"\n\tnewClientId := \"\"\n\tclientIndex := -1\n\tfor index, oldClient := range oldClients {\n\t\toldClientId := \"\"\n\t\tswitch oldInbound.Protocol {\n\t\tcase \"trojan\":\n\t\t\toldClientId = oldClient.Password\n\t\t\tnewClientId = clients[0].Password\n\t\tcase \"shadowsocks\":\n\t\t\toldClientId = oldClient.Email\n\t\t\tnewClientId = clients[0].Email\n\t\tdefault:\n\t\t\toldClientId = oldClient.ID\n\t\t\tnewClientId = clients[0].ID\n\t\t}\n\t\tif clientId == oldClientId {\n\t\t\toldEmail = oldClient.Email\n\t\t\tclientIndex = index\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Validate new client ID\n\tif newClientId == \"\" || clientIndex == -1 {\n\t\treturn false, common.NewError(\"empty client ID\")\n\t}\n\n\tif len(clients[0].Email) > 0 && clients[0].Email != oldEmail {\n\t\texistEmail, err := s.checkEmailsExistForClients(clients)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif existEmail != \"\" {\n\t\t\treturn false, common.NewError(\"Duplicate email:\", existEmail)\n\t\t}\n\t}\n\n\tvar oldSettings map[string]any\n\terr = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tsettingsClients := oldSettings[\"clients\"].([]any)\n\t// Preserve created_at and set updated_at for the replacing client\n\tvar preservedCreated any\n\tif clientIndex >= 0 && clientIndex < len(settingsClients) {\n\t\tif oldMap, ok := settingsClients[clientIndex].(map[string]any); ok {\n\t\t\tif v, ok2 := oldMap[\"created_at\"]; ok2 {\n\t\t\t\tpreservedCreated = v\n\t\t\t}\n\t\t}\n\t}\n\tif len(interfaceClients) > 0 {\n\t\tif newMap, ok := interfaceClients[0].(map[string]any); ok {\n\t\t\tif preservedCreated == nil {\n\t\t\t\tpreservedCreated = time.Now().Unix() * 1000\n\t\t\t}\n\t\t\tnewMap[\"created_at\"] = preservedCreated\n\t\t\tnewMap[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\tinterfaceClients[0] = newMap\n\t\t}\n\t}\n\tsettingsClients[clientIndex] = interfaceClients[0]\n\toldSettings[\"clients\"] = settingsClients\n\n\tnewSettings, err := json.MarshalIndent(oldSettings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\toldInbound.Settings = string(newSettings)\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ttx.Rollback()\n\t\t} else {\n\t\t\ttx.Commit()\n\t\t}\n\t}()\n\n\tif len(clients[0].Email) > 0 {\n\t\tif len(oldEmail) > 0 {\n\t\t\terr = s.UpdateClientStat(tx, oldEmail, &clients[0])\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\terr = s.UpdateClientIPs(tx, oldEmail, clients[0].Email)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t} else {\n\t\t\ts.AddClientStat(tx, data.Id, &clients[0])\n\t\t}\n\t} else {\n\t\terr = s.DelClientStat(tx, oldEmail)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\terr = s.DelClientIPs(tx, oldEmail)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\tneedRestart := false\n\tif len(oldEmail) > 0 {\n\t\ts.xrayApi.Init(p.GetAPIPort())\n\t\tif oldClients[clientIndex].Enable {\n\t\t\terr1 := s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail)\n\t\t\tif err1 == nil {\n\t\t\t\tlogger.Debug(\"Old client deleted by api:\", oldEmail)\n\t\t\t} else {\n\t\t\t\tif strings.Contains(err1.Error(), fmt.Sprintf(\"User %s not found.\", oldEmail)) {\n\t\t\t\t\tlogger.Debug(\"User is already deleted. Nothing to do more...\")\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Debug(\"Error in deleting client by api:\", err1)\n\t\t\t\t\tneedRestart = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif clients[0].Enable {\n\t\t\tcipher := \"\"\n\t\t\tif oldInbound.Protocol == \"shadowsocks\" {\n\t\t\t\tcipher = oldSettings[\"method\"].(string)\n\t\t\t}\n\t\t\terr1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{\n\t\t\t\t\"email\":    clients[0].Email,\n\t\t\t\t\"id\":       clients[0].ID,\n\t\t\t\t\"security\": clients[0].Security,\n\t\t\t\t\"flow\":     clients[0].Flow,\n\t\t\t\t\"password\": clients[0].Password,\n\t\t\t\t\"cipher\":   cipher,\n\t\t\t})\n\t\t\tif err1 == nil {\n\t\t\t\tlogger.Debug(\"Client edited by api:\", clients[0].Email)\n\t\t\t} else {\n\t\t\t\tlogger.Debug(\"Error in adding client by api:\", err1)\n\t\t\t\tneedRestart = true\n\t\t\t}\n\t\t}\n\t\ts.xrayApi.Close()\n\t} else {\n\t\tlogger.Debug(\"Client old email not found\")\n\t\tneedRestart = true\n\t}\n\treturn needRestart, tx.Save(oldInbound).Error\n}\n\nfunc (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {\n\tvar err error\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ttx.Rollback()\n\t\t} else {\n\t\t\ttx.Commit()\n\t\t}\n\t}()\n\terr = s.addInboundTraffic(tx, inboundTraffics)\n\tif err != nil {\n\t\treturn err, false\n\t}\n\terr = s.addClientTraffic(tx, clientTraffics)\n\tif err != nil {\n\t\treturn err, false\n\t}\n\n\tneedRestart0, count, err := s.autoRenewClients(tx)\n\tif err != nil {\n\t\tlogger.Warning(\"Error in renew clients:\", err)\n\t} else if count > 0 {\n\t\tlogger.Debugf(\"%v clients renewed\", count)\n\t}\n\n\tneedRestart1, count, err := s.disableInvalidClients(tx)\n\tif err != nil {\n\t\tlogger.Warning(\"Error in disabling invalid clients:\", err)\n\t} else if count > 0 {\n\t\tlogger.Debugf(\"%v clients disabled\", count)\n\t}\n\n\tneedRestart2, count, err := s.disableInvalidInbounds(tx)\n\tif err != nil {\n\t\tlogger.Warning(\"Error in disabling invalid inbounds:\", err)\n\t} else if count > 0 {\n\t\tlogger.Debugf(\"%v inbounds disabled\", count)\n\t}\n\treturn nil, (needRestart0 || needRestart1 || needRestart2)\n}\n\nfunc (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {\n\tif len(traffics) == 0 {\n\t\treturn nil\n\t}\n\n\tvar err error\n\n\tfor _, traffic := range traffics {\n\t\tif traffic.IsInbound {\n\t\t\terr = tx.Model(&model.Inbound{}).Where(\"tag = ?\", traffic.Tag).\n\t\t\t\tUpdates(map[string]any{\n\t\t\t\t\t\"up\":       gorm.Expr(\"up + ?\", traffic.Up),\n\t\t\t\t\t\"down\":     gorm.Expr(\"down + ?\", traffic.Down),\n\t\t\t\t\t\"all_time\": gorm.Expr(\"COALESCE(all_time, 0) + ?\", traffic.Up+traffic.Down),\n\t\t\t\t}).Error\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) {\n\tif len(traffics) == 0 {\n\t\t// Empty onlineUsers\n\t\tif p != nil {\n\t\t\tp.SetOnlineClients(make([]string, 0))\n\t\t}\n\t\treturn nil\n\t}\n\n\tonlineClients := make([]string, 0)\n\n\temails := make([]string, 0, len(traffics))\n\tfor _, traffic := range traffics {\n\t\temails = append(emails, traffic.Email)\n\t}\n\tdbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))\n\terr = tx.Model(xray.ClientTraffic{}).Where(\"email IN (?)\", emails).Find(&dbClientTraffics).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Avoid empty slice error\n\tif len(dbClientTraffics) == 0 {\n\t\treturn nil\n\t}\n\n\tdbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor dbTraffic_index := range dbClientTraffics {\n\t\tfor traffic_index := range traffics {\n\t\t\tif dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {\n\t\t\t\tdbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up\n\t\t\t\tdbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down\n\t\t\t\tdbClientTraffics[dbTraffic_index].AllTime += (traffics[traffic_index].Up + traffics[traffic_index].Down)\n\n\t\t\t\t// Add user in onlineUsers array on traffic\n\t\t\t\tif traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {\n\t\t\t\t\tonlineClients = append(onlineClients, traffics[traffic_index].Email)\n\t\t\t\t\tdbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set onlineUsers\n\tp.SetOnlineClients(onlineClients)\n\n\terr = tx.Save(dbClientTraffics).Error\n\tif err != nil {\n\t\tlogger.Warning(\"AddClientTraffic update data \", err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.ClientTraffic) ([]*xray.ClientTraffic, error) {\n\tinboundIds := make([]int, 0, len(dbClientTraffics))\n\tfor _, dbClientTraffic := range dbClientTraffics {\n\t\tif dbClientTraffic.ExpiryTime < 0 {\n\t\t\tinboundIds = append(inboundIds, dbClientTraffic.InboundId)\n\t\t}\n\t}\n\n\tif len(inboundIds) > 0 {\n\t\tvar inbounds []*model.Inbound\n\t\terr := tx.Model(model.Inbound{}).Where(\"id IN (?)\", inboundIds).Find(&inbounds).Error\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor inbound_index := range inbounds {\n\t\t\tsettings := map[string]any{}\n\t\t\tjson.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)\n\t\t\tclients, ok := settings[\"clients\"].([]any)\n\t\t\tif ok {\n\t\t\t\tvar newClients []any\n\t\t\t\tfor client_index := range clients {\n\t\t\t\t\tc := clients[client_index].(map[string]any)\n\t\t\t\t\tfor traffic_index := range dbClientTraffics {\n\t\t\t\t\t\tif dbClientTraffics[traffic_index].ExpiryTime < 0 && c[\"email\"] == dbClientTraffics[traffic_index].Email {\n\t\t\t\t\t\t\toldExpiryTime := c[\"expiryTime\"].(float64)\n\t\t\t\t\t\t\tnewExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)\n\t\t\t\t\t\t\tc[\"expiryTime\"] = newExpiryTime\n\t\t\t\t\t\t\tc[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\t\t\t\t\tdbClientTraffics[traffic_index].ExpiryTime = newExpiryTime\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Backfill created_at and updated_at\n\t\t\t\t\tif _, ok := c[\"created_at\"]; !ok {\n\t\t\t\t\t\tc[\"created_at\"] = time.Now().Unix() * 1000\n\t\t\t\t\t}\n\t\t\t\t\tc[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\t\t\tnewClients = append(newClients, any(c))\n\t\t\t\t}\n\t\t\t\tsettings[\"clients\"] = newClients\n\t\t\t\tmodifiedSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tinbounds[inbound_index].Settings = string(modifiedSettings)\n\t\t\t}\n\t\t}\n\t\terr = tx.Save(inbounds).Error\n\t\tif err != nil {\n\t\t\tlogger.Warning(\"AddClientTraffic update inbounds \", err)\n\t\t\tlogger.Error(inbounds)\n\t\t}\n\t}\n\n\treturn dbClientTraffics, nil\n}\n\nfunc (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {\n\t// check for time expired\n\tvar traffics []*xray.ClientTraffic\n\tnow := time.Now().Unix() * 1000\n\tvar err, err1 error\n\n\terr = tx.Model(xray.ClientTraffic{}).Where(\"reset > 0 and expiry_time > 0 and expiry_time <= ?\", now).Find(&traffics).Error\n\tif err != nil {\n\t\treturn false, 0, err\n\t}\n\t// return if there is no client to renew\n\tif len(traffics) == 0 {\n\t\treturn false, 0, nil\n\t}\n\n\tvar inbound_ids []int\n\tvar inbounds []*model.Inbound\n\tneedRestart := false\n\tvar clientsToAdd []struct {\n\t\tprotocol string\n\t\ttag      string\n\t\tclient   map[string]any\n\t}\n\n\tfor _, traffic := range traffics {\n\t\tinbound_ids = append(inbound_ids, traffic.InboundId)\n\t}\n\terr = tx.Model(model.Inbound{}).Where(\"id IN ?\", inbound_ids).Find(&inbounds).Error\n\tif err != nil {\n\t\treturn false, 0, err\n\t}\n\tfor inbound_index := range inbounds {\n\t\tsettings := map[string]any{}\n\t\tjson.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)\n\t\tclients := settings[\"clients\"].([]any)\n\t\tfor client_index := range clients {\n\t\t\tc := clients[client_index].(map[string]any)\n\t\t\tfor traffic_index, traffic := range traffics {\n\t\t\t\tif traffic.Email == c[\"email\"].(string) {\n\t\t\t\t\tnewExpiryTime := traffic.ExpiryTime\n\t\t\t\t\tfor newExpiryTime < now {\n\t\t\t\t\t\tnewExpiryTime += (int64(traffic.Reset) * 86400000)\n\t\t\t\t\t}\n\t\t\t\t\tc[\"expiryTime\"] = newExpiryTime\n\t\t\t\t\ttraffics[traffic_index].ExpiryTime = newExpiryTime\n\t\t\t\t\ttraffics[traffic_index].Down = 0\n\t\t\t\t\ttraffics[traffic_index].Up = 0\n\t\t\t\t\tif !traffic.Enable {\n\t\t\t\t\t\ttraffics[traffic_index].Enable = true\n\t\t\t\t\t\tclientsToAdd = append(clientsToAdd,\n\t\t\t\t\t\t\tstruct {\n\t\t\t\t\t\t\t\tprotocol string\n\t\t\t\t\t\t\t\ttag      string\n\t\t\t\t\t\t\t\tclient   map[string]any\n\t\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t\tprotocol: string(inbounds[inbound_index].Protocol),\n\t\t\t\t\t\t\t\ttag:      inbounds[inbound_index].Tag,\n\t\t\t\t\t\t\t\tclient:   c,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tclients[client_index] = any(c)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsettings[\"clients\"] = clients\n\t\tnewSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\t\tif err != nil {\n\t\t\treturn false, 0, err\n\t\t}\n\t\tinbounds[inbound_index].Settings = string(newSettings)\n\t}\n\terr = tx.Save(inbounds).Error\n\tif err != nil {\n\t\treturn false, 0, err\n\t}\n\terr = tx.Save(traffics).Error\n\tif err != nil {\n\t\treturn false, 0, err\n\t}\n\tif p != nil {\n\t\terr1 = s.xrayApi.Init(p.GetAPIPort())\n\t\tif err1 != nil {\n\t\t\treturn true, int64(len(traffics)), nil\n\t\t}\n\t\tfor _, clientToAdd := range clientsToAdd {\n\t\t\terr1 = s.xrayApi.AddUser(clientToAdd.protocol, clientToAdd.tag, clientToAdd.client)\n\t\t\tif err1 != nil {\n\t\t\t\tneedRestart = true\n\t\t\t}\n\t\t}\n\t\ts.xrayApi.Close()\n\t}\n\treturn needRestart, int64(len(traffics)), nil\n}\n\nfunc (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) {\n\tnow := time.Now().Unix() * 1000\n\tneedRestart := false\n\n\tif p != nil {\n\t\tvar tags []string\n\t\terr := tx.Table(\"inbounds\").\n\t\t\tSelect(\"inbounds.tag\").\n\t\t\tWhere(\"((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?\", now, true).\n\t\t\tScan(&tags).Error\n\t\tif err != nil {\n\t\t\treturn false, 0, err\n\t\t}\n\t\ts.xrayApi.Init(p.GetAPIPort())\n\t\tfor _, tag := range tags {\n\t\t\terr1 := s.xrayApi.DelInbound(tag)\n\t\t\tif err1 == nil {\n\t\t\t\tlogger.Debug(\"Inbound disabled by api:\", tag)\n\t\t\t} else {\n\t\t\t\tlogger.Debug(\"Error in disabling inbound by api:\", err1)\n\t\t\t\tneedRestart = true\n\t\t\t}\n\t\t}\n\t\ts.xrayApi.Close()\n\t}\n\n\tresult := tx.Model(model.Inbound{}).\n\t\tWhere(\"((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?\", now, true).\n\t\tUpdate(\"enable\", false)\n\terr := result.Error\n\tcount := result.RowsAffected\n\treturn needRestart, count, err\n}\n\nfunc (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) {\n\tnow := time.Now().Unix() * 1000\n\tneedRestart := false\n\n\tif p != nil {\n\t\tvar results []struct {\n\t\t\tTag   string\n\t\t\tEmail string\n\t\t}\n\n\t\terr := tx.Table(\"inbounds\").\n\t\t\tSelect(\"inbounds.tag, client_traffics.email\").\n\t\t\tJoins(\"JOIN client_traffics ON inbounds.id = client_traffics.inbound_id\").\n\t\t\tWhere(\"((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).\n\t\t\tScan(&results).Error\n\t\tif err != nil {\n\t\t\treturn false, 0, err\n\t\t}\n\t\ts.xrayApi.Init(p.GetAPIPort())\n\t\tfor _, result := range results {\n\t\t\terr1 := s.xrayApi.RemoveUser(result.Tag, result.Email)\n\t\t\tif err1 == nil {\n\t\t\t\tlogger.Debug(\"Client disabled by api:\", result.Email)\n\t\t\t} else {\n\t\t\t\tif strings.Contains(err1.Error(), fmt.Sprintf(\"User %s not found.\", result.Email)) {\n\t\t\t\t\tlogger.Debug(\"User is already disabled. Nothing to do more...\")\n\t\t\t\t} else {\n\t\t\t\t\tif strings.Contains(err1.Error(), fmt.Sprintf(\"User %s not found.\", result.Email)) {\n\t\t\t\t\t\tlogger.Debug(\"User is already disabled. Nothing to do more...\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogger.Debug(\"Error in disabling client by api:\", err1)\n\t\t\t\t\t\tneedRestart = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ts.xrayApi.Close()\n\t}\n\tresult := tx.Model(xray.ClientTraffic{}).\n\t\tWhere(\"((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?\", now, true).\n\t\tUpdate(\"enable\", false)\n\terr := result.Error\n\tcount := result.RowsAffected\n\treturn needRestart, count, err\n}\n\nfunc (s *InboundService) GetInboundTags() (string, error) {\n\tdb := database.GetDB()\n\tvar inboundTags []string\n\terr := db.Model(model.Inbound{}).Select(\"tag\").Find(&inboundTags).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn \"\", err\n\t}\n\ttags, _ := json.Marshal(inboundTags)\n\treturn string(tags), nil\n}\n\nfunc (s *InboundService) MigrationRemoveOrphanedTraffics() {\n\tdb := database.GetDB()\n\tdb.Exec(`\n\t\tDELETE FROM client_traffics\n\t\tWHERE email NOT IN (\n\t\t\tSELECT JSON_EXTRACT(client.value, '$.email')\n\t\t\tFROM inbounds,\n\t\t\t\tJSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client\n\t\t)\n\t`)\n}\n\nfunc (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error {\n\tclientTraffic := xray.ClientTraffic{}\n\tclientTraffic.InboundId = inboundId\n\tclientTraffic.Email = client.Email\n\tclientTraffic.Total = client.TotalGB\n\tclientTraffic.ExpiryTime = client.ExpiryTime\n\tclientTraffic.Enable = client.Enable\n\tclientTraffic.Up = 0\n\tclientTraffic.Down = 0\n\tclientTraffic.Reset = client.Reset\n\tresult := tx.Create(&clientTraffic)\n\terr := result.Error\n\treturn err\n}\n\nfunc (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {\n\tresult := tx.Model(xray.ClientTraffic{}).\n\t\tWhere(\"email = ?\", email).\n\t\tUpdates(map[string]any{\n\t\t\t\"enable\":      client.Enable,\n\t\t\t\"email\":       client.Email,\n\t\t\t\"total\":       client.TotalGB,\n\t\t\t\"expiry_time\": client.ExpiryTime,\n\t\t\t\"reset\":       client.Reset,\n\t\t})\n\terr := result.Error\n\treturn err\n}\n\nfunc (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {\n\treturn tx.Model(model.InboundClientIps{}).Where(\"client_email = ?\", oldEmail).Update(\"client_email\", newEmail).Error\n}\n\nfunc (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {\n\treturn tx.Where(\"email = ?\", email).Delete(xray.ClientTraffic{}).Error\n}\n\nfunc (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {\n\treturn tx.Where(\"client_email = ?\", email).Delete(model.InboundClientIps{}).Error\n}\n\nfunc (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {\n\tdb := database.GetDB()\n\tvar traffics []*xray.ClientTraffic\n\terr = db.Model(xray.ClientTraffic{}).Where(\"id = ?\", trafficId).Find(&traffics).Error\n\tif err != nil {\n\t\tlogger.Warningf(\"Error retrieving ClientTraffic with trafficId %d: %v\", trafficId, err)\n\t\treturn nil, nil, err\n\t}\n\tif len(traffics) > 0 {\n\t\tinbound, err = s.GetInbound(traffics[0].InboundId)\n\t\treturn traffics[0], inbound, err\n\t}\n\treturn nil, nil, nil\n}\n\nfunc (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {\n\tdb := database.GetDB()\n\tvar traffics []*xray.ClientTraffic\n\terr = db.Model(xray.ClientTraffic{}).Where(\"email = ?\", email).Find(&traffics).Error\n\tif err != nil {\n\t\tlogger.Warningf(\"Error retrieving ClientTraffic with email %s: %v\", email, err)\n\t\treturn nil, nil, err\n\t}\n\tif len(traffics) > 0 {\n\t\tinbound, err = s.GetInbound(traffics[0].InboundId)\n\t\treturn traffics[0], inbound, err\n\t}\n\treturn nil, nil, nil\n}\n\nfunc (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) {\n\ttraffic, inbound, err := s.GetClientInboundByEmail(clientEmail)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif inbound == nil {\n\t\treturn nil, nil, common.NewError(\"Inbound Not Found For Email:\", clientEmail)\n\t}\n\n\tclients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tfor _, client := range clients {\n\t\tif client.Email == clientEmail {\n\t\t\treturn traffic, &client, nil\n\t\t}\n\t}\n\n\treturn nil, nil, common.NewError(\"Client Not Found In Inbound For Email:\", clientEmail)\n}\n\nfunc (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (bool, error) {\n\ttraffic, inbound, err := s.GetClientInboundByTrafficID(trafficId)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif inbound == nil {\n\t\treturn false, common.NewError(\"Inbound Not Found For Traffic ID:\", trafficId)\n\t}\n\n\tclientEmail := traffic.Email\n\n\toldClients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tclientId := \"\"\n\n\tfor _, oldClient := range oldClients {\n\t\tif oldClient.Email == clientEmail {\n\t\t\tswitch inbound.Protocol {\n\t\t\tcase \"trojan\":\n\t\t\t\tclientId = oldClient.Password\n\t\t\tcase \"shadowsocks\":\n\t\t\t\tclientId = oldClient.Email\n\t\t\tdefault:\n\t\t\t\tclientId = oldClient.ID\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(clientId) == 0 {\n\t\treturn false, common.NewError(\"Client Not Found For Email:\", clientEmail)\n\t}\n\n\tvar settings map[string]any\n\terr = json.Unmarshal([]byte(inbound.Settings), &settings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tclients := settings[\"clients\"].([]any)\n\tvar newClients []any\n\tfor client_index := range clients {\n\t\tc := clients[client_index].(map[string]any)\n\t\tif c[\"email\"] == clientEmail {\n\t\t\tc[\"tgId\"] = tgId\n\t\t\tc[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\tnewClients = append(newClients, any(c))\n\t\t}\n\t}\n\tsettings[\"clients\"] = newClients\n\tmodifiedSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tinbound.Settings = string(modifiedSettings)\n\tneedRestart, err := s.UpdateInboundClient(inbound, clientId)\n\treturn needRestart, err\n}\n\nfunc (s *InboundService) checkIsEnabledByEmail(clientEmail string) (bool, error) {\n\t_, inbound, err := s.GetClientInboundByEmail(clientEmail)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif inbound == nil {\n\t\treturn false, common.NewError(\"Inbound Not Found For Email:\", clientEmail)\n\t}\n\n\tclients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tisEnable := false\n\n\tfor _, client := range clients {\n\t\tif client.Email == clientEmail {\n\t\t\tisEnable = client.Enable\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn isEnable, err\n}\n\nfunc (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bool, error) {\n\t_, inbound, err := s.GetClientInboundByEmail(clientEmail)\n\tif err != nil {\n\t\treturn false, false, err\n\t}\n\tif inbound == nil {\n\t\treturn false, false, common.NewError(\"Inbound Not Found For Email:\", clientEmail)\n\t}\n\n\toldClients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn false, false, err\n\t}\n\n\tclientId := \"\"\n\tclientOldEnabled := false\n\n\tfor _, oldClient := range oldClients {\n\t\tif oldClient.Email == clientEmail {\n\t\t\tswitch inbound.Protocol {\n\t\t\tcase \"trojan\":\n\t\t\t\tclientId = oldClient.Password\n\t\t\tcase \"shadowsocks\":\n\t\t\t\tclientId = oldClient.Email\n\t\t\tdefault:\n\t\t\t\tclientId = oldClient.ID\n\t\t\t}\n\t\t\tclientOldEnabled = oldClient.Enable\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(clientId) == 0 {\n\t\treturn false, false, common.NewError(\"Client Not Found For Email:\", clientEmail)\n\t}\n\n\tvar settings map[string]any\n\terr = json.Unmarshal([]byte(inbound.Settings), &settings)\n\tif err != nil {\n\t\treturn false, false, err\n\t}\n\tclients := settings[\"clients\"].([]any)\n\tvar newClients []any\n\tfor client_index := range clients {\n\t\tc := clients[client_index].(map[string]any)\n\t\tif c[\"email\"] == clientEmail {\n\t\t\tc[\"enable\"] = !clientOldEnabled\n\t\t\tc[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\tnewClients = append(newClients, any(c))\n\t\t}\n\t}\n\tsettings[\"clients\"] = newClients\n\tmodifiedSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, false, err\n\t}\n\tinbound.Settings = string(modifiedSettings)\n\n\tneedRestart, err := s.UpdateInboundClient(inbound, clientId)\n\tif err != nil {\n\t\treturn false, needRestart, err\n\t}\n\n\treturn !clientOldEnabled, needRestart, nil\n}\n\n// SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error)\nfunc (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) {\n\tcurrent, err := s.checkIsEnabledByEmail(clientEmail)\n\tif err != nil {\n\t\treturn false, false, err\n\t}\n\tif current == enable {\n\t\treturn false, false, nil\n\t}\n\tnewEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail)\n\tif err != nil {\n\t\treturn false, needRestart, err\n\t}\n\treturn newEnabled == enable, needRestart, nil\n}\n\nfunc (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) {\n\t_, inbound, err := s.GetClientInboundByEmail(clientEmail)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif inbound == nil {\n\t\treturn false, common.NewError(\"Inbound Not Found For Email:\", clientEmail)\n\t}\n\n\toldClients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tclientId := \"\"\n\n\tfor _, oldClient := range oldClients {\n\t\tif oldClient.Email == clientEmail {\n\t\t\tswitch inbound.Protocol {\n\t\t\tcase \"trojan\":\n\t\t\t\tclientId = oldClient.Password\n\t\t\tcase \"shadowsocks\":\n\t\t\t\tclientId = oldClient.Email\n\t\t\tdefault:\n\t\t\t\tclientId = oldClient.ID\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(clientId) == 0 {\n\t\treturn false, common.NewError(\"Client Not Found For Email:\", clientEmail)\n\t}\n\n\tvar settings map[string]any\n\terr = json.Unmarshal([]byte(inbound.Settings), &settings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tclients := settings[\"clients\"].([]any)\n\tvar newClients []any\n\tfor client_index := range clients {\n\t\tc := clients[client_index].(map[string]any)\n\t\tif c[\"email\"] == clientEmail {\n\t\t\tc[\"limitIp\"] = count\n\t\t\tc[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\tnewClients = append(newClients, any(c))\n\t\t}\n\t}\n\tsettings[\"clients\"] = newClients\n\tmodifiedSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tinbound.Settings = string(modifiedSettings)\n\tneedRestart, err := s.UpdateInboundClient(inbound, clientId)\n\treturn needRestart, err\n}\n\nfunc (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) (bool, error) {\n\t_, inbound, err := s.GetClientInboundByEmail(clientEmail)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif inbound == nil {\n\t\treturn false, common.NewError(\"Inbound Not Found For Email:\", clientEmail)\n\t}\n\n\toldClients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tclientId := \"\"\n\n\tfor _, oldClient := range oldClients {\n\t\tif oldClient.Email == clientEmail {\n\t\t\tswitch inbound.Protocol {\n\t\t\tcase \"trojan\":\n\t\t\t\tclientId = oldClient.Password\n\t\t\tcase \"shadowsocks\":\n\t\t\t\tclientId = oldClient.Email\n\t\t\tdefault:\n\t\t\t\tclientId = oldClient.ID\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(clientId) == 0 {\n\t\treturn false, common.NewError(\"Client Not Found For Email:\", clientEmail)\n\t}\n\n\tvar settings map[string]any\n\terr = json.Unmarshal([]byte(inbound.Settings), &settings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tclients := settings[\"clients\"].([]any)\n\tvar newClients []any\n\tfor client_index := range clients {\n\t\tc := clients[client_index].(map[string]any)\n\t\tif c[\"email\"] == clientEmail {\n\t\t\tc[\"expiryTime\"] = expiry_time\n\t\t\tc[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\tnewClients = append(newClients, any(c))\n\t\t}\n\t}\n\tsettings[\"clients\"] = newClients\n\tmodifiedSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tinbound.Settings = string(modifiedSettings)\n\tneedRestart, err := s.UpdateInboundClient(inbound, clientId)\n\treturn needRestart, err\n}\n\nfunc (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, totalGB int) (bool, error) {\n\tif totalGB < 0 {\n\t\treturn false, common.NewError(\"totalGB must be >= 0\")\n\t}\n\t_, inbound, err := s.GetClientInboundByEmail(clientEmail)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif inbound == nil {\n\t\treturn false, common.NewError(\"Inbound Not Found For Email:\", clientEmail)\n\t}\n\n\toldClients, err := s.GetClients(inbound)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tclientId := \"\"\n\n\tfor _, oldClient := range oldClients {\n\t\tif oldClient.Email == clientEmail {\n\t\t\tswitch inbound.Protocol {\n\t\t\tcase \"trojan\":\n\t\t\t\tclientId = oldClient.Password\n\t\t\tcase \"shadowsocks\":\n\t\t\t\tclientId = oldClient.Email\n\t\t\tdefault:\n\t\t\t\tclientId = oldClient.ID\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(clientId) == 0 {\n\t\treturn false, common.NewError(\"Client Not Found For Email:\", clientEmail)\n\t}\n\n\tvar settings map[string]any\n\terr = json.Unmarshal([]byte(inbound.Settings), &settings)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tclients := settings[\"clients\"].([]any)\n\tvar newClients []any\n\tfor client_index := range clients {\n\t\tc := clients[client_index].(map[string]any)\n\t\tif c[\"email\"] == clientEmail {\n\t\t\tc[\"totalGB\"] = totalGB * 1024 * 1024 * 1024\n\t\t\tc[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\tnewClients = append(newClients, any(c))\n\t\t}\n\t}\n\tsettings[\"clients\"] = newClients\n\tmodifiedSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tinbound.Settings = string(modifiedSettings)\n\tneedRestart, err := s.UpdateInboundClient(inbound, clientId)\n\treturn needRestart, err\n}\n\nfunc (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {\n\tdb := database.GetDB()\n\n\t// Reset traffic stats in ClientTraffic table\n\tresult := db.Model(xray.ClientTraffic{}).\n\t\tWhere(\"email = ?\", clientEmail).\n\t\tUpdates(map[string]any{\"enable\": true, \"up\": 0, \"down\": 0})\n\n\terr := result.Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, error) {\n\tneedRestart := false\n\n\ttraffic, err := s.GetClientTrafficByEmail(clientEmail)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif !traffic.Enable {\n\t\tinbound, err := s.GetInbound(id)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tclients, err := s.GetClients(inbound)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tfor _, client := range clients {\n\t\t\tif client.Email == clientEmail && client.Enable {\n\t\t\t\ts.xrayApi.Init(p.GetAPIPort())\n\t\t\t\tcipher := \"\"\n\t\t\t\tif string(inbound.Protocol) == \"shadowsocks\" {\n\t\t\t\t\tvar oldSettings map[string]any\n\t\t\t\t\terr = json.Unmarshal([]byte(inbound.Settings), &oldSettings)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn false, err\n\t\t\t\t\t}\n\t\t\t\t\tcipher = oldSettings[\"method\"].(string)\n\t\t\t\t}\n\t\t\t\terr1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{\n\t\t\t\t\t\"email\":    client.Email,\n\t\t\t\t\t\"id\":       client.ID,\n\t\t\t\t\t\"security\": client.Security,\n\t\t\t\t\t\"flow\":     client.Flow,\n\t\t\t\t\t\"password\": client.Password,\n\t\t\t\t\t\"cipher\":   cipher,\n\t\t\t\t})\n\t\t\t\tif err1 == nil {\n\t\t\t\t\tlogger.Debug(\"Client enabled due to reset traffic:\", clientEmail)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Debug(\"Error in enabling client by api:\", err1)\n\t\t\t\t\tneedRestart = true\n\t\t\t\t}\n\t\t\t\ts.xrayApi.Close()\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\ttraffic.Up = 0\n\ttraffic.Down = 0\n\ttraffic.Enable = true\n\n\tdb := database.GetDB()\n\terr = db.Save(traffic).Error\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn needRestart, nil\n}\n\nfunc (s *InboundService) ResetAllClientTraffics(id int) error {\n\tdb := database.GetDB()\n\tnow := time.Now().Unix() * 1000\n\n\treturn db.Transaction(func(tx *gorm.DB) error {\n\t\twhereText := \"inbound_id \"\n\t\tif id == -1 {\n\t\t\twhereText += \" > ?\"\n\t\t} else {\n\t\t\twhereText += \" = ?\"\n\t\t}\n\n\t\t// Reset client traffics\n\t\tresult := tx.Model(xray.ClientTraffic{}).\n\t\t\tWhere(whereText, id).\n\t\t\tUpdates(map[string]any{\"enable\": true, \"up\": 0, \"down\": 0})\n\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\t// Update lastTrafficResetTime for the inbound(s)\n\t\tinboundWhereText := \"id \"\n\t\tif id == -1 {\n\t\t\tinboundWhereText += \" > ?\"\n\t\t} else {\n\t\t\tinboundWhereText += \" = ?\"\n\t\t}\n\n\t\tresult = tx.Model(model.Inbound{}).\n\t\t\tWhere(inboundWhereText, id).\n\t\t\tUpdate(\"last_traffic_reset_time\", now)\n\n\t\treturn result.Error\n\t})\n}\n\nfunc (s *InboundService) ResetAllTraffics() error {\n\tdb := database.GetDB()\n\n\tresult := db.Model(model.Inbound{}).\n\t\tWhere(\"user_id > ?\", 0).\n\t\tUpdates(map[string]any{\"up\": 0, \"down\": 0})\n\n\terr := result.Error\n\treturn err\n}\n\nfunc (s *InboundService) DelDepletedClients(id int) (err error) {\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\tdefer func() {\n\t\tif err == nil {\n\t\t\ttx.Commit()\n\t\t} else {\n\t\t\ttx.Rollback()\n\t\t}\n\t}()\n\n\twhereText := \"reset = 0 and inbound_id \"\n\tif id < 0 {\n\t\twhereText += \"> ?\"\n\t} else {\n\t\twhereText += \"= ?\"\n\t}\n\n\t// Only consider truly depleted clients: expired OR traffic exhausted\n\tnow := time.Now().Unix() * 1000\n\tdepletedClients := []xray.ClientTraffic{}\n\terr = db.Model(xray.ClientTraffic{}).\n\t\tWhere(whereText+\" and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))\", id, now).\n\t\tSelect(\"inbound_id, GROUP_CONCAT(email) as email\").\n\t\tGroup(\"inbound_id\").\n\t\tFind(&depletedClients).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, depletedClient := range depletedClients {\n\t\temails := strings.Split(depletedClient.Email, \",\")\n\t\toldInbound, err := s.GetInbound(depletedClient.InboundId)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar oldSettings map[string]any\n\t\terr = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\toldClients := oldSettings[\"clients\"].([]any)\n\t\tvar newClients []any\n\t\tfor _, client := range oldClients {\n\t\t\tdeplete := false\n\t\t\tc := client.(map[string]any)\n\t\t\tfor _, email := range emails {\n\t\t\t\tif email == c[\"email\"].(string) {\n\t\t\t\t\tdeplete = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !deplete {\n\t\t\t\tnewClients = append(newClients, client)\n\t\t\t}\n\t\t}\n\t\tif len(newClients) > 0 {\n\t\t\toldSettings[\"clients\"] = newClients\n\n\t\t\tnewSettings, err := json.MarshalIndent(oldSettings, \"\", \"  \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\toldInbound.Settings = string(newSettings)\n\t\t\terr = tx.Save(oldInbound).Error\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\t// Delete inbound if no client remains\n\t\t\ts.DelInbound(depletedClient.InboundId)\n\t\t}\n\t}\n\n\t// Delete stats only for truly depleted clients\n\terr = tx.Where(whereText+\" and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))\", id, now).Delete(xray.ClientTraffic{}).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) {\n\tdb := database.GetDB()\n\tvar inbounds []*model.Inbound\n\n\t// Retrieve inbounds where settings contain the given tgId\n\terr := db.Model(model.Inbound{}).Where(\"settings LIKE ?\", fmt.Sprintf(`%%\"tgId\": %d%%`, tgId)).Find(&inbounds).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\tlogger.Errorf(\"Error retrieving inbounds with tgId %d: %v\", tgId, err)\n\t\treturn nil, err\n\t}\n\n\tvar emails []string\n\tfor _, inbound := range inbounds {\n\t\tclients, err := s.GetClients(inbound)\n\t\tif err != nil {\n\t\t\tlogger.Errorf(\"Error retrieving clients for inbound %d: %v\", inbound.Id, err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, client := range clients {\n\t\t\tif client.TgID == tgId {\n\t\t\t\temails = append(emails, client.Email)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar traffics []*xray.ClientTraffic\n\terr = db.Model(xray.ClientTraffic{}).Where(\"email IN ?\", emails).Find(&traffics).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\tlogger.Warning(\"No ClientTraffic records found for emails:\", emails)\n\t\t\treturn nil, nil\n\t\t}\n\t\tlogger.Errorf(\"Error retrieving ClientTraffic for emails %v: %v\", emails, err)\n\t\treturn nil, err\n\t}\n\n\t// Populate UUID and other client data for each traffic record\n\tfor i := range traffics {\n\t\tif ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {\n\t\t\ttraffics[i].Enable = client.Enable\n\t\t\ttraffics[i].UUID = client.ID\n\t\t\ttraffics[i].SubId = client.SubID\n\t\t}\n\t}\n\n\treturn traffics, nil\n}\n\nfunc (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.ClientTraffic, err error) {\n\t// Prefer retrieving along with client to reflect actual enabled state from inbound settings\n\tt, client, err := s.GetClientByEmail(email)\n\tif err != nil {\n\t\tlogger.Warningf(\"Error retrieving ClientTraffic with email %s: %v\", email, err)\n\t\treturn nil, err\n\t}\n\tif t != nil && client != nil {\n\t\tt.UUID = client.ID\n\t\tt.SubId = client.SubID\n\t\treturn t, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64, download int64) error {\n\tdb := database.GetDB()\n\n\tresult := db.Model(xray.ClientTraffic{}).\n\t\tWhere(\"email = ?\", email).\n\t\tUpdates(map[string]any{\"up\": upload, \"down\": download})\n\n\terr := result.Error\n\tif err != nil {\n\t\tlogger.Warningf(\"Error updating ClientTraffic with email %s: %v\", email, err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, error) {\n\tdb := database.GetDB()\n\tvar traffics []xray.ClientTraffic\n\n\terr := db.Model(xray.ClientTraffic{}).Where(`email IN(\n\t\tSELECT JSON_EXTRACT(client.value, '$.email') as email\n\t\tFROM inbounds,\n\t  \tJSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client\n\t\tWHERE\n\t  \tJSON_EXTRACT(client.value, '$.id') in (?)\n\t\t)`, id).Find(&traffics).Error\n\n\tif err != nil {\n\t\tlogger.Debug(err)\n\t\treturn nil, err\n\t}\n\t// Reconcile enable flag with client settings per email to avoid stale DB value\n\tfor i := range traffics {\n\t\tif ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {\n\t\t\ttraffics[i].Enable = client.Enable\n\t\t\ttraffics[i].UUID = client.ID\n\t\t\ttraffics[i].SubId = client.SubID\n\t\t}\n\t}\n\treturn traffics, err\n}\n\nfunc (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {\n\tdb := database.GetDB()\n\tinbound := &model.Inbound{}\n\ttraffic = &xray.ClientTraffic{}\n\n\t// Search for inbound settings that contain the query\n\terr = db.Model(model.Inbound{}).Where(\"settings LIKE ?\", \"%\\\"\"+query+\"\\\"%\").First(inbound).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\tlogger.Warningf(\"Inbound settings containing query %s not found: %v\", query, err)\n\t\t\treturn nil, err\n\t\t}\n\t\tlogger.Errorf(\"Error searching for inbound settings with query %s: %v\", query, err)\n\t\treturn nil, err\n\t}\n\n\ttraffic.InboundId = inbound.Id\n\n\t// Unmarshal settings to get clients\n\tsettings := map[string][]model.Client{}\n\tif err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {\n\t\tlogger.Errorf(\"Error unmarshalling inbound settings for inbound ID %d: %v\", inbound.Id, err)\n\t\treturn nil, err\n\t}\n\n\tclients := settings[\"clients\"]\n\tfor _, client := range clients {\n\t\tif (client.ID == query || client.Password == query) && client.Email != \"\" {\n\t\t\ttraffic.Email = client.Email\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif traffic.Email == \"\" {\n\t\tlogger.Warningf(\"No client found with query %s in inbound ID %d\", query, inbound.Id)\n\t\treturn nil, gorm.ErrRecordNotFound\n\t}\n\n\t// Retrieve ClientTraffic based on the found email\n\terr = db.Model(xray.ClientTraffic{}).Where(\"email = ?\", traffic.Email).First(traffic).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\tlogger.Warningf(\"ClientTraffic for email %s not found: %v\", traffic.Email, err)\n\t\t\treturn nil, err\n\t\t}\n\t\tlogger.Errorf(\"Error retrieving ClientTraffic for email %s: %v\", traffic.Email, err)\n\t\treturn nil, err\n\t}\n\n\treturn traffic, nil\n}\n\nfunc (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {\n\tdb := database.GetDB()\n\tInboundClientIps := &model.InboundClientIps{}\n\terr := db.Model(model.InboundClientIps{}).Where(\"client_email = ?\", clientEmail).First(InboundClientIps).Error\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif InboundClientIps.Ips == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\t// Try to parse as new format (with timestamps)\n\ttype IPWithTimestamp struct {\n\t\tIP        string `json:\"ip\"`\n\t\tTimestamp int64  `json:\"timestamp\"`\n\t}\n\n\tvar ipsWithTime []IPWithTimestamp\n\terr = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)\n\n\t// If successfully parsed as new format, return with timestamps\n\tif err == nil && len(ipsWithTime) > 0 {\n\t\treturn InboundClientIps.Ips, nil\n\t}\n\n\t// Otherwise, assume it's old format (simple string array)\n\t// Try to parse as simple array and convert to new format\n\tvar oldIps []string\n\terr = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)\n\tif err == nil && len(oldIps) > 0 {\n\t\t// Convert old format to new format with current timestamp\n\t\tnewIpsWithTime := make([]IPWithTimestamp, len(oldIps))\n\t\tfor i, ip := range oldIps {\n\t\t\tnewIpsWithTime[i] = IPWithTimestamp{\n\t\t\t\tIP:        ip,\n\t\t\t\tTimestamp: time.Now().Unix(),\n\t\t\t}\n\t\t}\n\t\tresult, _ := json.Marshal(newIpsWithTime)\n\t\treturn string(result), nil\n\t}\n\n\t// Return as-is if parsing fails\n\treturn InboundClientIps.Ips, nil\n}\n\nfunc (s *InboundService) ClearClientIps(clientEmail string) error {\n\tdb := database.GetDB()\n\n\tresult := db.Model(model.InboundClientIps{}).\n\t\tWhere(\"client_email = ?\", clientEmail).\n\t\tUpdate(\"ips\", \"\")\n\terr := result.Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {\n\tdb := database.GetDB()\n\tvar inbounds []*model.Inbound\n\terr := db.Model(model.Inbound{}).Preload(\"ClientStats\").Where(\"remark like ?\", \"%\"+query+\"%\").Find(&inbounds).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn nil, err\n\t}\n\treturn inbounds, nil\n}\n\nfunc (s *InboundService) MigrationRequirements() {\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\tvar err error\n\tdefer func() {\n\t\tif err == nil {\n\t\t\ttx.Commit()\n\t\t\tif dbErr := db.Exec(`VACUUM \"main\"`).Error; dbErr != nil {\n\t\t\t\tlogger.Warningf(\"VACUUM failed: %v\", dbErr)\n\t\t\t}\n\t\t} else {\n\t\t\ttx.Rollback()\n\t\t}\n\t}()\n\n\t// Calculate and backfill all_time from up+down for inbounds and clients\n\terr = tx.Exec(`\n\t\tUPDATE inbounds\n\t\tSET all_time = IFNULL(up, 0) + IFNULL(down, 0)\n\t\tWHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0\n\t`).Error\n\tif err != nil {\n\t\treturn\n\t}\n\terr = tx.Exec(`\n\t\tUPDATE client_traffics\n\t\tSET all_time = IFNULL(up, 0) + IFNULL(down, 0)\n\t\tWHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0\n\t`).Error\n\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Fix inbounds based problems\n\tvar inbounds []*model.Inbound\n\terr = tx.Model(model.Inbound{}).Where(\"protocol IN (?)\", []string{\"vmess\", \"vless\", \"trojan\"}).Find(&inbounds).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn\n\t}\n\tfor inbound_index := range inbounds {\n\t\tsettings := map[string]any{}\n\t\tjson.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)\n\t\tclients, ok := settings[\"clients\"].([]any)\n\t\tif ok {\n\t\t\t// Fix Client configuration problems\n\t\t\tvar newClients []any\n\t\t\tfor client_index := range clients {\n\t\t\t\tc := clients[client_index].(map[string]any)\n\n\t\t\t\t// Add email='' if it is not exists\n\t\t\t\tif _, ok := c[\"email\"]; !ok {\n\t\t\t\t\tc[\"email\"] = \"\"\n\t\t\t\t}\n\n\t\t\t\t// Convert string tgId to int64\n\t\t\t\tif _, ok := c[\"tgId\"]; ok {\n\t\t\t\t\tvar tgId any = c[\"tgId\"]\n\t\t\t\t\tif tgIdStr, ok2 := tgId.(string); ok2 {\n\t\t\t\t\t\ttgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, \" \", \"\"), 10, 64)\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\tc[\"tgId\"] = tgIdInt64\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Remove \"flow\": \"xtls-rprx-direct\"\n\t\t\t\tif _, ok := c[\"flow\"]; ok {\n\t\t\t\t\tif c[\"flow\"] == \"xtls-rprx-direct\" {\n\t\t\t\t\t\tc[\"flow\"] = \"\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Backfill created_at and updated_at\n\t\t\t\tif _, ok := c[\"created_at\"]; !ok {\n\t\t\t\t\tc[\"created_at\"] = time.Now().Unix() * 1000\n\t\t\t\t}\n\t\t\t\tc[\"updated_at\"] = time.Now().Unix() * 1000\n\t\t\t\tnewClients = append(newClients, any(c))\n\t\t\t}\n\t\t\tsettings[\"clients\"] = newClients\n\t\t\tmodifiedSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tinbounds[inbound_index].Settings = string(modifiedSettings)\n\t\t}\n\n\t\t// Add client traffic row for all clients which has email\n\t\tmodelClients, err := s.GetClients(inbounds[inbound_index])\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tfor _, modelClient := range modelClients {\n\t\t\tif len(modelClient.Email) > 0 {\n\t\t\t\tvar count int64\n\t\t\t\ttx.Model(xray.ClientTraffic{}).Where(\"email = ?\", modelClient.Email).Count(&count)\n\t\t\t\tif count == 0 {\n\t\t\t\t\ts.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\ttx.Save(inbounds)\n\n\t// Remove orphaned traffics\n\ttx.Where(\"inbound_id = 0\").Delete(xray.ClientTraffic{})\n\n\t// Migrate old MultiDomain to External Proxy\n\tvar externalProxy []struct {\n\t\tId             int\n\t\tPort           int\n\t\tStreamSettings []byte\n\t}\n\terr = tx.Raw(`select id, port, stream_settings\n\tfrom inbounds\n\tWHERE protocol in ('vmess','vless','trojan')\n\t  AND json_extract(stream_settings, '$.security') = 'tls'\n\t  AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error\n\tif err != nil || len(externalProxy) == 0 {\n\t\treturn\n\t}\n\n\tfor _, ep := range externalProxy {\n\t\tvar reverses any\n\t\tvar stream map[string]any\n\t\tjson.Unmarshal(ep.StreamSettings, &stream)\n\t\tif tlsSettings, ok := stream[\"tlsSettings\"].(map[string]any); ok {\n\t\t\tif settings, ok := tlsSettings[\"settings\"].(map[string]any); ok {\n\t\t\t\tif domains, ok := settings[\"domains\"].([]any); ok {\n\t\t\t\t\tfor _, domain := range domains {\n\t\t\t\t\t\tif domainMap, ok := domain.(map[string]any); ok {\n\t\t\t\t\t\t\tdomainMap[\"forceTls\"] = \"same\"\n\t\t\t\t\t\t\tdomainMap[\"port\"] = ep.Port\n\t\t\t\t\t\t\tdomainMap[\"dest\"] = domainMap[\"domain\"].(string)\n\t\t\t\t\t\t\tdelete(domainMap, \"domain\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treverses = settings[\"domains\"]\n\t\t\t\tdelete(settings, \"domains\")\n\t\t\t}\n\t\t}\n\t\tstream[\"externalProxy\"] = reverses\n\t\tnewStream, _ := json.MarshalIndent(stream, \" \", \"  \")\n\t\ttx.Model(model.Inbound{}).Where(\"id = ?\", ep.Id).Update(\"stream_settings\", newStream)\n\t}\n\n\terr = tx.Raw(`UPDATE inbounds\n\tSET tag = REPLACE(tag, '0.0.0.0:', '')\n\tWHERE INSTR(tag, '0.0.0.0:') > 0;`).Error\n\tif err != nil {\n\t\treturn\n\t}\n}\n\nfunc (s *InboundService) MigrateDB() {\n\ts.MigrationRequirements()\n\ts.MigrationRemoveOrphanedTraffics()\n}\n\nfunc (s *InboundService) GetOnlineClients() []string {\n\treturn p.GetOnlineClients()\n}\n\nfunc (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {\n\tdb := database.GetDB()\n\tvar rows []xray.ClientTraffic\n\terr := db.Model(&xray.ClientTraffic{}).Select(\"email, last_online\").Find(&rows).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn nil, err\n\t}\n\tresult := make(map[string]int64, len(rows))\n\tfor _, r := range rows {\n\t\tresult[r.Email] = r.LastOnline\n\t}\n\treturn result, nil\n}\n\nfunc (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {\n\tdb := database.GetDB()\n\n\t// Step 1: Get ClientTraffic records for emails in the input list\n\tvar clients []xray.ClientTraffic\n\terr := db.Where(\"email IN ?\", emails).Find(&clients).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn nil, nil, err\n\t}\n\n\t// Step 2: Sort clients by (Up + Down) descending\n\tsort.Slice(clients, func(i, j int) bool {\n\t\treturn (clients[i].Up + clients[i].Down) > (clients[j].Up + clients[j].Down)\n\t})\n\n\t// Step 3: Extract sorted valid emails and track found ones\n\tvalidEmails := make([]string, 0, len(clients))\n\tfound := make(map[string]bool)\n\tfor _, client := range clients {\n\t\tvalidEmails = append(validEmails, client.Email)\n\t\tfound[client.Email] = true\n\t}\n\n\t// Step 4: Identify emails that were not found in the database\n\textraEmails := make([]string, 0)\n\tfor _, email := range emails {\n\t\tif !found[email] {\n\t\t\textraEmails = append(extraEmails, email)\n\t\t}\n\t}\n\n\treturn validEmails, extraEmails, nil\n}\nfunc (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {\n\toldInbound, err := s.GetInbound(inboundId)\n\tif err != nil {\n\t\tlogger.Error(\"Load Old Data Error\")\n\t\treturn false, err\n\t}\n\n\tvar settings map[string]any\n\tif err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {\n\t\treturn false, err\n\t}\n\n\tinterfaceClients, ok := settings[\"clients\"].([]any)\n\tif !ok {\n\t\treturn false, common.NewError(\"invalid clients format in inbound settings\")\n\t}\n\n\tvar newClients []any\n\tneedApiDel := false\n\tfound := false\n\n\tfor _, client := range interfaceClients {\n\t\tc, ok := client.(map[string]any)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif cEmail, ok := c[\"email\"].(string); ok && cEmail == email {\n\t\t\t// matched client, drop it\n\t\t\tfound = true\n\t\t\tneedApiDel, _ = c[\"enable\"].(bool)\n\t\t} else {\n\t\t\tnewClients = append(newClients, client)\n\t\t}\n\t}\n\n\tif !found {\n\t\treturn false, common.NewError(fmt.Sprintf(\"client with email %s not found\", email))\n\t}\n\tif len(newClients) == 0 {\n\t\treturn false, common.NewError(\"no client remained in Inbound\")\n\t}\n\n\tsettings[\"clients\"] = newClients\n\tnewSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\toldInbound.Settings = string(newSettings)\n\n\tdb := database.GetDB()\n\n\t// remove IP bindings\n\tif err := s.DelClientIPs(db, email); err != nil {\n\t\tlogger.Error(\"Error in delete client IPs\")\n\t\treturn false, err\n\t}\n\n\tneedRestart := false\n\n\t// remove stats too\n\tif len(email) > 0 {\n\t\ttraffic, err := s.GetClientTrafficByEmail(email)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif traffic != nil {\n\t\t\tif err := s.DelClientStat(db, email); err != nil {\n\t\t\t\tlogger.Error(\"Delete stats Data Error\")\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t}\n\n\t\tif needApiDel {\n\t\t\ts.xrayApi.Init(p.GetAPIPort())\n\t\t\tif err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email); err1 == nil {\n\t\t\t\tlogger.Debug(\"Client deleted by api:\", email)\n\t\t\t\tneedRestart = false\n\t\t\t} else {\n\t\t\t\tif strings.Contains(err1.Error(), fmt.Sprintf(\"User %s not found.\", email)) {\n\t\t\t\t\tlogger.Debug(\"User is already deleted. Nothing to do more...\")\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Debug(\"Error in deleting client by api:\", err1)\n\t\t\t\t\tneedRestart = true\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.xrayApi.Close()\n\t\t}\n\t}\n\n\treturn needRestart, db.Save(oldInbound).Error\n}\n"
  },
  {
    "path": "web/service/outbound.go",
    "content": "package service\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/json_util\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n\n\t\"gorm.io/gorm\"\n)\n\n// OutboundService provides business logic for managing Xray outbound configurations.\n// It handles outbound traffic monitoring and statistics.\ntype OutboundService struct{}\n\n// testSemaphore limits concurrent outbound tests to prevent resource exhaustion.\nvar testSemaphore sync.Mutex\n\nfunc (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {\n\tvar err error\n\tdb := database.GetDB()\n\ttx := db.Begin()\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ttx.Rollback()\n\t\t} else {\n\t\t\ttx.Commit()\n\t\t}\n\t}()\n\n\terr = s.addOutboundTraffic(tx, traffics)\n\tif err != nil {\n\t\treturn err, false\n\t}\n\n\treturn nil, false\n}\n\nfunc (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {\n\tif len(traffics) == 0 {\n\t\treturn nil\n\t}\n\n\tvar err error\n\n\tfor _, traffic := range traffics {\n\t\tif traffic.IsOutbound {\n\n\t\t\tvar outbound model.OutboundTraffics\n\n\t\t\terr = tx.Model(&model.OutboundTraffics{}).Where(\"tag = ?\", traffic.Tag).\n\t\t\t\tFirstOrCreate(&outbound).Error\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\toutbound.Tag = traffic.Tag\n\t\t\toutbound.Up = outbound.Up + traffic.Up\n\t\t\toutbound.Down = outbound.Down + traffic.Down\n\t\t\toutbound.Total = outbound.Up + outbound.Down\n\n\t\t\terr = tx.Save(&outbound).Error\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) {\n\tdb := database.GetDB()\n\tvar traffics []*model.OutboundTraffics\n\n\terr := db.Model(model.OutboundTraffics{}).Find(&traffics).Error\n\tif err != nil {\n\t\tlogger.Warning(\"Error retrieving OutboundTraffics: \", err)\n\t\treturn nil, err\n\t}\n\n\treturn traffics, nil\n}\n\nfunc (s *OutboundService) ResetOutboundTraffic(tag string) error {\n\tdb := database.GetDB()\n\n\twhereText := \"tag \"\n\tif tag == \"-alltags-\" {\n\t\twhereText += \" <> ?\"\n\t} else {\n\t\twhereText += \" = ?\"\n\t}\n\n\tresult := db.Model(model.OutboundTraffics{}).\n\t\tWhere(whereText, tag).\n\t\tUpdates(map[string]any{\"up\": 0, \"down\": 0, \"total\": 0})\n\n\terr := result.Error\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// TestOutboundResult represents the result of testing an outbound\ntype TestOutboundResult struct {\n\tSuccess    bool   `json:\"success\"`\n\tDelay      int64  `json:\"delay\"` // Delay in milliseconds\n\tError      string `json:\"error,omitempty\"`\n\tStatusCode int    `json:\"statusCode,omitempty\"`\n}\n\n// TestOutbound tests an outbound by creating a temporary xray instance and measuring response time.\n// allOutboundsJSON must be a JSON array of all outbounds; they are copied into the test config unchanged.\n// Only the test inbound and a route rule (to the tested outbound tag) are added.\nfunc (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string) (*TestOutboundResult, error) {\n\tif testURL == \"\" {\n\t\ttestURL = \"https://www.google.com/generate_204\"\n\t}\n\n\t// Limit to one concurrent test at a time\n\tif !testSemaphore.TryLock() {\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   \"Another outbound test is already running, please wait\",\n\t\t}, nil\n\t}\n\tdefer testSemaphore.Unlock()\n\n\t// Parse the outbound being tested to get its tag\n\tvar testOutbound map[string]any\n\tif err := json.Unmarshal([]byte(outboundJSON), &testOutbound); err != nil {\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   fmt.Sprintf(\"Invalid outbound JSON: %v\", err),\n\t\t}, nil\n\t}\n\toutboundTag, _ := testOutbound[\"tag\"].(string)\n\tif outboundTag == \"\" {\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   \"Outbound has no tag\",\n\t\t}, nil\n\t}\n\tif protocol, _ := testOutbound[\"protocol\"].(string); protocol == \"blackhole\" || outboundTag == \"blocked\" {\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   \"Blocked/blackhole outbound cannot be tested\",\n\t\t}, nil\n\t}\n\n\t// Use all outbounds when provided; otherwise fall back to single outbound\n\tvar allOutbounds []any\n\tif allOutboundsJSON != \"\" {\n\t\tif err := json.Unmarshal([]byte(allOutboundsJSON), &allOutbounds); err != nil {\n\t\t\treturn &TestOutboundResult{\n\t\t\t\tSuccess: false,\n\t\t\t\tError:   fmt.Sprintf(\"Invalid allOutbounds JSON: %v\", err),\n\t\t\t}, nil\n\t\t}\n\t}\n\tif len(allOutbounds) == 0 {\n\t\tallOutbounds = []any{testOutbound}\n\t}\n\n\t// Find an available port for test inbound\n\ttestPort, err := findAvailablePort()\n\tif err != nil {\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   fmt.Sprintf(\"Failed to find available port: %v\", err),\n\t\t}, nil\n\t}\n\n\t// Copy all outbounds as-is, add only test inbound and route rule\n\ttestConfig := s.createTestConfig(outboundTag, allOutbounds, testPort)\n\n\t// Use a temporary config file so the main config.json is never overwritten\n\ttestConfigPath, err := createTestConfigPath()\n\tif err != nil {\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   fmt.Sprintf(\"Failed to create test config path: %v\", err),\n\t\t}, nil\n\t}\n\tdefer os.Remove(testConfigPath) // ensure temp file is removed even if process is not stopped\n\n\t// Create temporary xray process with its own config file\n\ttestProcess := xray.NewTestProcess(testConfig, testConfigPath)\n\tdefer func() {\n\t\tif testProcess.IsRunning() {\n\t\t\ttestProcess.Stop()\n\t\t}\n\t}()\n\n\t// Start the test process\n\tif err := testProcess.Start(); err != nil {\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   fmt.Sprintf(\"Failed to start test xray instance: %v\", err),\n\t\t}, nil\n\t}\n\n\t// Wait for xray to start listening on the test port\n\tif err := waitForPort(testPort, 3*time.Second); err != nil {\n\t\tif !testProcess.IsRunning() {\n\t\t\tresult := testProcess.GetResult()\n\t\t\treturn &TestOutboundResult{\n\t\t\t\tSuccess: false,\n\t\t\t\tError:   fmt.Sprintf(\"Xray process exited: %s\", result),\n\t\t\t}, nil\n\t\t}\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   fmt.Sprintf(\"Xray failed to start listening: %v\", err),\n\t\t}, nil\n\t}\n\n\t// Check if process is still running\n\tif !testProcess.IsRunning() {\n\t\tresult := testProcess.GetResult()\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   fmt.Sprintf(\"Xray process exited: %s\", result),\n\t\t}, nil\n\t}\n\n\t// Test the connection through proxy\n\tdelay, statusCode, err := s.testConnection(testPort, testURL)\n\tif err != nil {\n\t\treturn &TestOutboundResult{\n\t\t\tSuccess: false,\n\t\t\tError:   err.Error(),\n\t\t}, nil\n\t}\n\n\treturn &TestOutboundResult{\n\t\tSuccess:    true,\n\t\tDelay:      delay,\n\t\tStatusCode: statusCode,\n\t}, nil\n}\n\n// createTestConfig creates a test config by copying all outbounds unchanged and adding\n// only the test inbound (SOCKS) and a route rule that sends traffic to the given outbound tag.\nfunc (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []any, testPort int) *xray.Config {\n\t// Test inbound (SOCKS proxy) - only addition to inbounds\n\ttestInbound := xray.InboundConfig{\n\t\tTag:      \"test-inbound\",\n\t\tListen:   json_util.RawMessage(`\"127.0.0.1\"`),\n\t\tPort:     testPort,\n\t\tProtocol: \"socks\",\n\t\tSettings: json_util.RawMessage(`{\"auth\":\"noauth\",\"udp\":true}`),\n\t}\n\n\t// Outbounds: copy all, but set noKernelTun=true for WireGuard outbounds\n\tprocessedOutbounds := make([]any, len(allOutbounds))\n\tfor i, ob := range allOutbounds {\n\t\toutbound, ok := ob.(map[string]any)\n\t\tif !ok {\n\t\t\tprocessedOutbounds[i] = ob\n\t\t\tcontinue\n\t\t}\n\t\tif protocol, ok := outbound[\"protocol\"].(string); ok && protocol == \"wireguard\" {\n\t\t\t// Set noKernelTun to true for WireGuard outbounds\n\t\t\tif settings, ok := outbound[\"settings\"].(map[string]any); ok {\n\t\t\t\tsettings[\"noKernelTun\"] = true\n\t\t\t} else {\n\t\t\t\t// Create settings if it doesn't exist\n\t\t\t\toutbound[\"settings\"] = map[string]any{\n\t\t\t\t\t\"noKernelTun\": true,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tprocessedOutbounds[i] = outbound\n\t}\n\toutboundsJSON, _ := json.Marshal(processedOutbounds)\n\n\t// Create routing rule to route all traffic through test outbound\n\troutingRules := []map[string]any{\n\t\t{\n\t\t\t\"type\":        \"field\",\n\t\t\t\"outboundTag\": outboundTag,\n\t\t\t\"network\":     \"tcp,udp\",\n\t\t},\n\t}\n\n\troutingJSON, _ := json.Marshal(map[string]any{\n\t\t\"domainStrategy\": \"AsIs\",\n\t\t\"rules\":          routingRules,\n\t})\n\n\t// Disable logging for test process to avoid creating orphaned log files\n\tlogConfig := map[string]any{\n\t\t\"loglevel\": \"warning\",\n\t\t\"access\":   \"none\",\n\t\t\"error\":    \"none\",\n\t\t\"dnsLog\":   false,\n\t}\n\tlogJSON, _ := json.Marshal(logConfig)\n\n\t// Create minimal config\n\tcfg := &xray.Config{\n\t\tLogConfig: json_util.RawMessage(logJSON),\n\t\tInboundConfigs: []xray.InboundConfig{\n\t\t\ttestInbound,\n\t\t},\n\t\tOutboundConfigs: json_util.RawMessage(string(outboundsJSON)),\n\t\tRouterConfig:    json_util.RawMessage(string(routingJSON)),\n\t\tPolicy:          json_util.RawMessage(`{}`),\n\t\tStats:           json_util.RawMessage(`{}`),\n\t}\n\n\treturn cfg\n}\n\n// testConnection tests the connection through the proxy and measures delay.\n// It performs a warmup request first to establish the SOCKS connection and populate DNS caches,\n// then measures the second request for a more accurate latency reading.\nfunc (s *OutboundService) testConnection(proxyPort int, testURL string) (int64, int, error) {\n\t// Create SOCKS5 proxy URL\n\tproxyURL := fmt.Sprintf(\"socks5://127.0.0.1:%d\", proxyPort)\n\n\t// Parse proxy URL\n\tproxyURLParsed, err := url.Parse(proxyURL)\n\tif err != nil {\n\t\treturn 0, 0, common.NewErrorf(\"Invalid proxy URL: %v\", err)\n\t}\n\n\t// Create HTTP client with proxy and keep-alive for connection reuse\n\tclient := &http.Client{\n\t\tTimeout: 10 * time.Second,\n\t\tTransport: &http.Transport{\n\t\t\tProxy: http.ProxyURL(proxyURLParsed),\n\t\t\tDialContext: (&net.Dialer{\n\t\t\t\tTimeout:   5 * time.Second,\n\t\t\t\tKeepAlive: 30 * time.Second,\n\t\t\t}).DialContext,\n\t\t\tMaxIdleConns:       1,\n\t\t\tIdleConnTimeout:    10 * time.Second,\n\t\t\tDisableCompression: true,\n\t\t},\n\t}\n\n\t// Warmup request: establishes SOCKS/TLS connection, DNS, and TCP to the target.\n\t// This mirrors real-world usage where connections are reused.\n\twarmupResp, err := client.Get(testURL)\n\tif err != nil {\n\t\treturn 0, 0, common.NewErrorf(\"Request failed: %v\", err)\n\t}\n\tio.Copy(io.Discard, warmupResp.Body)\n\twarmupResp.Body.Close()\n\n\t// Measure the actual request on the warm connection\n\tstartTime := time.Now()\n\tresp, err := client.Get(testURL)\n\tdelay := time.Since(startTime).Milliseconds()\n\n\tif err != nil {\n\t\treturn 0, 0, common.NewErrorf(\"Request failed: %v\", err)\n\t}\n\tio.Copy(io.Discard, resp.Body)\n\tresp.Body.Close()\n\n\treturn delay, resp.StatusCode, nil\n}\n\n// waitForPort polls until the given TCP port is accepting connections or the timeout expires.\nfunc waitForPort(port int, timeout time.Duration) error {\n\tdeadline := time.Now().Add(timeout)\n\tfor time.Now().Before(deadline) {\n\t\tconn, err := net.DialTimeout(\"tcp\", fmt.Sprintf(\"127.0.0.1:%d\", port), 100*time.Millisecond)\n\t\tif err == nil {\n\t\t\tconn.Close()\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(50 * time.Millisecond)\n\t}\n\treturn fmt.Errorf(\"port %d not ready after %v\", port, timeout)\n}\n\n// findAvailablePort finds an available port for testing\nfunc findAvailablePort() (int, error) {\n\tlistener, err := net.Listen(\"tcp\", \":0\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer listener.Close()\n\n\taddr := listener.Addr().(*net.TCPAddr)\n\treturn addr.Port, nil\n}\n\n// createTestConfigPath returns a unique path for a temporary xray config file in the bin folder.\n// The temp file is created and closed so the path is reserved; Start() will overwrite it.\nfunc createTestConfigPath() (string, error) {\n\ttmpFile, err := os.CreateTemp(config.GetBinFolderPath(), \"xray_test_*.json\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tpath := tmpFile.Name()\n\tif err := tmpFile.Close(); err != nil {\n\t\tos.Remove(path)\n\t\treturn \"\", err\n\t}\n\treturn path, nil\n}\n"
  },
  {
    "path": "web/service/panel.go",
    "content": "package service\n\nimport (\n\t\"os\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n)\n\n// PanelService provides business logic for panel management operations.\n// It handles panel restart, updates, and system-level panel controls.\ntype PanelService struct{}\n\nfunc (s *PanelService) RestartPanel(delay time.Duration) error {\n\tp, err := os.FindProcess(syscall.Getpid())\n\tif err != nil {\n\t\treturn err\n\t}\n\tgo func() {\n\t\ttime.Sleep(delay)\n\t\terr := p.Signal(syscall.SIGHUP)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"failed to send SIGHUP signal:\", err)\n\t\t}\n\t}()\n\treturn nil\n}\n"
  },
  {
    "path": "web/service/server.go",
    "content": "package service\n\nimport (\n\t\"archive/zip\"\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/sys\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shirou/gopsutil/v4/cpu\"\n\t\"github.com/shirou/gopsutil/v4/disk\"\n\t\"github.com/shirou/gopsutil/v4/host\"\n\t\"github.com/shirou/gopsutil/v4/load\"\n\t\"github.com/shirou/gopsutil/v4/mem\"\n\t\"github.com/shirou/gopsutil/v4/net\"\n)\n\n// ProcessState represents the current state of a system process.\ntype ProcessState string\n\n// Process state constants\nconst (\n\tRunning ProcessState = \"running\" // Process is running normally\n\tStop    ProcessState = \"stop\"    // Process is stopped\n\tError   ProcessState = \"error\"   // Process is in error state\n)\n\n// Status represents comprehensive system and application status information.\n// It includes CPU, memory, disk, network statistics, and Xray process status.\ntype Status struct {\n\tT           time.Time `json:\"-\"`\n\tCpu         float64   `json:\"cpu\"`\n\tCpuCores    int       `json:\"cpuCores\"`\n\tLogicalPro  int       `json:\"logicalPro\"`\n\tCpuSpeedMhz float64   `json:\"cpuSpeedMhz\"`\n\tMem         struct {\n\t\tCurrent uint64 `json:\"current\"`\n\t\tTotal   uint64 `json:\"total\"`\n\t} `json:\"mem\"`\n\tSwap struct {\n\t\tCurrent uint64 `json:\"current\"`\n\t\tTotal   uint64 `json:\"total\"`\n\t} `json:\"swap\"`\n\tDisk struct {\n\t\tCurrent uint64 `json:\"current\"`\n\t\tTotal   uint64 `json:\"total\"`\n\t} `json:\"disk\"`\n\tXray struct {\n\t\tState    ProcessState `json:\"state\"`\n\t\tErrorMsg string       `json:\"errorMsg\"`\n\t\tVersion  string       `json:\"version\"`\n\t} `json:\"xray\"`\n\tUptime   uint64    `json:\"uptime\"`\n\tLoads    []float64 `json:\"loads\"`\n\tTcpCount int       `json:\"tcpCount\"`\n\tUdpCount int       `json:\"udpCount\"`\n\tNetIO    struct {\n\t\tUp   uint64 `json:\"up\"`\n\t\tDown uint64 `json:\"down\"`\n\t} `json:\"netIO\"`\n\tNetTraffic struct {\n\t\tSent uint64 `json:\"sent\"`\n\t\tRecv uint64 `json:\"recv\"`\n\t} `json:\"netTraffic\"`\n\tPublicIP struct {\n\t\tIPv4 string `json:\"ipv4\"`\n\t\tIPv6 string `json:\"ipv6\"`\n\t} `json:\"publicIP\"`\n\tAppStats struct {\n\t\tThreads uint32 `json:\"threads\"`\n\t\tMem     uint64 `json:\"mem\"`\n\t\tUptime  uint64 `json:\"uptime\"`\n\t} `json:\"appStats\"`\n}\n\n// Release represents information about a software release from GitHub.\ntype Release struct {\n\tTagName string `json:\"tag_name\"` // The tag name of the release\n}\n\n// ServerService provides business logic for server monitoring and management.\n// It handles system status collection, IP detection, and application statistics.\ntype ServerService struct {\n\txrayService        XrayService\n\tinboundService     InboundService\n\tcachedIPv4         string\n\tcachedIPv6         string\n\tnoIPv6             bool\n\tmu                 sync.Mutex\n\tlastCPUTimes       cpu.TimesStat\n\thasLastCPUSample   bool\n\thasNativeCPUSample bool\n\temaCPU             float64\n\tcpuHistory         []CPUSample\n\tcachedCpuSpeedMhz  float64\n\tlastCpuInfoAttempt time.Time\n}\n\n// AggregateCpuHistory returns up to maxPoints averaged buckets of size bucketSeconds over recent data.\nfunc (s *ServerService) AggregateCpuHistory(bucketSeconds int, maxPoints int) []map[string]any {\n\tif bucketSeconds <= 0 || maxPoints <= 0 {\n\t\treturn nil\n\t}\n\tcutoff := time.Now().Add(-time.Duration(bucketSeconds*maxPoints) * time.Second).Unix()\n\ts.mu.Lock()\n\t// find start index (history sorted ascending)\n\thist := s.cpuHistory\n\t// binary-ish scan (simple linear from end since size capped ~10800 is fine)\n\tstartIdx := 0\n\tfor i := len(hist) - 1; i >= 0; i-- {\n\t\tif hist[i].T < cutoff {\n\t\t\tstartIdx = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\tif startIdx >= len(hist) {\n\t\ts.mu.Unlock()\n\t\treturn []map[string]any{}\n\t}\n\tslice := hist[startIdx:]\n\t// copy for unlock\n\ttmp := make([]CPUSample, len(slice))\n\tcopy(tmp, slice)\n\ts.mu.Unlock()\n\tif len(tmp) == 0 {\n\t\treturn []map[string]any{}\n\t}\n\tvar out []map[string]any\n\tvar acc []float64\n\tbSize := int64(bucketSeconds)\n\tcurBucket := (tmp[0].T / bSize) * bSize\n\tflush := func(ts int64) {\n\t\tif len(acc) == 0 {\n\t\t\treturn\n\t\t}\n\t\tsum := 0.0\n\t\tfor _, v := range acc {\n\t\t\tsum += v\n\t\t}\n\t\tavg := sum / float64(len(acc))\n\t\tout = append(out, map[string]any{\"t\": ts, \"cpu\": avg})\n\t\tacc = acc[:0]\n\t}\n\tfor _, p := range tmp {\n\t\tb := (p.T / bSize) * bSize\n\t\tif b != curBucket {\n\t\t\tflush(curBucket)\n\t\t\tcurBucket = b\n\t\t}\n\t\tacc = append(acc, p.Cpu)\n\t}\n\tflush(curBucket)\n\tif len(out) > maxPoints {\n\t\tout = out[len(out)-maxPoints:]\n\t}\n\treturn out\n}\n\n// CPUSample single CPU utilization sample\ntype CPUSample struct {\n\tT   int64   `json:\"t\"`   // unix seconds\n\tCpu float64 `json:\"cpu\"` // percent 0..100\n}\n\ntype LogEntry struct {\n\tDateTime    time.Time\n\tFromAddress string\n\tToAddress   string\n\tInbound     string\n\tOutbound    string\n\tEmail       string\n\tEvent       int\n}\n\nfunc getPublicIP(url string) string {\n\tclient := &http.Client{\n\t\tTimeout: 3 * time.Second,\n\t}\n\n\tresp, err := client.Get(url)\n\tif err != nil {\n\t\treturn \"N/A\"\n\t}\n\tdefer resp.Body.Close()\n\n\t// Don't retry if access is blocked or region-restricted\n\tif resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnavailableForLegalReasons {\n\t\treturn \"N/A\"\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"N/A\"\n\t}\n\n\tip, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"N/A\"\n\t}\n\n\tipString := strings.TrimSpace(string(ip))\n\tif ipString == \"\" {\n\t\treturn \"N/A\"\n\t}\n\n\treturn ipString\n}\n\nfunc (s *ServerService) GetStatus(lastStatus *Status) *Status {\n\tnow := time.Now()\n\tstatus := &Status{\n\t\tT: now,\n\t}\n\n\t// CPU stats\n\tutil, err := s.sampleCPUUtilization()\n\tif err != nil {\n\t\tlogger.Warning(\"get cpu percent failed:\", err)\n\t} else {\n\t\tstatus.Cpu = util\n\t}\n\n\tstatus.CpuCores, err = cpu.Counts(false)\n\tif err != nil {\n\t\tlogger.Warning(\"get cpu cores count failed:\", err)\n\t}\n\n\tstatus.LogicalPro = runtime.NumCPU()\n\n\tif status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute {\n\t\ts.lastCpuInfoAttempt = time.Now()\n\t\tdone := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer close(done)\n\t\t\tcpuInfos, err := cpu.Info()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warning(\"get cpu info failed:\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif len(cpuInfos) > 0 {\n\t\t\t\ts.cachedCpuSpeedMhz = cpuInfos[0].Mhz\n\t\t\t\tstatus.CpuSpeedMhz = s.cachedCpuSpeedMhz\n\t\t\t} else {\n\t\t\t\tlogger.Warning(\"could not find cpu info\")\n\t\t\t}\n\t\t}()\n\t\tselect {\n\t\tcase <-done:\n\t\tcase <-time.After(1500 * time.Millisecond):\n\t\t\tlogger.Warning(\"cpu info query timed out; will retry later\")\n\t\t}\n\t} else if s.cachedCpuSpeedMhz != 0 {\n\t\tstatus.CpuSpeedMhz = s.cachedCpuSpeedMhz\n\t}\n\n\t// Uptime\n\tupTime, err := host.Uptime()\n\tif err != nil {\n\t\tlogger.Warning(\"get uptime failed:\", err)\n\t} else {\n\t\tstatus.Uptime = upTime\n\t}\n\n\t// Memory stats\n\tmemInfo, err := mem.VirtualMemory()\n\tif err != nil {\n\t\tlogger.Warning(\"get virtual memory failed:\", err)\n\t} else {\n\t\tstatus.Mem.Current = memInfo.Used\n\t\tstatus.Mem.Total = memInfo.Total\n\t}\n\n\tswapInfo, err := mem.SwapMemory()\n\tif err != nil {\n\t\tlogger.Warning(\"get swap memory failed:\", err)\n\t} else {\n\t\tstatus.Swap.Current = swapInfo.Used\n\t\tstatus.Swap.Total = swapInfo.Total\n\t}\n\n\t// Disk stats\n\tdiskInfo, err := disk.Usage(\"/\")\n\tif err != nil {\n\t\tlogger.Warning(\"get disk usage failed:\", err)\n\t} else {\n\t\tstatus.Disk.Current = diskInfo.Used\n\t\tstatus.Disk.Total = diskInfo.Total\n\t}\n\n\t// Load averages\n\tavgState, err := load.Avg()\n\tif err != nil {\n\t\tlogger.Warning(\"get load avg failed:\", err)\n\t} else {\n\t\tstatus.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15}\n\t}\n\n\t// Network stats\n\tioStats, err := net.IOCounters(false)\n\tif err != nil {\n\t\tlogger.Warning(\"get io counters failed:\", err)\n\t} else if len(ioStats) > 0 {\n\t\tioStat := ioStats[0]\n\t\tstatus.NetTraffic.Sent = ioStat.BytesSent\n\t\tstatus.NetTraffic.Recv = ioStat.BytesRecv\n\n\t\tif lastStatus != nil {\n\t\t\tduration := now.Sub(lastStatus.T)\n\t\t\tseconds := float64(duration) / float64(time.Second)\n\t\t\tup := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds)\n\t\t\tdown := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds)\n\t\t\tstatus.NetIO.Up = up\n\t\t\tstatus.NetIO.Down = down\n\t\t}\n\t} else {\n\t\tlogger.Warning(\"can not find io counters\")\n\t}\n\n\t// TCP/UDP connections\n\tstatus.TcpCount, err = sys.GetTCPCount()\n\tif err != nil {\n\t\tlogger.Warning(\"get tcp connections failed:\", err)\n\t}\n\n\tstatus.UdpCount, err = sys.GetUDPCount()\n\tif err != nil {\n\t\tlogger.Warning(\"get udp connections failed:\", err)\n\t}\n\n\t// IP fetching with caching\n\tshowIp4ServiceLists := []string{\n\t\t\"https://api4.ipify.org\",\n\t\t\"https://ipv4.icanhazip.com\",\n\t\t\"https://v4.api.ipinfo.io/ip\",\n\t\t\"https://ipv4.myexternalip.com/raw\",\n\t\t\"https://4.ident.me\",\n\t\t\"https://check-host.net/ip\",\n\t}\n\tshowIp6ServiceLists := []string{\n\t\t\"https://api6.ipify.org\",\n\t\t\"https://ipv6.icanhazip.com\",\n\t\t\"https://v6.api.ipinfo.io/ip\",\n\t\t\"https://ipv6.myexternalip.com/raw\",\n\t\t\"https://6.ident.me\",\n\t}\n\n\tif s.cachedIPv4 == \"\" {\n\t\tfor _, ip4Service := range showIp4ServiceLists {\n\t\t\ts.cachedIPv4 = getPublicIP(ip4Service)\n\t\t\tif s.cachedIPv4 != \"N/A\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif s.cachedIPv6 == \"\" && !s.noIPv6 {\n\t\tfor _, ip6Service := range showIp6ServiceLists {\n\t\t\ts.cachedIPv6 = getPublicIP(ip6Service)\n\t\t\tif s.cachedIPv6 != \"N/A\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif s.cachedIPv6 == \"N/A\" {\n\t\ts.noIPv6 = true\n\t}\n\n\tstatus.PublicIP.IPv4 = s.cachedIPv4\n\tstatus.PublicIP.IPv6 = s.cachedIPv6\n\n\t// Xray status\n\tif s.xrayService.IsXrayRunning() {\n\t\tstatus.Xray.State = Running\n\t\tstatus.Xray.ErrorMsg = \"\"\n\t} else {\n\t\terr := s.xrayService.GetXrayErr()\n\t\tif err != nil {\n\t\t\tstatus.Xray.State = Error\n\t\t} else {\n\t\t\tstatus.Xray.State = Stop\n\t\t}\n\t\tstatus.Xray.ErrorMsg = s.xrayService.GetXrayResult()\n\t}\n\tstatus.Xray.Version = s.xrayService.GetXrayVersion()\n\n\t// Application stats\n\tvar rtm runtime.MemStats\n\truntime.ReadMemStats(&rtm)\n\tstatus.AppStats.Mem = rtm.Sys\n\tstatus.AppStats.Threads = uint32(runtime.NumGoroutine())\n\tif p != nil && p.IsRunning() {\n\t\tstatus.AppStats.Uptime = p.GetUptime()\n\t} else {\n\t\tstatus.AppStats.Uptime = 0\n\t}\n\n\treturn status\n}\n\nfunc (s *ServerService) AppendCpuSample(t time.Time, v float64) {\n\tconst capacity = 9000 // ~5 hours @ 2s interval\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tp := CPUSample{T: t.Unix(), Cpu: v}\n\tif n := len(s.cpuHistory); n > 0 && s.cpuHistory[n-1].T == p.T {\n\t\ts.cpuHistory[n-1] = p\n\t} else {\n\t\ts.cpuHistory = append(s.cpuHistory, p)\n\t}\n\tif len(s.cpuHistory) > capacity {\n\t\ts.cpuHistory = s.cpuHistory[len(s.cpuHistory)-capacity:]\n\t}\n}\n\nfunc (s *ServerService) sampleCPUUtilization() (float64, error) {\n\t// Try native platform-specific CPU implementation first (Windows, Linux, macOS)\n\tif pct, err := sys.CPUPercentRaw(); err == nil {\n\t\ts.mu.Lock()\n\t\t// First call to native method returns 0 (initializes baseline)\n\t\tif !s.hasNativeCPUSample {\n\t\t\ts.hasNativeCPUSample = true\n\t\t\ts.mu.Unlock()\n\t\t\treturn 0, nil\n\t\t}\n\t\t// Smooth with EMA\n\t\tconst alpha = 0.3\n\t\tif s.emaCPU == 0 {\n\t\t\ts.emaCPU = pct\n\t\t} else {\n\t\t\ts.emaCPU = alpha*pct + (1-alpha)*s.emaCPU\n\t\t}\n\t\tval := s.emaCPU\n\t\ts.mu.Unlock()\n\t\treturn val, nil\n\t}\n\t// If native call fails, fall back to gopsutil times\n\t// Read aggregate CPU times (all CPUs combined)\n\ttimes, err := cpu.Times(false)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif len(times) == 0 {\n\t\treturn 0, fmt.Errorf(\"no cpu times available\")\n\t}\n\n\tcur := times[0]\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\t// If this is the first sample, initialize and return current EMA (0 by default)\n\tif !s.hasLastCPUSample {\n\t\ts.lastCPUTimes = cur\n\t\ts.hasLastCPUSample = true\n\t\treturn s.emaCPU, nil\n\t}\n\n\t// Compute busy and total deltas\n\t// Note: Guest and GuestNice times are already included in User and Nice respectively,\n\t// so we exclude them to avoid double-counting (Linux kernel accounting)\n\tidleDelta := cur.Idle - s.lastCPUTimes.Idle\n\tbusyDelta := (cur.User - s.lastCPUTimes.User) +\n\t\t(cur.System - s.lastCPUTimes.System) +\n\t\t(cur.Nice - s.lastCPUTimes.Nice) +\n\t\t(cur.Iowait - s.lastCPUTimes.Iowait) +\n\t\t(cur.Irq - s.lastCPUTimes.Irq) +\n\t\t(cur.Softirq - s.lastCPUTimes.Softirq) +\n\t\t(cur.Steal - s.lastCPUTimes.Steal)\n\n\ttotalDelta := busyDelta + idleDelta\n\n\t// Update last sample for next time\n\ts.lastCPUTimes = cur\n\n\t// Guard against division by zero or negative deltas (e.g., counter resets)\n\tif totalDelta <= 0 {\n\t\treturn s.emaCPU, nil\n\t}\n\n\traw := 100.0 * (busyDelta / totalDelta)\n\tif raw < 0 {\n\t\traw = 0\n\t}\n\tif raw > 100 {\n\t\traw = 100\n\t}\n\n\t// Exponential moving average to smooth spikes\n\tconst alpha = 0.3 // smoothing factor (0<alpha<=1). Higher = more responsive, lower = smoother\n\tif s.emaCPU == 0 {\n\t\t// Initialize EMA with the first real reading to avoid long warm-up from zero\n\t\ts.emaCPU = raw\n\t} else {\n\t\ts.emaCPU = alpha*raw + (1-alpha)*s.emaCPU\n\t}\n\n\treturn s.emaCPU, nil\n}\n\nfunc (s *ServerService) GetXrayVersions() ([]string, error) {\n\tconst (\n\t\tXrayURL    = \"https://api.github.com/repos/XTLS/Xray-core/releases\"\n\t\tbufferSize = 8192\n\t)\n\n\tresp, err := http.Get(XrayURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\t// Check HTTP status code - GitHub API returns object instead of array on error\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\tvar errorResponse struct {\n\t\t\tMessage string `json:\"message\"`\n\t\t}\n\t\tif json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != \"\" {\n\t\t\treturn nil, fmt.Errorf(\"GitHub API error: %s\", errorResponse.Message)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"GitHub API returned status %d: %s\", resp.StatusCode, resp.Status)\n\t}\n\n\tbuffer := bytes.NewBuffer(make([]byte, bufferSize))\n\tbuffer.Reset()\n\tif _, err := buffer.ReadFrom(resp.Body); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar releases []Release\n\tif err := json.Unmarshal(buffer.Bytes(), &releases); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar versions []string\n\tfor _, release := range releases {\n\t\ttagVersion := strings.TrimPrefix(release.TagName, \"v\")\n\t\ttagParts := strings.Split(tagVersion, \".\")\n\t\tif len(tagParts) != 3 {\n\t\t\tcontinue\n\t\t}\n\n\t\tmajor, err1 := strconv.Atoi(tagParts[0])\n\t\tminor, err2 := strconv.Atoi(tagParts[1])\n\t\tpatch, err3 := strconv.Atoi(tagParts[2])\n\t\tif err1 != nil || err2 != nil || err3 != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif major > 26 || (major == 26 && minor > 2) || (major == 26 && minor == 2 && patch >= 6) {\n\t\t\tversions = append(versions, release.TagName)\n\t\t}\n\t}\n\treturn versions, nil\n}\n\nfunc (s *ServerService) StopXrayService() error {\n\terr := s.xrayService.StopXray()\n\tif err != nil {\n\t\tlogger.Error(\"stop xray failed:\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *ServerService) RestartXrayService() error {\n\terr := s.xrayService.RestartXray(true)\n\tif err != nil {\n\t\tlogger.Error(\"start xray failed:\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *ServerService) downloadXRay(version string) (string, error) {\n\tosName := runtime.GOOS\n\tarch := runtime.GOARCH\n\n\tswitch osName {\n\tcase \"darwin\":\n\t\tosName = \"macos\"\n\tcase \"windows\":\n\t\tosName = \"windows\"\n\t}\n\n\tswitch arch {\n\tcase \"amd64\":\n\t\tarch = \"64\"\n\tcase \"arm64\":\n\t\tarch = \"arm64-v8a\"\n\tcase \"armv7\":\n\t\tarch = \"arm32-v7a\"\n\tcase \"armv6\":\n\t\tarch = \"arm32-v6\"\n\tcase \"armv5\":\n\t\tarch = \"arm32-v5\"\n\tcase \"386\":\n\t\tarch = \"32\"\n\tcase \"s390x\":\n\t\tarch = \"s390x\"\n\t}\n\n\tfileName := fmt.Sprintf(\"Xray-%s-%s.zip\", osName, arch)\n\turl := fmt.Sprintf(\"https://github.com/XTLS/Xray-core/releases/download/%s/%s\", version, fileName)\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tos.Remove(fileName)\n\tfile, err := os.Create(fileName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer file.Close()\n\n\t_, err = io.Copy(file, resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn fileName, nil\n}\n\nfunc (s *ServerService) UpdateXray(version string) error {\n\t// 1. Stop xray before doing anything\n\tif err := s.StopXrayService(); err != nil {\n\t\tlogger.Warning(\"failed to stop xray before update:\", err)\n\t}\n\n\t// 2. Download the zip\n\tzipFileName, err := s.downloadXRay(version)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(zipFileName)\n\n\tzipFile, err := os.Open(zipFileName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer zipFile.Close()\n\n\tstat, err := zipFile.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\treader, err := zip.NewReader(zipFile, stat.Size())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 3. Helper to extract files\n\tcopyZipFile := func(zipName string, fileName string) error {\n\t\tzipFile, err := reader.Open(zipName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer zipFile.Close()\n\t\tos.MkdirAll(filepath.Dir(fileName), 0755)\n\t\tos.Remove(fileName)\n\t\tfile, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer file.Close()\n\t\t_, err = io.Copy(file, zipFile)\n\t\treturn err\n\t}\n\n\t// 4. Extract correct binary\n\tif runtime.GOOS == \"windows\" {\n\t\ttargetBinary := filepath.Join(\"bin\", \"xray-windows-amd64.exe\")\n\t\terr = copyZipFile(\"xray.exe\", targetBinary)\n\t} else {\n\t\terr = copyZipFile(\"xray\", xray.GetBinaryPath())\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 5. Restart xray\n\tif err := s.xrayService.RestartXray(true); err != nil {\n\t\tlogger.Error(\"start xray failed:\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *ServerService) GetLogs(count string, level string, syslog string) []string {\n\tc, _ := strconv.Atoi(count)\n\tvar lines []string\n\n\tif syslog == \"true\" {\n\t\t// Check if running on Windows - journalctl is not available\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\treturn []string{\"Syslog is not supported on Windows. Please use application logs instead by unchecking the 'Syslog' option.\"}\n\t\t}\n\n\t\t// Validate and sanitize count parameter\n\t\tcountInt, err := strconv.Atoi(count)\n\t\tif err != nil || countInt < 1 || countInt > 10000 {\n\t\t\treturn []string{\"Invalid count parameter - must be a number between 1 and 10000\"}\n\t\t}\n\n\t\t// Validate level parameter - only allow valid syslog levels\n\t\tvalidLevels := map[string]bool{\n\t\t\t\"0\": true, \"emerg\": true,\n\t\t\t\"1\": true, \"alert\": true,\n\t\t\t\"2\": true, \"crit\": true,\n\t\t\t\"3\": true, \"err\": true,\n\t\t\t\"4\": true, \"warning\": true,\n\t\t\t\"5\": true, \"notice\": true,\n\t\t\t\"6\": true, \"info\": true,\n\t\t\t\"7\": true, \"debug\": true,\n\t\t}\n\t\tif !validLevels[level] {\n\t\t\treturn []string{\"Invalid level parameter - must be a valid syslog level\"}\n\t\t}\n\n\t\t// Use hardcoded command with validated parameters\n\t\tcmd := exec.Command(\"journalctl\", \"-u\", \"x-ui\", \"--no-pager\", \"-n\", strconv.Itoa(countInt), \"-p\", level)\n\t\tvar out bytes.Buffer\n\t\tcmd.Stdout = &out\n\t\terr = cmd.Run()\n\t\tif err != nil {\n\t\t\treturn []string{\"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered.\"}\n\t\t}\n\t\tlines = strings.Split(out.String(), \"\\n\")\n\t} else {\n\t\tlines = logger.GetLogs(c, level)\n\t}\n\n\treturn lines\n}\n\nfunc (s *ServerService) GetXrayLogs(\n\tcount string,\n\tfilter string,\n\tshowDirect string,\n\tshowBlocked string,\n\tshowProxy string,\n\tfreedoms []string,\n\tblackholes []string) []LogEntry {\n\n\tconst (\n\t\tDirect = iota\n\t\tBlocked\n\t\tProxied\n\t)\n\n\tcountInt, _ := strconv.Atoi(count)\n\tvar entries []LogEntry\n\n\tpathToAccessLog, err := xray.GetAccessLogPath()\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tfile, err := os.Open(pathToAccessLog)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tdefer file.Close()\n\n\tscanner := bufio.NewScanner(file)\n\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\n\t\tif line == \"\" || strings.Contains(line, \"api -> api\") {\n\t\t\t//skipping empty lines and api calls\n\t\t\tcontinue\n\t\t}\n\n\t\tif filter != \"\" && !strings.Contains(line, filter) {\n\t\t\t//applying filter if it's not empty\n\t\t\tcontinue\n\t\t}\n\n\t\tvar entry LogEntry\n\t\tparts := strings.Fields(line)\n\n\t\tfor i, part := range parts {\n\n\t\t\tif i == 0 {\n\t\t\t\tdateTime, err := time.ParseInLocation(\"2006/01/02 15:04:05.999999\", parts[0]+\" \"+parts[1], time.Local)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tentry.DateTime = dateTime.UTC()\n\t\t\t}\n\n\t\t\tif part == \"from\" {\n\t\t\t\tentry.FromAddress = strings.TrimLeft(parts[i+1], \"/\")\n\t\t\t} else if part == \"accepted\" {\n\t\t\t\tentry.ToAddress = strings.TrimLeft(parts[i+1], \"/\")\n\t\t\t} else if strings.HasPrefix(part, \"[\") {\n\t\t\t\tentry.Inbound = part[1:]\n\t\t\t} else if strings.HasSuffix(part, \"]\") {\n\t\t\t\tentry.Outbound = part[:len(part)-1]\n\t\t\t} else if part == \"email:\" {\n\t\t\t\tentry.Email = parts[i+1]\n\t\t\t}\n\t\t}\n\n\t\tif logEntryContains(line, freedoms) {\n\t\t\tif showDirect == \"false\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tentry.Event = Direct\n\t\t} else if logEntryContains(line, blackholes) {\n\t\t\tif showBlocked == \"false\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tentry.Event = Blocked\n\t\t} else {\n\t\t\tif showProxy == \"false\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tentry.Event = Proxied\n\t\t}\n\n\t\tentries = append(entries, entry)\n\t}\n\n\tif len(entries) > countInt {\n\t\tentries = entries[len(entries)-countInt:]\n\t}\n\n\treturn entries\n}\n\nfunc logEntryContains(line string, suffixes []string) bool {\n\tfor _, sfx := range suffixes {\n\t\tif strings.Contains(line, sfx+\"]\") {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (s *ServerService) GetConfigJson() (any, error) {\n\tconfig, err := s.xrayService.GetXrayConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcontents, err := json.MarshalIndent(config, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar jsonData any\n\terr = json.Unmarshal(contents, &jsonData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn jsonData, nil\n}\n\nfunc (s *ServerService) GetDb() ([]byte, error) {\n\t// Update by manually trigger a checkpoint operation\n\terr := database.Checkpoint()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Open the file for reading\n\tfile, err := os.Open(config.GetDBPath())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer file.Close()\n\n\t// Read the file contents\n\tfileContents, err := io.ReadAll(file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn fileContents, nil\n}\n\nfunc (s *ServerService) ImportDB(file multipart.File) error {\n\t// Check if the file is a SQLite database\n\tisValidDb, err := database.IsSQLiteDB(file)\n\tif err != nil {\n\t\treturn common.NewErrorf(\"Error checking db file format: %v\", err)\n\t}\n\tif !isValidDb {\n\t\treturn common.NewError(\"Invalid db file format\")\n\t}\n\n\t// Reset the file reader to the beginning\n\t_, err = file.Seek(0, 0)\n\tif err != nil {\n\t\treturn common.NewErrorf(\"Error resetting file reader: %v\", err)\n\t}\n\n\t// Save the file as a temporary file\n\ttempPath := fmt.Sprintf(\"%s.temp\", config.GetDBPath())\n\n\t// Remove the existing temporary file (if any)\n\tif _, err := os.Stat(tempPath); err == nil {\n\t\tif errRemove := os.Remove(tempPath); errRemove != nil {\n\t\t\treturn common.NewErrorf(\"Error removing existing temporary db file: %v\", errRemove)\n\t\t}\n\t}\n\n\t// Create the temporary file\n\ttempFile, err := os.Create(tempPath)\n\tif err != nil {\n\t\treturn common.NewErrorf(\"Error creating temporary db file: %v\", err)\n\t}\n\n\t// Robust deferred cleanup for the temporary file\n\tdefer func() {\n\t\tif tempFile != nil {\n\t\t\tif cerr := tempFile.Close(); cerr != nil {\n\t\t\t\tlogger.Warningf(\"Warning: failed to close temp file: %v\", cerr)\n\t\t\t}\n\t\t}\n\t\tif _, err := os.Stat(tempPath); err == nil {\n\t\t\tif rerr := os.Remove(tempPath); rerr != nil {\n\t\t\t\tlogger.Warningf(\"Warning: failed to remove temp file: %v\", rerr)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Save uploaded file to temporary file\n\tif _, err = io.Copy(tempFile, file); err != nil {\n\t\treturn common.NewErrorf(\"Error saving db: %v\", err)\n\t}\n\n\t// Close temp file before opening via sqlite\n\tif err = tempFile.Close(); err != nil {\n\t\treturn common.NewErrorf(\"Error closing temporary db file: %v\", err)\n\t}\n\ttempFile = nil\n\n\t// Validate integrity (no migrations / side effects)\n\tif err = database.ValidateSQLiteDB(tempPath); err != nil {\n\t\treturn common.NewErrorf(\"Invalid or corrupt db file: %v\", err)\n\t}\n\n\t// Stop Xray (ignore error but log)\n\tif errStop := s.StopXrayService(); errStop != nil {\n\t\tlogger.Warningf(\"Failed to stop Xray before DB import: %v\", errStop)\n\t}\n\n\t// Close existing DB to release file locks (especially on Windows)\n\tif errClose := database.CloseDB(); errClose != nil {\n\t\tlogger.Warningf(\"Failed to close existing DB before replacement: %v\", errClose)\n\t}\n\n\t// Backup the current database for fallback\n\tfallbackPath := fmt.Sprintf(\"%s.backup\", config.GetDBPath())\n\n\t// Remove the existing fallback file (if any)\n\tif _, err := os.Stat(fallbackPath); err == nil {\n\t\tif errRemove := os.Remove(fallbackPath); errRemove != nil {\n\t\t\treturn common.NewErrorf(\"Error removing existing fallback db file: %v\", errRemove)\n\t\t}\n\t}\n\n\t// Move the current database to the fallback location\n\tif err = os.Rename(config.GetDBPath(), fallbackPath); err != nil {\n\t\treturn common.NewErrorf(\"Error backing up current db file: %v\", err)\n\t}\n\n\t// Defer fallback cleanup ONLY if everything goes well\n\tdefer func() {\n\t\tif _, err := os.Stat(fallbackPath); err == nil {\n\t\t\tif rerr := os.Remove(fallbackPath); rerr != nil {\n\t\t\t\tlogger.Warningf(\"Warning: failed to remove fallback file: %v\", rerr)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Move temp to DB path\n\tif err = os.Rename(tempPath, config.GetDBPath()); err != nil {\n\t\t// Restore from fallback\n\t\tif errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {\n\t\t\treturn common.NewErrorf(\"Error moving db file and restoring fallback: %v\", errRename)\n\t\t}\n\t\treturn common.NewErrorf(\"Error moving db file: %v\", err)\n\t}\n\n\t// Open & migrate new DB\n\tif err = database.InitDB(config.GetDBPath()); err != nil {\n\t\tif errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {\n\t\t\treturn common.NewErrorf(\"Error migrating db and restoring fallback: %v\", errRename)\n\t\t}\n\t\treturn common.NewErrorf(\"Error migrating db: %v\", err)\n\t}\n\n\ts.inboundService.MigrateDB()\n\n\t// Start Xray\n\tif err = s.RestartXrayService(); err != nil {\n\t\treturn common.NewErrorf(\"Imported DB but failed to start Xray: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// IsValidGeofileName validates that the filename is safe for geofile operations.\n// It checks for path traversal attempts and ensures the filename contains only safe characters.\nfunc (s *ServerService) IsValidGeofileName(filename string) bool {\n\tif filename == \"\" {\n\t\treturn false\n\t}\n\n\t// Check for path traversal attempts\n\tif strings.Contains(filename, \"..\") {\n\t\treturn false\n\t}\n\n\t// Check for path separators (both forward and backward slash)\n\tif strings.ContainsAny(filename, `/\\`) {\n\t\treturn false\n\t}\n\n\t// Check for absolute path indicators\n\tif filepath.IsAbs(filename) {\n\t\treturn false\n\t}\n\n\t// Additional security: only allow alphanumeric, dots, underscores, and hyphens\n\t// This is stricter than the general filename regex\n\tvalidGeofilePattern := `^[a-zA-Z0-9._-]+\\.dat$`\n\tmatched, _ := regexp.MatchString(validGeofilePattern, filename)\n\treturn matched\n}\n\nfunc (s *ServerService) UpdateGeofile(fileName string) error {\n\ttype geofileEntry struct {\n\t\tURL      string\n\t\tFileName string\n\t}\n\tgeofileAllowlist := map[string]geofileEntry{\n\t\t\"geoip.dat\":      {\"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat\", \"geoip.dat\"},\n\t\t\"geosite.dat\":    {\"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat\", \"geosite.dat\"},\n\t\t\"geoip_IR.dat\":   {\"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat\", \"geoip_IR.dat\"},\n\t\t\"geosite_IR.dat\": {\"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat\", \"geosite_IR.dat\"},\n\t\t\"geoip_RU.dat\":   {\"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat\", \"geoip_RU.dat\"},\n\t\t\"geosite_RU.dat\": {\"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat\", \"geosite_RU.dat\"},\n\t}\n\n\t// Strict allowlist check to avoid writing uncontrolled files\n\tif fileName != \"\" {\n\t\tif _, ok := geofileAllowlist[fileName]; !ok {\n\t\t\treturn common.NewErrorf(\"Invalid geofile name: %q not in allowlist\", fileName)\n\t\t}\n\t}\n\n\tdownloadFile := func(url, destPath string) error {\n\t\tvar req *http.Request\n\t\treq, err := http.NewRequest(\"GET\", url, nil)\n\t\tif err != nil {\n\t\t\treturn common.NewErrorf(\"Failed to create HTTP request for %s: %v\", url, err)\n\t\t}\n\n\t\tvar localFileModTime time.Time\n\t\tif fileInfo, err := os.Stat(destPath); err == nil {\n\t\t\tlocalFileModTime = fileInfo.ModTime()\n\t\t\tif !localFileModTime.IsZero() {\n\t\t\t\treq.Header.Set(\"If-Modified-Since\", localFileModTime.UTC().Format(http.TimeFormat))\n\t\t\t}\n\t\t}\n\n\t\tclient := &http.Client{}\n\t\tresp, err := client.Do(req)\n\t\tif err != nil {\n\t\t\treturn common.NewErrorf(\"Failed to download Geofile from %s: %v\", url, err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\t// Parse Last-Modified header from server\n\t\tvar serverModTime time.Time\n\t\tserverModTimeStr := resp.Header.Get(\"Last-Modified\")\n\t\tif serverModTimeStr != \"\" {\n\t\t\tparsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warningf(\"Failed to parse Last-Modified header for %s: %v\", url, err)\n\t\t\t} else {\n\t\t\t\tserverModTime = parsedTime\n\t\t\t}\n\t\t}\n\n\t\t// Function to update local file's modification time\n\t\tupdateFileModTime := func() {\n\t\t\tif !serverModTime.IsZero() {\n\t\t\t\tif err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil {\n\t\t\t\t\tlogger.Warningf(\"Failed to update modification time for %s: %v\", destPath, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Handle 304 Not Modified\n\t\tif resp.StatusCode == http.StatusNotModified {\n\t\t\tupdateFileModTime()\n\t\t\treturn nil\n\t\t}\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn common.NewErrorf(\"Failed to download Geofile from %s: received status code %d\", url, resp.StatusCode)\n\t\t}\n\n\t\tfile, err := os.Create(destPath)\n\t\tif err != nil {\n\t\t\treturn common.NewErrorf(\"Failed to create Geofile %s: %v\", destPath, err)\n\t\t}\n\t\tdefer file.Close()\n\n\t\t_, err = io.Copy(file, resp.Body)\n\t\tif err != nil {\n\t\t\treturn common.NewErrorf(\"Failed to save Geofile %s: %v\", destPath, err)\n\t\t}\n\n\t\tupdateFileModTime()\n\t\treturn nil\n\t}\n\n\tvar errorMessages []string\n\n\tif fileName == \"\" {\n\t\t// Download all geofiles\n\t\tfor _, entry := range geofileAllowlist {\n\t\t\tdestPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)\n\t\t\tif err := downloadFile(entry.URL, destPath); err != nil {\n\t\t\t\terrorMessages = append(errorMessages, fmt.Sprintf(\"Error downloading Geofile '%s': %v\", entry.FileName, err))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tentry := geofileAllowlist[fileName]\n\t\tdestPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)\n\t\tif err := downloadFile(entry.URL, destPath); err != nil {\n\t\t\terrorMessages = append(errorMessages, fmt.Sprintf(\"Error downloading Geofile '%s': %v\", entry.FileName, err))\n\t\t}\n\t}\n\n\terr := s.RestartXrayService()\n\tif err != nil {\n\t\terrorMessages = append(errorMessages, fmt.Sprintf(\"Updated Geofile '%s' but Failed to start Xray: %v\", fileName, err))\n\t}\n\n\tif len(errorMessages) > 0 {\n\t\treturn common.NewErrorf(\"%s\", strings.Join(errorMessages, \"\\r\\n\"))\n\t}\n\n\treturn nil\n}\n\nfunc (s *ServerService) GetNewX25519Cert() (any, error) {\n\t// Run the command\n\tcmd := exec.Command(xray.GetBinaryPath(), \"x25519\")\n\tvar out bytes.Buffer\n\tcmd.Stdout = &out\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlines := strings.Split(out.String(), \"\\n\")\n\n\tprivateKeyLine := strings.Split(lines[0], \":\")\n\tpublicKeyLine := strings.Split(lines[1], \":\")\n\n\tprivateKey := strings.TrimSpace(privateKeyLine[1])\n\tpublicKey := strings.TrimSpace(publicKeyLine[1])\n\n\tkeyPair := map[string]any{\n\t\t\"privateKey\": privateKey,\n\t\t\"publicKey\":  publicKey,\n\t}\n\n\treturn keyPair, nil\n}\n\nfunc (s *ServerService) GetNewmldsa65() (any, error) {\n\t// Run the command\n\tcmd := exec.Command(xray.GetBinaryPath(), \"mldsa65\")\n\tvar out bytes.Buffer\n\tcmd.Stdout = &out\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlines := strings.Split(out.String(), \"\\n\")\n\n\tSeedLine := strings.Split(lines[0], \":\")\n\tVerifyLine := strings.Split(lines[1], \":\")\n\n\tseed := strings.TrimSpace(SeedLine[1])\n\tverify := strings.TrimSpace(VerifyLine[1])\n\n\tkeyPair := map[string]any{\n\t\t\"seed\":   seed,\n\t\t\"verify\": verify,\n\t}\n\n\treturn keyPair, nil\n}\n\nfunc (s *ServerService) GetNewEchCert(sni string) (any, error) {\n\t// Run the command\n\tcmd := exec.Command(xray.GetBinaryPath(), \"tls\", \"ech\", \"--serverName\", sni)\n\tvar out bytes.Buffer\n\tcmd.Stdout = &out\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlines := strings.Split(out.String(), \"\\n\")\n\tif len(lines) < 4 {\n\t\treturn nil, common.NewError(\"invalid ech cert\")\n\t}\n\n\tconfigList := lines[1]\n\tserverKeys := lines[3]\n\n\treturn map[string]any{\n\t\t\"echServerKeys\": serverKeys,\n\t\t\"echConfigList\": configList,\n\t}, nil\n}\n\nfunc (s *ServerService) GetNewVlessEnc() (any, error) {\n\tcmd := exec.Command(xray.GetBinaryPath(), \"vlessenc\")\n\tvar out bytes.Buffer\n\tcmd.Stdout = &out\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tlines := strings.Split(out.String(), \"\\n\")\n\tvar auths []map[string]string\n\tvar current map[string]string\n\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif strings.HasPrefix(line, \"Authentication:\") {\n\t\t\tif current != nil {\n\t\t\t\tauths = append(auths, current)\n\t\t\t}\n\t\t\tcurrent = map[string]string{\n\t\t\t\t\"label\": strings.TrimSpace(strings.TrimPrefix(line, \"Authentication:\")),\n\t\t\t}\n\t\t} else if strings.HasPrefix(line, `\"decryption\"`) || strings.HasPrefix(line, `\"encryption\"`) {\n\t\t\tparts := strings.SplitN(line, \":\", 2)\n\t\t\tif len(parts) == 2 && current != nil {\n\t\t\t\tkey := strings.Trim(parts[0], `\" `)\n\t\t\t\tval := strings.Trim(parts[1], `\" `)\n\t\t\t\tcurrent[key] = val\n\t\t\t}\n\t\t}\n\t}\n\n\tif current != nil {\n\t\tauths = append(auths, current)\n\t}\n\n\treturn map[string]any{\n\t\t\"auths\": auths,\n\t}, nil\n}\n\nfunc (s *ServerService) GetNewUUID() (map[string]string, error) {\n\tnewUUID, err := uuid.NewRandom()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to generate UUID: %w\", err)\n\t}\n\n\treturn map[string]string{\n\t\t\"uuid\": newUUID.String(),\n\t}, nil\n}\n\nfunc (s *ServerService) GetNewmlkem768() (any, error) {\n\t// Run the command\n\tcmd := exec.Command(xray.GetBinaryPath(), \"mlkem768\")\n\tvar out bytes.Buffer\n\tcmd.Stdout = &out\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlines := strings.Split(out.String(), \"\\n\")\n\n\tSeedLine := strings.Split(lines[0], \":\")\n\tClientLine := strings.Split(lines[1], \":\")\n\n\tseed := strings.TrimSpace(SeedLine[1])\n\tclient := strings.TrimSpace(ClientLine[1])\n\n\tkeyPair := map[string]any{\n\t\t\"seed\":   seed,\n\t\t\"client\": client,\n\t}\n\n\treturn keyPair, nil\n}\n"
  },
  {
    "path": "web/service/setting.go",
    "content": "package service\n\nimport (\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/random\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/reflect_util\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/entity\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n)\n\n//go:embed config.json\nvar xrayTemplateConfig string\n\nvar defaultValueMap = map[string]string{\n\t\"xrayTemplateConfig\":          xrayTemplateConfig,\n\t\"webListen\":                   \"\",\n\t\"webDomain\":                   \"\",\n\t\"webPort\":                     \"2053\",\n\t\"webCertFile\":                 \"\",\n\t\"webKeyFile\":                  \"\",\n\t\"secret\":                      random.Seq(32),\n\t\"webBasePath\":                 \"/\",\n\t\"sessionMaxAge\":               \"360\",\n\t\"pageSize\":                    \"25\",\n\t\"expireDiff\":                  \"0\",\n\t\"trafficDiff\":                 \"0\",\n\t\"remarkModel\":                 \"-ieo\",\n\t\"timeLocation\":                \"Local\",\n\t\"tgBotEnable\":                 \"false\",\n\t\"tgBotToken\":                  \"\",\n\t\"tgBotProxy\":                  \"\",\n\t\"tgBotAPIServer\":              \"\",\n\t\"tgBotChatId\":                 \"\",\n\t\"tgRunTime\":                   \"@daily\",\n\t\"tgBotBackup\":                 \"false\",\n\t\"tgBotLoginNotify\":            \"true\",\n\t\"tgCpu\":                       \"80\",\n\t\"tgLang\":                      \"en-US\",\n\t\"twoFactorEnable\":             \"false\",\n\t\"twoFactorToken\":              \"\",\n\t\"subEnable\":                   \"true\",\n\t\"subJsonEnable\":               \"false\",\n\t\"subTitle\":                    \"\",\n\t\"subSupportUrl\":               \"\",\n\t\"subProfileUrl\":               \"\",\n\t\"subAnnounce\":                 \"\",\n\t\"subEnableRouting\":            \"true\",\n\t\"subRoutingRules\":             \"\",\n\t\"subListen\":                   \"\",\n\t\"subPort\":                     \"2096\",\n\t\"subPath\":                     \"/sub/\",\n\t\"subDomain\":                   \"\",\n\t\"subCertFile\":                 \"\",\n\t\"subKeyFile\":                  \"\",\n\t\"subUpdates\":                  \"12\",\n\t\"subEncrypt\":                  \"true\",\n\t\"subShowInfo\":                 \"true\",\n\t\"subURI\":                      \"\",\n\t\"subJsonPath\":                 \"/json/\",\n\t\"subJsonURI\":                  \"\",\n\t\"subJsonFragment\":             \"\",\n\t\"subJsonNoises\":               \"\",\n\t\"subJsonMux\":                  \"\",\n\t\"subJsonRules\":                \"\",\n\t\"datepicker\":                  \"gregorian\",\n\t\"warp\":                        \"\",\n\t\"externalTrafficInformEnable\": \"false\",\n\t\"externalTrafficInformURI\":    \"\",\n\t\"xrayOutboundTestUrl\":         \"https://www.google.com/generate_204\",\n\n\t// LDAP defaults\n\t\"ldapEnable\":            \"false\",\n\t\"ldapHost\":              \"\",\n\t\"ldapPort\":              \"389\",\n\t\"ldapUseTLS\":            \"false\",\n\t\"ldapBindDN\":            \"\",\n\t\"ldapPassword\":          \"\",\n\t\"ldapBaseDN\":            \"\",\n\t\"ldapUserFilter\":        \"(objectClass=person)\",\n\t\"ldapUserAttr\":          \"mail\",\n\t\"ldapVlessField\":        \"vless_enabled\",\n\t\"ldapSyncCron\":          \"@every 1m\",\n\t\"ldapFlagField\":         \"\",\n\t\"ldapTruthyValues\":      \"true,1,yes,on\",\n\t\"ldapInvertFlag\":        \"false\",\n\t\"ldapInboundTags\":       \"\",\n\t\"ldapAutoCreate\":        \"false\",\n\t\"ldapAutoDelete\":        \"false\",\n\t\"ldapDefaultTotalGB\":    \"0\",\n\t\"ldapDefaultExpiryDays\": \"0\",\n\t\"ldapDefaultLimitIP\":    \"0\",\n}\n\n// SettingService provides business logic for application settings management.\n// It handles configuration storage, retrieval, and validation for all system settings.\ntype SettingService struct{}\n\nfunc (s *SettingService) GetDefaultJSONConfig() (any, error) {\n\tvar jsonData any\n\terr := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn jsonData, nil\n}\n\nfunc (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {\n\tdb := database.GetDB()\n\tsettings := make([]*model.Setting, 0)\n\terr := db.Model(model.Setting{}).Not(\"key = ?\", \"xrayTemplateConfig\").Find(&settings).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tallSetting := &entity.AllSetting{}\n\tt := reflect.TypeFor[entity.AllSetting]()\n\tv := reflect.ValueOf(allSetting).Elem()\n\tfields := reflect_util.GetFields(t)\n\n\tsetSetting := func(key, value string) (err error) {\n\t\tdefer func() {\n\t\t\tpanicErr := recover()\n\t\t\tif panicErr != nil {\n\t\t\t\terr = errors.New(fmt.Sprint(panicErr))\n\t\t\t}\n\t\t}()\n\n\t\tvar found bool\n\t\tvar field reflect.StructField\n\t\tfor _, f := range fields {\n\t\t\tif f.Tag.Get(\"json\") == key {\n\t\t\t\tfield = f\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\t// Some settings are automatically generated, no need to return to the front end to modify the user\n\t\t\treturn nil\n\t\t}\n\n\t\tfieldV := v.FieldByName(field.Name)\n\t\tswitch t := fieldV.Interface().(type) {\n\t\tcase int:\n\t\t\tn, err := strconv.ParseInt(value, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfieldV.SetInt(n)\n\t\tcase string:\n\t\t\tfieldV.SetString(value)\n\t\tcase bool:\n\t\t\tfieldV.SetBool(value == \"true\")\n\t\tdefault:\n\t\t\treturn common.NewErrorf(\"unknown field %v type %v\", key, t)\n\t\t}\n\t\treturn\n\t}\n\n\tkeyMap := map[string]bool{}\n\tfor _, setting := range settings {\n\t\terr := setSetting(setting.Key, setting.Value)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tkeyMap[setting.Key] = true\n\t}\n\n\tfor key, value := range defaultValueMap {\n\t\tif keyMap[key] {\n\t\t\tcontinue\n\t\t}\n\t\terr := setSetting(key, value)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn allSetting, nil\n}\n\nfunc (s *SettingService) ResetSettings() error {\n\tdb := database.GetDB()\n\terr := db.Where(\"1 = 1\").Delete(model.Setting{}).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn db.Model(model.User{}).\n\t\tWhere(\"1 = 1\").Error\n}\n\nfunc (s *SettingService) getSetting(key string) (*model.Setting, error) {\n\tdb := database.GetDB()\n\tsetting := &model.Setting{}\n\terr := db.Model(model.Setting{}).Where(\"key = ?\", key).First(setting).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn setting, nil\n}\n\nfunc (s *SettingService) saveSetting(key string, value string) error {\n\tsetting, err := s.getSetting(key)\n\tdb := database.GetDB()\n\tif database.IsNotFound(err) {\n\t\treturn db.Create(&model.Setting{\n\t\t\tKey:   key,\n\t\t\tValue: value,\n\t\t}).Error\n\t} else if err != nil {\n\t\treturn err\n\t}\n\tsetting.Key = key\n\tsetting.Value = value\n\treturn db.Save(setting).Error\n}\n\nfunc (s *SettingService) getString(key string) (string, error) {\n\tsetting, err := s.getSetting(key)\n\tif database.IsNotFound(err) {\n\t\tvalue, ok := defaultValueMap[key]\n\t\tif !ok {\n\t\t\treturn \"\", common.NewErrorf(\"key <%v> not in defaultValueMap\", key)\n\t\t}\n\t\treturn value, nil\n\t} else if err != nil {\n\t\treturn \"\", err\n\t}\n\treturn setting.Value, nil\n}\n\nfunc (s *SettingService) setString(key string, value string) error {\n\treturn s.saveSetting(key, value)\n}\n\nfunc (s *SettingService) getBool(key string) (bool, error) {\n\tstr, err := s.getString(key)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn strconv.ParseBool(str)\n}\n\nfunc (s *SettingService) setBool(key string, value bool) error {\n\treturn s.setString(key, strconv.FormatBool(value))\n}\n\nfunc (s *SettingService) getInt(key string) (int, error) {\n\tstr, err := s.getString(key)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn strconv.Atoi(str)\n}\n\nfunc (s *SettingService) setInt(key string, value int) error {\n\treturn s.setString(key, strconv.Itoa(value))\n}\n\nfunc (s *SettingService) GetXrayConfigTemplate() (string, error) {\n\treturn s.getString(\"xrayTemplateConfig\")\n}\n\nfunc (s *SettingService) GetXrayOutboundTestUrl() (string, error) {\n\treturn s.getString(\"xrayOutboundTestUrl\")\n}\n\nfunc (s *SettingService) SetXrayOutboundTestUrl(url string) error {\n\treturn s.setString(\"xrayOutboundTestUrl\", url)\n}\n\nfunc (s *SettingService) GetListen() (string, error) {\n\treturn s.getString(\"webListen\")\n}\n\nfunc (s *SettingService) SetListen(ip string) error {\n\treturn s.setString(\"webListen\", ip)\n}\n\nfunc (s *SettingService) GetWebDomain() (string, error) {\n\treturn s.getString(\"webDomain\")\n}\n\nfunc (s *SettingService) GetTgBotToken() (string, error) {\n\treturn s.getString(\"tgBotToken\")\n}\n\nfunc (s *SettingService) SetTgBotToken(token string) error {\n\treturn s.setString(\"tgBotToken\", token)\n}\n\nfunc (s *SettingService) GetTgBotProxy() (string, error) {\n\treturn s.getString(\"tgBotProxy\")\n}\n\nfunc (s *SettingService) SetTgBotProxy(token string) error {\n\treturn s.setString(\"tgBotProxy\", token)\n}\n\nfunc (s *SettingService) GetTgBotAPIServer() (string, error) {\n\treturn s.getString(\"tgBotAPIServer\")\n}\n\nfunc (s *SettingService) SetTgBotAPIServer(token string) error {\n\treturn s.setString(\"tgBotAPIServer\", token)\n}\n\nfunc (s *SettingService) GetTgBotChatId() (string, error) {\n\treturn s.getString(\"tgBotChatId\")\n}\n\nfunc (s *SettingService) SetTgBotChatId(chatIds string) error {\n\treturn s.setString(\"tgBotChatId\", chatIds)\n}\n\nfunc (s *SettingService) GetTgbotEnabled() (bool, error) {\n\treturn s.getBool(\"tgBotEnable\")\n}\n\nfunc (s *SettingService) SetTgbotEnabled(value bool) error {\n\treturn s.setBool(\"tgBotEnable\", value)\n}\n\nfunc (s *SettingService) GetTgbotRuntime() (string, error) {\n\treturn s.getString(\"tgRunTime\")\n}\n\nfunc (s *SettingService) SetTgbotRuntime(time string) error {\n\treturn s.setString(\"tgRunTime\", time)\n}\n\nfunc (s *SettingService) GetTgBotBackup() (bool, error) {\n\treturn s.getBool(\"tgBotBackup\")\n}\n\nfunc (s *SettingService) GetTgBotLoginNotify() (bool, error) {\n\treturn s.getBool(\"tgBotLoginNotify\")\n}\n\nfunc (s *SettingService) GetTgCpu() (int, error) {\n\treturn s.getInt(\"tgCpu\")\n}\n\nfunc (s *SettingService) GetTgLang() (string, error) {\n\treturn s.getString(\"tgLang\")\n}\n\nfunc (s *SettingService) GetTwoFactorEnable() (bool, error) {\n\treturn s.getBool(\"twoFactorEnable\")\n}\n\nfunc (s *SettingService) SetTwoFactorEnable(value bool) error {\n\treturn s.setBool(\"twoFactorEnable\", value)\n}\n\nfunc (s *SettingService) GetTwoFactorToken() (string, error) {\n\treturn s.getString(\"twoFactorToken\")\n}\n\nfunc (s *SettingService) SetTwoFactorToken(value string) error {\n\treturn s.setString(\"twoFactorToken\", value)\n}\n\nfunc (s *SettingService) GetPort() (int, error) {\n\treturn s.getInt(\"webPort\")\n}\n\nfunc (s *SettingService) SetPort(port int) error {\n\treturn s.setInt(\"webPort\", port)\n}\n\nfunc (s *SettingService) SetCertFile(webCertFile string) error {\n\treturn s.setString(\"webCertFile\", webCertFile)\n}\n\nfunc (s *SettingService) GetCertFile() (string, error) {\n\treturn s.getString(\"webCertFile\")\n}\n\nfunc (s *SettingService) SetKeyFile(webKeyFile string) error {\n\treturn s.setString(\"webKeyFile\", webKeyFile)\n}\n\nfunc (s *SettingService) GetKeyFile() (string, error) {\n\treturn s.getString(\"webKeyFile\")\n}\n\nfunc (s *SettingService) GetExpireDiff() (int, error) {\n\treturn s.getInt(\"expireDiff\")\n}\n\nfunc (s *SettingService) GetTrafficDiff() (int, error) {\n\treturn s.getInt(\"trafficDiff\")\n}\n\nfunc (s *SettingService) GetSessionMaxAge() (int, error) {\n\treturn s.getInt(\"sessionMaxAge\")\n}\n\nfunc (s *SettingService) GetRemarkModel() (string, error) {\n\treturn s.getString(\"remarkModel\")\n}\n\nfunc (s *SettingService) GetSecret() ([]byte, error) {\n\tsecret, err := s.getString(\"secret\")\n\tif secret == defaultValueMap[\"secret\"] {\n\t\terr := s.saveSetting(\"secret\", secret)\n\t\tif err != nil {\n\t\t\tlogger.Warning(\"save secret failed:\", err)\n\t\t}\n\t}\n\treturn []byte(secret), err\n}\n\nfunc (s *SettingService) SetBasePath(basePath string) error {\n\tif !strings.HasPrefix(basePath, \"/\") {\n\t\tbasePath = \"/\" + basePath\n\t}\n\tif !strings.HasSuffix(basePath, \"/\") {\n\t\tbasePath += \"/\"\n\t}\n\treturn s.setString(\"webBasePath\", basePath)\n}\n\nfunc (s *SettingService) GetBasePath() (string, error) {\n\tbasePath, err := s.getString(\"webBasePath\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !strings.HasPrefix(basePath, \"/\") {\n\t\tbasePath = \"/\" + basePath\n\t}\n\tif !strings.HasSuffix(basePath, \"/\") {\n\t\tbasePath += \"/\"\n\t}\n\treturn basePath, nil\n}\n\nfunc (s *SettingService) GetTimeLocation() (*time.Location, error) {\n\tl, err := s.getString(\"timeLocation\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlocation, err := time.LoadLocation(l)\n\tif err != nil {\n\t\tdefaultLocation := defaultValueMap[\"timeLocation\"]\n\t\tlogger.Errorf(\"location <%v> not exist, using default location: %v\", l, defaultLocation)\n\t\treturn time.LoadLocation(defaultLocation)\n\t}\n\treturn location, nil\n}\n\nfunc (s *SettingService) GetSubEnable() (bool, error) {\n\treturn s.getBool(\"subEnable\")\n}\n\nfunc (s *SettingService) GetSubJsonEnable() (bool, error) {\n\treturn s.getBool(\"subJsonEnable\")\n}\n\nfunc (s *SettingService) GetSubTitle() (string, error) {\n\treturn s.getString(\"subTitle\")\n}\n\nfunc (s *SettingService) GetSubSupportUrl() (string, error) {\n\treturn s.getString(\"subSupportUrl\")\n}\n\nfunc (s *SettingService) GetSubProfileUrl() (string, error) {\n\treturn s.getString(\"subProfileUrl\")\n}\n\nfunc (s *SettingService) GetSubAnnounce() (string, error) {\n\treturn s.getString(\"subAnnounce\")\n}\n\nfunc (s *SettingService) GetSubEnableRouting() (bool, error) {\n\treturn s.getBool(\"subEnableRouting\")\n}\n\nfunc (s *SettingService) GetSubRoutingRules() (string, error) {\n\treturn s.getString(\"subRoutingRules\")\n}\n\nfunc (s *SettingService) GetSubListen() (string, error) {\n\treturn s.getString(\"subListen\")\n}\n\nfunc (s *SettingService) GetSubPort() (int, error) {\n\treturn s.getInt(\"subPort\")\n}\n\nfunc (s *SettingService) GetSubPath() (string, error) {\n\treturn s.getString(\"subPath\")\n}\n\nfunc (s *SettingService) GetSubJsonPath() (string, error) {\n\treturn s.getString(\"subJsonPath\")\n}\n\nfunc (s *SettingService) GetSubDomain() (string, error) {\n\treturn s.getString(\"subDomain\")\n}\n\nfunc (s *SettingService) SetSubCertFile(subCertFile string) error {\n\treturn s.setString(\"subCertFile\", subCertFile)\n}\n\nfunc (s *SettingService) GetSubCertFile() (string, error) {\n\treturn s.getString(\"subCertFile\")\n}\n\nfunc (s *SettingService) SetSubKeyFile(subKeyFile string) error {\n\treturn s.setString(\"subKeyFile\", subKeyFile)\n}\n\nfunc (s *SettingService) GetSubKeyFile() (string, error) {\n\treturn s.getString(\"subKeyFile\")\n}\n\nfunc (s *SettingService) GetSubUpdates() (string, error) {\n\treturn s.getString(\"subUpdates\")\n}\n\nfunc (s *SettingService) GetSubEncrypt() (bool, error) {\n\treturn s.getBool(\"subEncrypt\")\n}\n\nfunc (s *SettingService) GetSubShowInfo() (bool, error) {\n\treturn s.getBool(\"subShowInfo\")\n}\n\nfunc (s *SettingService) GetPageSize() (int, error) {\n\treturn s.getInt(\"pageSize\")\n}\n\nfunc (s *SettingService) GetSubURI() (string, error) {\n\treturn s.getString(\"subURI\")\n}\n\nfunc (s *SettingService) GetSubJsonURI() (string, error) {\n\treturn s.getString(\"subJsonURI\")\n}\n\nfunc (s *SettingService) GetSubJsonFragment() (string, error) {\n\treturn s.getString(\"subJsonFragment\")\n}\n\nfunc (s *SettingService) GetSubJsonNoises() (string, error) {\n\treturn s.getString(\"subJsonNoises\")\n}\n\nfunc (s *SettingService) GetSubJsonMux() (string, error) {\n\treturn s.getString(\"subJsonMux\")\n}\n\nfunc (s *SettingService) GetSubJsonRules() (string, error) {\n\treturn s.getString(\"subJsonRules\")\n}\n\nfunc (s *SettingService) GetDatepicker() (string, error) {\n\treturn s.getString(\"datepicker\")\n}\n\nfunc (s *SettingService) GetWarp() (string, error) {\n\treturn s.getString(\"warp\")\n}\n\nfunc (s *SettingService) SetWarp(data string) error {\n\treturn s.setString(\"warp\", data)\n}\n\nfunc (s *SettingService) GetExternalTrafficInformEnable() (bool, error) {\n\treturn s.getBool(\"externalTrafficInformEnable\")\n}\n\nfunc (s *SettingService) SetExternalTrafficInformEnable(value bool) error {\n\treturn s.setBool(\"externalTrafficInformEnable\", value)\n}\n\nfunc (s *SettingService) GetExternalTrafficInformURI() (string, error) {\n\treturn s.getString(\"externalTrafficInformURI\")\n}\n\nfunc (s *SettingService) SetExternalTrafficInformURI(InformURI string) error {\n\treturn s.setString(\"externalTrafficInformURI\", InformURI)\n}\n\nfunc (s *SettingService) GetIpLimitEnable() (bool, error) {\n\taccessLogPath, err := xray.GetAccessLogPath()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn (accessLogPath != \"none\" && accessLogPath != \"\"), nil\n}\n\n// GetLdapEnable returns whether LDAP is enabled.\nfunc (s *SettingService) GetLdapEnable() (bool, error) {\n\treturn s.getBool(\"ldapEnable\")\n}\n\nfunc (s *SettingService) GetLdapHost() (string, error) {\n\treturn s.getString(\"ldapHost\")\n}\n\nfunc (s *SettingService) GetLdapPort() (int, error) {\n\treturn s.getInt(\"ldapPort\")\n}\n\nfunc (s *SettingService) GetLdapUseTLS() (bool, error) {\n\treturn s.getBool(\"ldapUseTLS\")\n}\n\nfunc (s *SettingService) GetLdapBindDN() (string, error) {\n\treturn s.getString(\"ldapBindDN\")\n}\n\nfunc (s *SettingService) GetLdapPassword() (string, error) {\n\treturn s.getString(\"ldapPassword\")\n}\n\nfunc (s *SettingService) GetLdapBaseDN() (string, error) {\n\treturn s.getString(\"ldapBaseDN\")\n}\n\nfunc (s *SettingService) GetLdapUserFilter() (string, error) {\n\treturn s.getString(\"ldapUserFilter\")\n}\n\nfunc (s *SettingService) GetLdapUserAttr() (string, error) {\n\treturn s.getString(\"ldapUserAttr\")\n}\n\nfunc (s *SettingService) GetLdapVlessField() (string, error) {\n\treturn s.getString(\"ldapVlessField\")\n}\n\nfunc (s *SettingService) GetLdapSyncCron() (string, error) {\n\treturn s.getString(\"ldapSyncCron\")\n}\n\nfunc (s *SettingService) GetLdapFlagField() (string, error) {\n\treturn s.getString(\"ldapFlagField\")\n}\n\nfunc (s *SettingService) GetLdapTruthyValues() (string, error) {\n\treturn s.getString(\"ldapTruthyValues\")\n}\n\nfunc (s *SettingService) GetLdapInvertFlag() (bool, error) {\n\treturn s.getBool(\"ldapInvertFlag\")\n}\n\nfunc (s *SettingService) GetLdapInboundTags() (string, error) {\n\treturn s.getString(\"ldapInboundTags\")\n}\n\nfunc (s *SettingService) GetLdapAutoCreate() (bool, error) {\n\treturn s.getBool(\"ldapAutoCreate\")\n}\n\nfunc (s *SettingService) GetLdapAutoDelete() (bool, error) {\n\treturn s.getBool(\"ldapAutoDelete\")\n}\n\nfunc (s *SettingService) GetLdapDefaultTotalGB() (int, error) {\n\treturn s.getInt(\"ldapDefaultTotalGB\")\n}\n\nfunc (s *SettingService) GetLdapDefaultExpiryDays() (int, error) {\n\treturn s.getInt(\"ldapDefaultExpiryDays\")\n}\n\nfunc (s *SettingService) GetLdapDefaultLimitIP() (int, error) {\n\treturn s.getInt(\"ldapDefaultLimitIP\")\n}\n\nfunc (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {\n\tif err := allSetting.CheckValid(); err != nil {\n\t\treturn err\n\t}\n\n\tv := reflect.ValueOf(allSetting).Elem()\n\tt := reflect.TypeFor[entity.AllSetting]()\n\tfields := reflect_util.GetFields(t)\n\terrs := make([]error, 0)\n\tfor _, field := range fields {\n\t\tkey := field.Tag.Get(\"json\")\n\t\tfieldV := v.FieldByName(field.Name)\n\t\tvalue := fmt.Sprint(fieldV.Interface())\n\t\terr := s.saveSetting(key, value)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\treturn common.Combine(errs...)\n}\n\nfunc (s *SettingService) GetDefaultXrayConfig() (any, error) {\n\tvar jsonData any\n\terr := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn jsonData, nil\n}\n\nfunc extractHostname(host string) string {\n\th, _, err := net.SplitHostPort(host)\n\t// Err is not nil means host does not contain port\n\tif err != nil {\n\t\th = host\n\t}\n\n\tip := net.ParseIP(h)\n\t// If it's not an IP, return as is\n\tif ip == nil {\n\t\treturn h\n\t}\n\n\t// If it's an IPv4, return as is\n\tif ip.To4() != nil {\n\t\treturn h\n\t}\n\n\t// IPv6 needs bracketing\n\treturn \"[\" + h + \"]\"\n}\n\nfunc (s *SettingService) GetDefaultSettings(host string) (any, error) {\n\ttype settingFunc func() (any, error)\n\tsettings := map[string]settingFunc{\n\t\t\"expireDiff\":    func() (any, error) { return s.GetExpireDiff() },\n\t\t\"trafficDiff\":   func() (any, error) { return s.GetTrafficDiff() },\n\t\t\"pageSize\":      func() (any, error) { return s.GetPageSize() },\n\t\t\"defaultCert\":   func() (any, error) { return s.GetCertFile() },\n\t\t\"defaultKey\":    func() (any, error) { return s.GetKeyFile() },\n\t\t\"tgBotEnable\":   func() (any, error) { return s.GetTgbotEnabled() },\n\t\t\"subEnable\":     func() (any, error) { return s.GetSubEnable() },\n\t\t\"subJsonEnable\": func() (any, error) { return s.GetSubJsonEnable() },\n\t\t\"subTitle\":      func() (any, error) { return s.GetSubTitle() },\n\t\t\"subURI\":        func() (any, error) { return s.GetSubURI() },\n\t\t\"subJsonURI\":    func() (any, error) { return s.GetSubJsonURI() },\n\t\t\"remarkModel\":   func() (any, error) { return s.GetRemarkModel() },\n\t\t\"datepicker\":    func() (any, error) { return s.GetDatepicker() },\n\t\t\"ipLimitEnable\": func() (any, error) { return s.GetIpLimitEnable() },\n\t}\n\n\tresult := make(map[string]any)\n\n\tfor key, fn := range settings {\n\t\tvalue, err := fn()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tresult[key] = value\n\t}\n\n\tsubEnable := result[\"subEnable\"].(bool)\n\tsubJsonEnable := false\n\tif v, ok := result[\"subJsonEnable\"]; ok {\n\t\tif b, ok2 := v.(bool); ok2 {\n\t\t\tsubJsonEnable = b\n\t\t}\n\t}\n\tif (subEnable && result[\"subURI\"].(string) == \"\") || (subJsonEnable && result[\"subJsonURI\"].(string) == \"\") {\n\t\tsubURI := \"\"\n\t\tsubTitle, _ := s.GetSubTitle()\n\t\tsubPort, _ := s.GetSubPort()\n\t\tsubPath, _ := s.GetSubPath()\n\t\tsubJsonPath, _ := s.GetSubJsonPath()\n\t\tsubDomain, _ := s.GetSubDomain()\n\t\tsubKeyFile, _ := s.GetSubKeyFile()\n\t\tsubCertFile, _ := s.GetSubCertFile()\n\t\tsubTLS := false\n\t\tif subKeyFile != \"\" && subCertFile != \"\" {\n\t\t\tsubTLS = true\n\t\t}\n\t\tif subDomain == \"\" {\n\t\t\tsubDomain = extractHostname(host)\n\t\t}\n\t\tif subTLS {\n\t\t\tsubURI = \"https://\"\n\t\t} else {\n\t\t\tsubURI = \"http://\"\n\t\t}\n\t\tif (subPort == 443 && subTLS) || (subPort == 80 && !subTLS) {\n\t\t\tsubURI += subDomain\n\t\t} else {\n\t\t\tsubURI += fmt.Sprintf(\"%s:%d\", subDomain, subPort)\n\t\t}\n\t\tif subEnable && result[\"subURI\"].(string) == \"\" {\n\t\t\tresult[\"subURI\"] = subURI + subPath\n\t\t}\n\t\tif result[\"subTitle\"].(string) == \"\" {\n\t\t\tresult[\"subTitle\"] = subTitle\n\t\t}\n\t\tif subJsonEnable && result[\"subJsonURI\"].(string) == \"\" {\n\t\t\tresult[\"subJsonURI\"] = subURI + subJsonPath\n\t\t}\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "web/service/tgbot.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"embed\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\t\"io\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/global\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/locale\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/mymmrac/telego\"\n\tth \"github.com/mymmrac/telego/telegohandler\"\n\ttu \"github.com/mymmrac/telego/telegoutil\"\n\t\"github.com/skip2/go-qrcode\"\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttpproxy\"\n)\n\nvar (\n\tbot *telego.Bot\n\n\t// botCancel stores the function to cancel the context, stopping Long Polling gracefully.\n\tbotCancel context.CancelFunc\n\t// tgBotMutex protects concurrent access to botCancel variable\n\ttgBotMutex sync.Mutex\n\t// botWG waits for the OnReceive Long Polling goroutine to finish.\n\tbotWG sync.WaitGroup\n\n\tbotHandler  *th.BotHandler\n\tadminIds    []int64\n\tisRunning   bool\n\thostname    string\n\thashStorage *global.HashStorage\n\n\t// Performance improvements\n\tmessageWorkerPool   chan struct{} // Semaphore for limiting concurrent message processing\n\toptimizedHTTPClient *http.Client  // HTTP client with connection pooling and timeouts\n\n\t// Simple cache for frequently accessed data\n\tstatusCache struct {\n\t\tdata      *Status\n\t\ttimestamp time.Time\n\t\tmutex     sync.RWMutex\n\t}\n\n\tserverStatsCache struct {\n\t\tdata      string\n\t\ttimestamp time.Time\n\t\tmutex     sync.RWMutex\n\t}\n\n\t// clients data to adding new client\n\treceiver_inbound_ID int\n\tclient_Id           string\n\tclient_Flow         string\n\tclient_Email        string\n\tclient_LimitIP      int\n\tclient_TotalGB      int64\n\tclient_ExpiryTime   int64\n\tclient_Enable       bool\n\tclient_TgID         string\n\tclient_SubID        string\n\tclient_Comment      string\n\tclient_Reset        int\n\tclient_Security     string\n\tclient_ShPassword   string\n\tclient_TrPassword   string\n\tclient_Method       string\n)\n\nvar userStates = make(map[int64]string)\n\n// LoginStatus represents the result of a login attempt.\ntype LoginStatus byte\n\n// Login status constants\nconst (\n\tLoginSuccess        LoginStatus = 1        // Login was successful\n\tLoginFail           LoginStatus = 0        // Login failed\n\tEmptyTelegramUserID             = int64(0) // Default value for empty Telegram user ID\n)\n\n// Tgbot provides business logic for Telegram bot integration.\n// It handles bot commands, user interactions, and status reporting via Telegram.\ntype Tgbot struct {\n\tinboundService InboundService\n\tsettingService SettingService\n\tserverService  ServerService\n\txrayService    XrayService\n\tlastStatus     *Status\n}\n\n// NewTgbot creates a new Tgbot instance.\nfunc (t *Tgbot) NewTgbot() *Tgbot {\n\treturn new(Tgbot)\n}\n\n// I18nBot retrieves a localized message for the bot interface.\nfunc (t *Tgbot) I18nBot(name string, params ...string) string {\n\treturn locale.I18n(locale.Bot, name, params...)\n}\n\n// GetHashStorage returns the hash storage instance for callback queries.\nfunc (t *Tgbot) GetHashStorage() *global.HashStorage {\n\treturn hashStorage\n}\n\n// getCachedStatus returns cached server status if it's fresh enough (less than 5 seconds old)\nfunc (t *Tgbot) getCachedStatus() (*Status, bool) {\n\tstatusCache.mutex.RLock()\n\tdefer statusCache.mutex.RUnlock()\n\n\tif statusCache.data != nil && time.Since(statusCache.timestamp) < 5*time.Second {\n\t\treturn statusCache.data, true\n\t}\n\treturn nil, false\n}\n\n// setCachedStatus updates the status cache\nfunc (t *Tgbot) setCachedStatus(status *Status) {\n\tstatusCache.mutex.Lock()\n\tdefer statusCache.mutex.Unlock()\n\n\tstatusCache.data = status\n\tstatusCache.timestamp = time.Now()\n}\n\n// getCachedServerStats returns cached server stats if it's fresh enough (less than 10 seconds old)\nfunc (t *Tgbot) getCachedServerStats() (string, bool) {\n\tserverStatsCache.mutex.RLock()\n\tdefer serverStatsCache.mutex.RUnlock()\n\n\tif serverStatsCache.data != \"\" && time.Since(serverStatsCache.timestamp) < 10*time.Second {\n\t\treturn serverStatsCache.data, true\n\t}\n\treturn \"\", false\n}\n\n// setCachedServerStats updates the server stats cache\nfunc (t *Tgbot) setCachedServerStats(stats string) {\n\tserverStatsCache.mutex.Lock()\n\tdefer serverStatsCache.mutex.Unlock()\n\n\tserverStatsCache.data = stats\n\tserverStatsCache.timestamp = time.Now()\n}\n\n// Start initializes and starts the Telegram bot with the provided translation files.\nfunc (t *Tgbot) Start(i18nFS embed.FS) error {\n\t// Initialize localizer\n\terr := locale.InitLocalizer(i18nFS, &t.settingService)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If Start is called again (e.g. during reload), ensure any previous long-polling\n\t// loop is stopped before creating a new bot / receiver.\n\tStopBot()\n\n\t// Initialize hash storage to store callback queries\n\thashStorage = global.NewHashStorage(20 * time.Minute)\n\n\t// Initialize worker pool for concurrent message processing (max 10 concurrent handlers)\n\tmessageWorkerPool = make(chan struct{}, 10)\n\n\t// Initialize optimized HTTP client with connection pooling\n\toptimizedHTTPClient = &http.Client{\n\t\tTimeout: 15 * time.Second,\n\t\tTransport: &http.Transport{\n\t\t\tMaxIdleConns:        100,\n\t\t\tMaxIdleConnsPerHost: 10,\n\t\t\tIdleConnTimeout:     30 * time.Second,\n\t\t\tDisableKeepAlives:   false,\n\t\t},\n\t}\n\n\tt.SetHostname()\n\n\t// Get Telegram bot token\n\ttgBotToken, err := t.settingService.GetTgBotToken()\n\tif err != nil || tgBotToken == \"\" {\n\t\tlogger.Warning(\"Failed to get Telegram bot token:\", err)\n\t\treturn err\n\t}\n\n\t// Get Telegram bot chat ID(s)\n\ttgBotID, err := t.settingService.GetTgBotChatId()\n\tif err != nil {\n\t\tlogger.Warning(\"Failed to get Telegram bot chat ID:\", err)\n\t\treturn err\n\t}\n\n\tparsedAdminIds := make([]int64, 0)\n\t// Parse admin IDs from comma-separated string\n\tif tgBotID != \"\" {\n\t\tfor _, adminID := range strings.Split(tgBotID, \",\") {\n\t\t\tid, err := strconv.ParseInt(adminID, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warning(\"Failed to parse admin ID from Telegram bot chat ID:\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tparsedAdminIds = append(parsedAdminIds, int64(id))\n\t\t}\n\t}\n\ttgBotMutex.Lock()\n\tadminIds = parsedAdminIds\n\ttgBotMutex.Unlock()\n\n\t// Get Telegram bot proxy URL\n\ttgBotProxy, err := t.settingService.GetTgBotProxy()\n\tif err != nil {\n\t\tlogger.Warning(\"Failed to get Telegram bot proxy URL:\", err)\n\t}\n\n\t// Get Telegram bot API server URL\n\ttgBotAPIServer, err := t.settingService.GetTgBotAPIServer()\n\tif err != nil {\n\t\tlogger.Warning(\"Failed to get Telegram bot API server URL:\", err)\n\t}\n\n\t// Create new Telegram bot instance\n\tbot, err = t.NewBot(tgBotToken, tgBotProxy, tgBotAPIServer)\n\tif err != nil {\n\t\tlogger.Error(\"Failed to initialize Telegram bot API:\", err)\n\t\treturn err\n\t}\n\n\t// After bot initialization, set up bot commands with localized descriptions\n\terr = bot.SetMyCommands(context.Background(), &telego.SetMyCommandsParams{\n\t\tCommands: []telego.BotCommand{\n\t\t\t{Command: \"start\", Description: t.I18nBot(\"tgbot.commands.startDesc\")},\n\t\t\t{Command: \"help\", Description: t.I18nBot(\"tgbot.commands.helpDesc\")},\n\t\t\t{Command: \"status\", Description: t.I18nBot(\"tgbot.commands.statusDesc\")},\n\t\t\t{Command: \"id\", Description: t.I18nBot(\"tgbot.commands.idDesc\")},\n\t\t},\n\t})\n\tif err != nil {\n\t\tlogger.Warning(\"Failed to set bot commands:\", err)\n\t}\n\n\t// Start receiving Telegram bot messages\n\ttgBotMutex.Lock()\n\talreadyRunning := isRunning || botCancel != nil\n\ttgBotMutex.Unlock()\n\tif !alreadyRunning {\n\t\tlogger.Info(\"Telegram bot receiver started\")\n\t\tgo t.OnReceive()\n\t}\n\n\treturn nil\n}\n\n// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling\nfunc (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {\n\tclient := &fasthttp.Client{\n\t\t// Connection timeouts\n\t\tReadTimeout:                   30 * time.Second,\n\t\tWriteTimeout:                  30 * time.Second,\n\t\tMaxIdleConnDuration:           60 * time.Second,\n\t\tMaxConnDuration:               0, // unlimited, but controlled by MaxIdleConnDuration\n\t\tMaxIdemponentCallAttempts:     3,\n\t\tReadBufferSize:                4096,\n\t\tWriteBufferSize:               4096,\n\t\tMaxConnsPerHost:               100,\n\t\tMaxConnWaitTimeout:            10 * time.Second,\n\t\tDisableHeaderNamesNormalizing: false,\n\t\tDisablePathNormalizing:        false,\n\t\t// Retry on connection errors\n\t\tRetryIf: func(request *fasthttp.Request) bool {\n\t\t\t// Retry on connection errors for GET requests\n\t\t\treturn string(request.Header.Method()) == \"GET\" || string(request.Header.Method()) == \"POST\"\n\t\t},\n\t}\n\n\t// Set proxy if provided\n\tif proxyUrl != \"\" {\n\t\tclient.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl)\n\t}\n\n\treturn client\n}\n\n// NewBot creates a new Telegram bot instance with optional proxy and API server settings.\nfunc (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {\n\t// Validate proxy URL if provided\n\tif proxyUrl != \"\" {\n\t\tif !strings.HasPrefix(proxyUrl, \"socks5://\") {\n\t\t\tlogger.Warning(\"Invalid socks5 URL, ignoring proxy\")\n\t\t\tproxyUrl = \"\" // Clear invalid proxy\n\t\t} else {\n\t\t\t_, err := url.Parse(proxyUrl)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warningf(\"Can't parse proxy URL, ignoring proxy: %v\", err)\n\t\t\t\tproxyUrl = \"\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// Validate API server URL if provided\n\tif apiServerUrl != \"\" {\n\t\tif !strings.HasPrefix(apiServerUrl, \"http\") {\n\t\t\tlogger.Warning(\"Invalid http(s) URL for API server, using default\")\n\t\t\tapiServerUrl = \"\"\n\t\t} else {\n\t\t\t_, err := url.Parse(apiServerUrl)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warningf(\"Can't parse API server URL, using default: %v\", err)\n\t\t\t\tapiServerUrl = \"\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// Create robust fasthttp client\n\tclient := t.createRobustFastHTTPClient(proxyUrl)\n\n\t// Build bot options\n\tvar options []telego.BotOption\n\toptions = append(options, telego.WithFastHTTPClient(client))\n\n\tif apiServerUrl != \"\" {\n\t\toptions = append(options, telego.WithAPIServer(apiServerUrl))\n\t}\n\n\treturn telego.NewBot(token, options...)\n}\n\n// IsRunning checks if the Telegram bot is currently running.\nfunc (t *Tgbot) IsRunning() bool {\n\ttgBotMutex.Lock()\n\tdefer tgBotMutex.Unlock()\n\treturn isRunning\n}\n\n// SetHostname sets the hostname for the bot.\nfunc (t *Tgbot) SetHostname() {\n\thost, err := os.Hostname()\n\tif err != nil {\n\t\tlogger.Error(\"get hostname error:\", err)\n\t\thostname = \"\"\n\t\treturn\n\t}\n\thostname = host\n}\n\n// Stop safely stops the Telegram bot's Long Polling operation.\n// This method now calls the global StopBot function and cleans up other resources.\nfunc (t *Tgbot) Stop() {\n\tStopBot()\n\tlogger.Info(\"Stop Telegram receiver ...\")\n\ttgBotMutex.Lock()\n\tadminIds = nil\n\ttgBotMutex.Unlock()\n}\n\n// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.\n// This is the global function called from main.go's signal handler and t.Stop().\nfunc StopBot() {\n\t// Don't hold the mutex while cancelling/waiting.\n\ttgBotMutex.Lock()\n\tcancel := botCancel\n\tbotCancel = nil\n\thandler := botHandler\n\tbotHandler = nil\n\tisRunning = false\n\ttgBotMutex.Unlock()\n\n\tif handler != nil {\n\t\thandler.Stop()\n\t}\n\n\tif cancel != nil {\n\t\tlogger.Info(\"Sending cancellation signal to Telegram bot...\")\n\t\t// Cancels the context passed to UpdatesViaLongPolling; this closes updates channel\n\t\t// and lets botHandler.Start() exit cleanly.\n\t\tcancel()\n\t\tbotWG.Wait()\n\t\tlogger.Info(\"Telegram bot successfully stopped.\")\n\t}\n}\n\n// encodeQuery encodes the query string if it's longer than 64 characters.\nfunc (t *Tgbot) encodeQuery(query string) string {\n\t// NOTE: we only need to hash for more than 64 chars\n\tif len(query) <= 64 {\n\t\treturn query\n\t}\n\n\treturn hashStorage.SaveHash(query)\n}\n\n// decodeQuery decodes a hashed query string back to its original form.\nfunc (t *Tgbot) decodeQuery(query string) (string, error) {\n\tif !hashStorage.IsMD5(query) {\n\t\treturn query, nil\n\t}\n\n\tdecoded, exists := hashStorage.GetValue(query)\n\tif !exists {\n\t\treturn \"\", common.NewError(\"hash not found in storage!\")\n\t}\n\n\treturn decoded, nil\n}\n\n// OnReceive starts the message receiving loop for the Telegram bot.\nfunc (t *Tgbot) OnReceive() {\n\tparams := telego.GetUpdatesParams{\n\t\tTimeout: 20, // Reduced timeout to detect connection issues faster\n\t}\n\t// Strict singleton: never start a second long-polling loop.\n\ttgBotMutex.Lock()\n\tif botCancel != nil || isRunning {\n\t\ttgBotMutex.Unlock()\n\t\tlogger.Warning(\"TgBot OnReceive called while already running; ignoring.\")\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tbotCancel = cancel\n\tisRunning = true\n\t// Add to WaitGroup before releasing the lock so StopBot() can't return\n\t// before this receiver goroutine is accounted for.\n\tbotWG.Add(1)\n\ttgBotMutex.Unlock()\n\n\t// Get updates channel using the context with shorter timeout for better error recovery\n\tupdates, _ := bot.UpdatesViaLongPolling(ctx, &params)\n\tgo func() {\n\t\tdefer botWG.Done()\n\t\th, _ := th.NewBotHandler(bot, updates)\n\t\ttgBotMutex.Lock()\n\t\tbotHandler = h\n\t\ttgBotMutex.Unlock()\n\n\t\th.HandleMessage(func(ctx *th.Context, message telego.Message) error {\n\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\tt.SendMsgToTgbot(message.Chat.ID, t.I18nBot(\"tgbot.keyboardClosed\"), tu.ReplyKeyboardRemove())\n\t\t\treturn nil\n\t\t}, th.TextEqual(t.I18nBot(\"tgbot.buttons.closeKeyboard\")))\n\n\t\th.HandleMessage(func(ctx *th.Context, message telego.Message) error {\n\t\t\t// Use goroutine with worker pool for concurrent command processing\n\t\t\tgo func() {\n\t\t\t\tmessageWorkerPool <- struct{}{}        // Acquire worker\n\t\t\t\tdefer func() { <-messageWorkerPool }() // Release worker\n\n\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\tt.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))\n\t\t\t}()\n\t\t\treturn nil\n\t\t}, th.AnyCommand())\n\n\t\th.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {\n\t\t\t// Use goroutine with worker pool for concurrent callback processing\n\t\t\tgo func() {\n\t\t\t\tmessageWorkerPool <- struct{}{}        // Acquire worker\n\t\t\t\tdefer func() { <-messageWorkerPool }() // Release worker\n\n\t\t\t\tdelete(userStates, query.Message.GetChat().ID)\n\t\t\t\tt.answerCallback(&query, checkAdmin(query.From.ID))\n\t\t\t}()\n\t\t\treturn nil\n\t\t}, th.AnyCallbackQueryWithMessage())\n\n\t\th.HandleMessage(func(ctx *th.Context, message telego.Message) error {\n\t\t\tif userState, exists := userStates[message.Chat.ID]; exists {\n\t\t\t\tswitch userState {\n\t\t\t\tcase \"awaiting_id\":\n\t\t\t\t\tif client_Id == strings.TrimSpace(message.Text) {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.using_default_value\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\tinbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\t\t\tmessage_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\t\t\tt.addClient(message.Chat.ID, message_text)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tclient_Id = strings.TrimSpace(message.Text)\n\t\t\t\t\tif t.isSingleWord(client_Id) {\n\t\t\t\t\t\tuserStates[message.Chat.ID] = \"awaiting_id\"\n\n\t\t\t\t\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\tt.SendMsgToTgbot(message.Chat.ID, t.I18nBot(\"tgbot.messages.incorrect_input\"), cancel_btn_markup)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.received_id\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\tinbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\t\t\tmessage_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\t\t\tt.addClient(message.Chat.ID, message_text)\n\t\t\t\t\t}\n\t\t\t\tcase \"awaiting_password_tr\":\n\t\t\t\t\tif client_TrPassword == strings.TrimSpace(message.Text) {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.using_default_value\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tclient_TrPassword = strings.TrimSpace(message.Text)\n\t\t\t\t\tif t.isSingleWord(client_TrPassword) {\n\t\t\t\t\t\tuserStates[message.Chat.ID] = \"awaiting_password_tr\"\n\n\t\t\t\t\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\tt.SendMsgToTgbot(message.Chat.ID, t.I18nBot(\"tgbot.messages.incorrect_input\"), cancel_btn_markup)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.received_password\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\tinbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\t\t\tmessage_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\t\t\tt.addClient(message.Chat.ID, message_text)\n\t\t\t\t\t}\n\t\t\t\tcase \"awaiting_password_sh\":\n\t\t\t\t\tif client_ShPassword == strings.TrimSpace(message.Text) {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.using_default_value\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tclient_ShPassword = strings.TrimSpace(message.Text)\n\t\t\t\t\tif t.isSingleWord(client_ShPassword) {\n\t\t\t\t\t\tuserStates[message.Chat.ID] = \"awaiting_password_sh\"\n\n\t\t\t\t\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\tt.SendMsgToTgbot(message.Chat.ID, t.I18nBot(\"tgbot.messages.incorrect_input\"), cancel_btn_markup)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.received_password\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\tinbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\t\t\tmessage_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\t\t\tt.addClient(message.Chat.ID, message_text)\n\t\t\t\t\t}\n\t\t\t\tcase \"awaiting_email\":\n\t\t\t\t\tif client_Email == strings.TrimSpace(message.Text) {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.using_default_value\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tclient_Email = strings.TrimSpace(message.Text)\n\t\t\t\t\tif t.isSingleWord(client_Email) {\n\t\t\t\t\t\tuserStates[message.Chat.ID] = \"awaiting_email\"\n\n\t\t\t\t\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\tt.SendMsgToTgbot(message.Chat.ID, t.I18nBot(\"tgbot.messages.incorrect_input\"), cancel_btn_markup)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.received_email\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\tinbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\t\t\tmessage_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\t\t\tt.addClient(message.Chat.ID, message_text)\n\t\t\t\t\t}\n\t\t\t\tcase \"awaiting_comment\":\n\t\t\t\t\tif client_Comment == strings.TrimSpace(message.Text) {\n\t\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.using_default_value\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tclient_Comment = strings.TrimSpace(message.Text)\n\t\t\t\t\tt.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot(\"tgbot.messages.received_comment\"), 3, tu.ReplyKeyboardRemove())\n\t\t\t\t\tdelete(userStates, message.Chat.ID)\n\t\t\t\t\tinbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\t\tmessage_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\t\tt.addClient(message.Chat.ID, message_text)\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\tif message.UsersShared != nil {\n\t\t\t\t\tif checkAdmin(message.From.ID) {\n\t\t\t\t\t\tfor _, sharedUser := range message.UsersShared.Users {\n\t\t\t\t\t\t\tuserID := sharedUser.UserID\n\t\t\t\t\t\t\tneedRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID)\n\t\t\t\t\t\t\tif needRestart {\n\t\t\t\t\t\t\t\tt.xrayService.SetToNeedRestart()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\toutput := \"\"\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\toutput += t.I18nBot(\"tgbot.messages.selectUserFailed\")\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\toutput += t.I18nBot(\"tgbot.messages.userSaved\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tt.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove())\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.SendMsgToTgbot(message.Chat.ID, t.I18nBot(\"tgbot.noResult\"), tu.ReplyKeyboardRemove())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}, th.AnyMessage())\n\n\t\th.Start()\n\t}()\n}\n\n// answerCommand processes incoming command messages from Telegram users.\nfunc (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {\n\tmsg, onlyMessage := \"\", false\n\n\tcommand, _, commandArgs := tu.ParseCommand(message.Text)\n\n\t// Helper function to handle unknown commands.\n\thandleUnknownCommand := func() {\n\t\tmsg += t.I18nBot(\"tgbot.commands.unknown\")\n\t}\n\n\t// Handle the command.\n\tswitch command {\n\tcase \"help\":\n\t\tmsg += t.I18nBot(\"tgbot.commands.help\")\n\t\tmsg += t.I18nBot(\"tgbot.commands.pleaseChoose\")\n\tcase \"start\":\n\t\tmsg += t.I18nBot(\"tgbot.commands.start\", \"Firstname==\"+html.EscapeString(message.From.FirstName))\n\t\tif isAdmin {\n\t\t\tmsg += t.I18nBot(\"tgbot.commands.welcome\", \"Hostname==\"+hostname)\n\t\t}\n\t\tmsg += \"\\n\\n\" + t.I18nBot(\"tgbot.commands.pleaseChoose\")\n\tcase \"status\":\n\t\tonlyMessage = true\n\t\tmsg += t.I18nBot(\"tgbot.commands.status\")\n\tcase \"id\":\n\t\tonlyMessage = true\n\t\tmsg += t.I18nBot(\"tgbot.commands.getID\", \"ID==\"+strconv.FormatInt(message.From.ID, 10))\n\tcase \"usage\":\n\t\tonlyMessage = true\n\t\tif len(commandArgs) > 0 {\n\t\t\tif isAdmin {\n\t\t\t\tt.searchClient(chatId, commandArgs[0])\n\t\t\t} else {\n\t\t\t\tt.getClientUsage(chatId, int64(message.From.ID), commandArgs[0])\n\t\t\t}\n\t\t} else {\n\t\t\tmsg += t.I18nBot(\"tgbot.commands.usage\")\n\t\t}\n\tcase \"inbound\":\n\t\tonlyMessage = true\n\t\tif isAdmin && len(commandArgs) > 0 {\n\t\t\tt.searchInbound(chatId, commandArgs[0])\n\t\t} else {\n\t\t\thandleUnknownCommand()\n\t\t}\n\tcase \"restart\":\n\t\tonlyMessage = true\n\t\tif isAdmin {\n\t\t\tif len(commandArgs) == 0 {\n\t\t\t\tif t.xrayService.IsXrayRunning() {\n\t\t\t\t\terr := t.xrayService.RestartXray(true)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tmsg += t.I18nBot(\"tgbot.commands.restartFailed\", \"Error==\"+err.Error())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmsg += t.I18nBot(\"tgbot.commands.restartSuccess\")\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tmsg += t.I18nBot(\"tgbot.commands.xrayNotRunning\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\thandleUnknownCommand()\n\t\t\t\tmsg += t.I18nBot(\"tgbot.commands.restartUsage\")\n\t\t\t}\n\t\t} else {\n\t\t\thandleUnknownCommand()\n\t\t}\n\tdefault:\n\t\thandleUnknownCommand()\n\t}\n\n\tif msg != \"\" {\n\t\tt.sendResponse(chatId, msg, onlyMessage, isAdmin)\n\t}\n}\n\n// sendResponse sends the response message based on the onlyMessage flag.\nfunc (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) {\n\tif onlyMessage {\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t} else {\n\t\tt.SendAnswer(chatId, msg, isAdmin)\n\t}\n}\n\n// randomLowerAndNum generates a random string of lowercase letters and numbers.\nfunc (t *Tgbot) randomLowerAndNum(length int) string {\n\tcharset := \"abcdefghijklmnopqrstuvwxyz0123456789\"\n\tbytes := make([]byte, length)\n\tfor i := range bytes {\n\t\trandomIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))\n\t\tbytes[i] = charset[randomIndex.Int64()]\n\t}\n\treturn string(bytes)\n}\n\n// randomShadowSocksPassword generates a random password for Shadowsocks.\nfunc (t *Tgbot) randomShadowSocksPassword() string {\n\tarray := make([]byte, 32)\n\t_, err := rand.Read(array)\n\tif err != nil {\n\t\treturn t.randomLowerAndNum(32)\n\t}\n\treturn base64.StdEncoding.EncodeToString(array)\n}\n\n// answerCallback processes callback queries from inline keyboards.\nfunc (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {\n\tchatId := callbackQuery.Message.GetChat().ID\n\n\tif isAdmin {\n\t\t// get query from hash storage\n\t\tdecodedQuery, err := t.decodeQuery(callbackQuery.Data)\n\t\tif err != nil {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.noQuery\"))\n\t\t\treturn\n\t\t}\n\t\tdataArray := strings.Split(decodedQuery, \" \")\n\n\t\tif len(dataArray) >= 2 && len(dataArray[1]) > 0 {\n\t\t\temail := dataArray[1]\n\t\t\tswitch dataArray[0] {\n\t\t\tcase \"get_clients_for_sub\":\n\t\t\t\tinboundId := dataArray[1]\n\t\t\t\tinboundIdInt, err := strconv.Atoi(inboundId)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tclientsKB, err := t.getInboundClientsFor(inboundIdInt, \"client_sub_links\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tinbound, _ := t.inboundService.GetInbound(inboundIdInt)\n\t\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseClient\", \"Inbound==\"+inbound.Remark), clientsKB)\n\t\t\tcase \"get_clients_for_individual\":\n\t\t\t\tinboundId := dataArray[1]\n\t\t\t\tinboundIdInt, err := strconv.Atoi(inboundId)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tclientsKB, err := t.getInboundClientsFor(inboundIdInt, \"client_individual_links\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tinbound, _ := t.inboundService.GetInbound(inboundIdInt)\n\t\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseClient\", \"Inbound==\"+inbound.Remark), clientsKB)\n\t\t\tcase \"get_clients_for_qr\":\n\t\t\t\tinboundId := dataArray[1]\n\t\t\t\tinboundIdInt, err := strconv.Atoi(inboundId)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tclientsKB, err := t.getInboundClientsFor(inboundIdInt, \"client_qr_links\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tinbound, _ := t.inboundService.GetInbound(inboundIdInt)\n\t\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseClient\", \"Inbound==\"+inbound.Remark), clientsKB)\n\t\t\tcase \"client_sub_links\":\n\t\t\t\tt.sendClientSubLinks(chatId, email)\n\t\t\t\treturn\n\t\t\tcase \"client_individual_links\":\n\t\t\t\tt.sendClientIndividualLinks(chatId, email)\n\t\t\t\treturn\n\t\t\tcase \"client_qr_links\":\n\t\t\t\tt.sendClientQRLinks(chatId, email)\n\t\t\t\treturn\n\t\t\tcase \"client_get_usage\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.messages.email\", \"Email==\"+email))\n\t\t\t\tt.searchClient(chatId, email)\n\t\t\tcase \"client_refresh\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.clientRefreshSuccess\", \"Email==\"+email))\n\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"client_cancel\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.canceled\", \"Email==\"+email))\n\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"ips_refresh\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.IpRefreshSuccess\", \"Email==\"+email))\n\t\t\t\tt.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"ips_cancel\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.canceled\", \"Email==\"+email))\n\t\t\t\tt.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"tgid_refresh\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.TGIdRefreshSuccess\", \"Email==\"+email))\n\t\t\t\tt.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"tgid_cancel\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.canceled\", \"Email==\"+email))\n\t\t\t\tt.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"reset_traffic\":\n\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancelReset\")).WithCallbackData(t.encodeQuery(\"client_cancel \"+email)),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmResetTraffic\")).WithCallbackData(t.encodeQuery(\"reset_traffic_c \"+email)),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\tcase \"reset_traffic_c\":\n\t\t\t\terr := t.inboundService.ResetClientTrafficByEmail(email)\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.resetTrafficSuccess\", \"Email==\"+email))\n\t\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\t\t} else {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t}\n\t\t\tcase \"limit_traffic\":\n\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"client_cancel \"+email)),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.unlimited\")).WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 0\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.custom\")).WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" 0\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"1 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 1\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"5 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 5\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"10 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 10\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"20 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 20\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"30 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 30\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"40 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 40\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"50 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 50\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"60 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 60\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"80 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 80\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"100 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 100\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"150 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 150\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"200 GB\").WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" 200\")),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\tcase \"limit_traffic_c\":\n\t\t\t\tif len(dataArray) == 3 {\n\t\t\t\t\tlimitTraffic, err := strconv.Atoi(dataArray[2])\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tneedRestart, err := t.inboundService.ResetClientTrafficLimitByEmail(email, limitTraffic)\n\t\t\t\t\t\tif needRestart {\n\t\t\t\t\t\t\tt.xrayService.SetToNeedRestart()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.setTrafficLimitSuccess\", \"Email==\"+email))\n\t\t\t\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"limit_traffic_in\":\n\t\t\t\tif len(dataArray) >= 3 {\n\t\t\t\t\toldInputNumber, err := strconv.Atoi(dataArray[2])\n\t\t\t\t\tinputNumber := oldInputNumber\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif len(dataArray) == 4 {\n\t\t\t\t\t\t\tnum, err := strconv.Atoi(dataArray[3])\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tswitch num {\n\t\t\t\t\t\t\t\tcase -2:\n\t\t\t\t\t\t\t\t\tinputNumber = 0\n\t\t\t\t\t\t\t\tcase -1:\n\t\t\t\t\t\t\t\t\tif inputNumber > 0 {\n\t\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber / 10)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber * 10) + num\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber == oldInputNumber {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber >= 999999 {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"client_cancel \"+email)),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmNumberAdd\", \"Num==\"+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery(\"limit_traffic_c \"+email+\" \"+strconv.Itoa(inputNumber))),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"1\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 1\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"2\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"3\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 3\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"4\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 4\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"5\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 5\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"6\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 6\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"7\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 7\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"8\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 8\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"9\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 9\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"🔄\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" -2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"0\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 0\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"⬅️\").WithCallbackData(t.encodeQuery(\"limit_traffic_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" -1\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"add_client_limit_traffic_c\":\n\t\t\t\tlimitTraffic, _ := strconv.ParseInt(dataArray[1], 10, 64)\n\t\t\t\tclient_TotalGB = limitTraffic * 1024 * 1024 * 1024\n\t\t\t\tmessageId := callbackQuery.Message.GetMessageID()\n\t\t\t\tinbound, err := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tmessage_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tt.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\tcase \"add_client_limit_traffic_in\":\n\t\t\t\tif len(dataArray) >= 2 {\n\t\t\t\t\toldInputNumber, err := strconv.Atoi(dataArray[1])\n\t\t\t\t\tinputNumber := oldInputNumber\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif len(dataArray) == 3 {\n\t\t\t\t\t\t\tnum, err := strconv.Atoi(dataArray[2])\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tswitch num {\n\t\t\t\t\t\t\t\tcase -2:\n\t\t\t\t\t\t\t\t\tinputNumber = 0\n\t\t\t\t\t\t\t\tcase -1:\n\t\t\t\t\t\t\t\t\tif inputNumber > 0 {\n\t\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber / 10)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber * 10) + num\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber == oldInputNumber {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber >= 999999 {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"add_client_default_traffic_exp\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmNumberAdd\", \"Num==\"+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c \"+strconv.Itoa(inputNumber))),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"1\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 1\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"2\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"3\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 3\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"4\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 4\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"5\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 5\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"6\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 6\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"7\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 7\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"8\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 8\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"9\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 9\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"🔄\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" -2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"0\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" 0\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"⬅️\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in \"+strconv.Itoa(inputNumber)+\" -1\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"reset_exp\":\n\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancelReset\")).WithCallbackData(t.encodeQuery(\"client_cancel \"+email)),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.unlimited\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 0\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.custom\")).WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" 0\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 7 \"+t.I18nBot(\"tgbot.days\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 7\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 10 \"+t.I18nBot(\"tgbot.days\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 10\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 14 \"+t.I18nBot(\"tgbot.days\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 14\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 20 \"+t.I18nBot(\"tgbot.days\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 20\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 1 \"+t.I18nBot(\"tgbot.month\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 30\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 3 \"+t.I18nBot(\"tgbot.months\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 90\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 6 \"+t.I18nBot(\"tgbot.months\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 180\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 12 \"+t.I18nBot(\"tgbot.months\")).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" 365\")),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\tcase \"reset_exp_c\":\n\t\t\t\tif len(dataArray) == 3 {\n\t\t\t\t\tdays, err := strconv.ParseInt(dataArray[2], 10, 64)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tvar date int64\n\t\t\t\t\t\tif days > 0 {\n\t\t\t\t\t\t\ttraffic, err := t.inboundService.GetClientTrafficByEmail(email)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tlogger.Warning(err)\n\t\t\t\t\t\t\t\tmsg := t.I18nBot(\"tgbot.wentWrong\")\n\t\t\t\t\t\t\t\tt.SendMsgToTgbot(chatId, msg)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif traffic == nil {\n\t\t\t\t\t\t\t\tmsg := t.I18nBot(\"tgbot.noResult\")\n\t\t\t\t\t\t\t\tt.SendMsgToTgbot(chatId, msg)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif traffic.ExpiryTime > 0 {\n\t\t\t\t\t\t\t\tif traffic.ExpiryTime-time.Now().Unix()*1000 < 0 {\n\t\t\t\t\t\t\t\t\tdate = -int64(days * 24 * 60 * 60000)\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tdate = traffic.ExpiryTime + int64(days*24*60*60000)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tdate = traffic.ExpiryTime - int64(days*24*60*60000)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\t\t\t\t\t\tneedRestart, err := t.inboundService.ResetClientExpiryTimeByEmail(email, date)\n\t\t\t\t\t\tif needRestart {\n\t\t\t\t\t\t\tt.xrayService.SetToNeedRestart()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.expireResetSuccess\", \"Email==\"+email))\n\t\t\t\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"reset_exp_in\":\n\t\t\t\tif len(dataArray) >= 3 {\n\t\t\t\t\toldInputNumber, err := strconv.Atoi(dataArray[2])\n\t\t\t\t\tinputNumber := oldInputNumber\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif len(dataArray) == 4 {\n\t\t\t\t\t\t\tnum, err := strconv.Atoi(dataArray[3])\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tswitch num {\n\t\t\t\t\t\t\t\tcase -2:\n\t\t\t\t\t\t\t\t\tinputNumber = 0\n\t\t\t\t\t\t\t\tcase -1:\n\t\t\t\t\t\t\t\t\tif inputNumber > 0 {\n\t\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber / 10)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber * 10) + num\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber == oldInputNumber {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber >= 999999 {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"client_cancel \"+email)),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmNumber\", \"Num==\"+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery(\"reset_exp_c \"+email+\" \"+strconv.Itoa(inputNumber))),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"1\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 1\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"2\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"3\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 3\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"4\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 4\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"5\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 5\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"6\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 6\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"7\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 7\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"8\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 8\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"9\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 9\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"🔄\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" -2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"0\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 0\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"⬅️\").WithCallbackData(t.encodeQuery(\"reset_exp_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" -1\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"add_client_reset_exp_c\":\n\t\t\t\tclient_ExpiryTime = 0\n\t\t\t\tdays, _ := strconv.ParseInt(dataArray[1], 10, 64)\n\t\t\t\tvar date int64\n\t\t\t\tif client_ExpiryTime > 0 {\n\t\t\t\t\tif client_ExpiryTime-time.Now().Unix()*1000 < 0 {\n\t\t\t\t\t\tdate = -int64(days * 24 * 60 * 60000)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdate = client_ExpiryTime + int64(days*24*60*60000)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tdate = client_ExpiryTime - int64(days*24*60*60000)\n\t\t\t\t}\n\t\t\t\tclient_ExpiryTime = date\n\n\t\t\t\tmessageId := callbackQuery.Message.GetMessageID()\n\t\t\t\tinbound, err := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tmessage_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tt.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\tcase \"add_client_reset_exp_in\":\n\t\t\t\tif len(dataArray) >= 2 {\n\t\t\t\t\toldInputNumber, err := strconv.Atoi(dataArray[1])\n\t\t\t\t\tinputNumber := oldInputNumber\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif len(dataArray) == 3 {\n\t\t\t\t\t\t\tnum, err := strconv.Atoi(dataArray[2])\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tswitch num {\n\t\t\t\t\t\t\t\tcase -2:\n\t\t\t\t\t\t\t\t\tinputNumber = 0\n\t\t\t\t\t\t\t\tcase -1:\n\t\t\t\t\t\t\t\t\tif inputNumber > 0 {\n\t\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber / 10)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber * 10) + num\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber == oldInputNumber {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber >= 999999 {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"add_client_default_traffic_exp\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmNumberAdd\", \"Num==\"+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c \"+strconv.Itoa(inputNumber))),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"1\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 1\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"2\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"3\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 3\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"4\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 4\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"5\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 5\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"6\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 6\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"7\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 7\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"8\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 8\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"9\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 9\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"🔄\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" -2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"0\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" 0\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"⬅️\").WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in \"+strconv.Itoa(inputNumber)+\" -1\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"ip_limit\":\n\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancelIpLimit\")).WithCallbackData(t.encodeQuery(\"client_cancel \"+email)),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.unlimited\")).WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 0\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.custom\")).WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" 0\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"1\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 1\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"2\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 2\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"3\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 3\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"4\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 4\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"5\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 5\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"6\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 6\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"7\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 7\")),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"8\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 8\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"9\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 9\")),\n\t\t\t\t\t\ttu.InlineKeyboardButton(\"10\").WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" 10\")),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\tcase \"ip_limit_c\":\n\t\t\t\tif len(dataArray) == 3 {\n\t\t\t\t\tcount, err := strconv.Atoi(dataArray[2])\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tneedRestart, err := t.inboundService.ResetClientIpLimitByEmail(email, count)\n\t\t\t\t\t\tif needRestart {\n\t\t\t\t\t\t\tt.xrayService.SetToNeedRestart()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.resetIpSuccess\", \"Email==\"+email, \"Count==\"+strconv.Itoa(count)))\n\t\t\t\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"ip_limit_in\":\n\t\t\t\tif len(dataArray) >= 3 {\n\t\t\t\t\toldInputNumber, err := strconv.Atoi(dataArray[2])\n\t\t\t\t\tinputNumber := oldInputNumber\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif len(dataArray) == 4 {\n\t\t\t\t\t\t\tnum, err := strconv.Atoi(dataArray[3])\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tswitch num {\n\t\t\t\t\t\t\t\tcase -2:\n\t\t\t\t\t\t\t\t\tinputNumber = 0\n\t\t\t\t\t\t\t\tcase -1:\n\t\t\t\t\t\t\t\t\tif inputNumber > 0 {\n\t\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber / 10)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber * 10) + num\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber == oldInputNumber {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber >= 999999 {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"client_cancel \"+email)),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmNumber\", \"Num==\"+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery(\"ip_limit_c \"+email+\" \"+strconv.Itoa(inputNumber))),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"1\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 1\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"2\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"3\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 3\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"4\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 4\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"5\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 5\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"6\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 6\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"7\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 7\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"8\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 8\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"9\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 9\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"🔄\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" -2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"0\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" 0\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"⬅️\").WithCallbackData(t.encodeQuery(\"ip_limit_in \"+email+\" \"+strconv.Itoa(inputNumber)+\" -1\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\tcase \"add_client_ip_limit_c\":\n\t\t\t\tif len(dataArray) == 2 {\n\t\t\t\t\tcount, _ := strconv.Atoi(dataArray[1])\n\t\t\t\t\tclient_LimitIP = count\n\t\t\t\t}\n\n\t\t\t\tmessageId := callbackQuery.Message.GetMessageID()\n\t\t\t\tinbound, err := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tmessage_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tt.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\tcase \"add_client_ip_limit_in\":\n\t\t\t\tif len(dataArray) >= 2 {\n\t\t\t\t\toldInputNumber, err := strconv.Atoi(dataArray[1])\n\t\t\t\t\tinputNumber := oldInputNumber\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tif len(dataArray) == 3 {\n\t\t\t\t\t\t\tnum, err := strconv.Atoi(dataArray[2])\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tswitch num {\n\t\t\t\t\t\t\t\tcase -2:\n\t\t\t\t\t\t\t\t\tinputNumber = 0\n\t\t\t\t\t\t\t\tcase -1:\n\t\t\t\t\t\t\t\t\tif inputNumber > 0 {\n\t\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber / 10)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tinputNumber = (inputNumber * 10) + num\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber == oldInputNumber {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif inputNumber >= 999999 {\n\t\t\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"add_client_default_ip_limit\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmNumber\", \"Num==\"+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c \"+strconv.Itoa(inputNumber))),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"1\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 1\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"2\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"3\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 3\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"4\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 4\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"5\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 5\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"6\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 6\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"7\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 7\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"8\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 8\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"9\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 9\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"🔄\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" -2\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"0\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" 0\")),\n\t\t\t\t\t\t\t\ttu.InlineKeyboardButton(\"⬅️\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in \"+strconv.Itoa(inputNumber)+\" -1\")),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"clear_ips\":\n\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"ips_cancel \"+email)),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmClearIps\")).WithCallbackData(t.encodeQuery(\"clear_ips_c \"+email)),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\tcase \"clear_ips_c\":\n\t\t\t\terr := t.inboundService.ClearClientIps(email)\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.clearIpSuccess\", \"Email==\"+email))\n\t\t\t\t\tt.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\t\t} else {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t}\n\t\t\tcase \"ip_log\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.getIpLog\", \"Email==\"+email))\n\t\t\t\tt.searchClientIps(chatId, email)\n\t\t\tcase \"tg_user\":\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.getUserInfo\", \"Email==\"+email))\n\t\t\t\tt.clientTelegramUserInfo(chatId, email)\n\t\t\tcase \"tgid_remove\":\n\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"tgid_cancel \"+email)),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmRemoveTGUser\")).WithCallbackData(t.encodeQuery(\"tgid_remove_c \"+email)),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\tcase \"tgid_remove_c\":\n\t\t\t\ttraffic, err := t.inboundService.GetClientTrafficByEmail(email)\n\t\t\t\tif err != nil || traffic == nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tneedRestart, err := t.inboundService.SetClientTelegramUserID(traffic.Id, EmptyTelegramUserID)\n\t\t\t\tif needRestart {\n\t\t\t\t\tt.xrayService.SetToNeedRestart()\n\t\t\t\t}\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.removedTGUserSuccess\", \"Email==\"+email))\n\t\t\t\t\tt.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\t\t} else {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t}\n\t\t\tcase \"toggle_enable\":\n\t\t\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"client_cancel \"+email)),\n\t\t\t\t\t),\n\t\t\t\t\ttu.InlineKeyboardRow(\n\t\t\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmToggle\")).WithCallbackData(t.encodeQuery(\"toggle_enable_c \"+email)),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\t\t\tcase \"toggle_enable_c\":\n\t\t\t\tenabled, needRestart, err := t.inboundService.ToggleClientEnableByEmail(email)\n\t\t\t\tif needRestart {\n\t\t\t\t\tt.xrayService.SetToNeedRestart()\n\t\t\t\t}\n\t\t\t\tif err == nil {\n\t\t\t\t\tif enabled {\n\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.enableSuccess\", \"Email==\"+email))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.disableSuccess\", \"Email==\"+email))\n\t\t\t\t\t}\n\t\t\t\t\tt.searchClient(chatId, email, callbackQuery.Message.GetMessageID())\n\t\t\t\t} else {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.errorOperation\"))\n\t\t\t\t}\n\t\t\tcase \"get_clients\":\n\t\t\t\tinboundId := dataArray[1]\n\t\t\t\tinboundIdInt, err := strconv.Atoi(inboundId)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tinbound, err := t.inboundService.GetInbound(inboundIdInt)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tclients, err := t.getInboundClients(inboundIdInt)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseClient\", \"Inbound==\"+inbound.Remark), clients)\n\t\t\tcase \"add_client_to\":\n\t\t\t\t// assign default values to clients variables\n\t\t\t\tclient_Id = uuid.New().String()\n\t\t\t\tclient_Flow = \"\"\n\t\t\t\tclient_Email = t.randomLowerAndNum(8)\n\t\t\t\tclient_LimitIP = 0\n\t\t\t\tclient_TotalGB = 0\n\t\t\t\tclient_ExpiryTime = 0\n\t\t\t\tclient_Enable = true\n\t\t\t\tclient_TgID = \"\"\n\t\t\t\tclient_SubID = t.randomLowerAndNum(16)\n\t\t\t\tclient_Comment = \"\"\n\t\t\t\tclient_Reset = 0\n\t\t\t\tclient_Security = \"auto\"\n\t\t\t\tclient_ShPassword = t.randomShadowSocksPassword()\n\t\t\t\tclient_TrPassword = t.randomLowerAndNum(10)\n\t\t\t\tclient_Method = \"\"\n\n\t\t\t\tinboundId := dataArray[1]\n\t\t\t\tinboundIdInt, err := strconv.Atoi(inboundId)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\treceiver_inbound_ID = inboundIdInt\n\t\t\t\tinbound, err := t.inboundService.GetInbound(inboundIdInt)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tmessage_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tt.addClient(callbackQuery.Message.GetChat().ID, message_text)\n\t\t\t}\n\t\t\treturn\n\t\t} else {\n\t\t\tswitch callbackQuery.Data {\n\t\t\tcase \"get_inbounds\":\n\t\t\t\tinbounds, err := t.getInbounds()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\n\t\t\t\t}\n\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.allClients\"))\n\t\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseInbound\"), inbounds)\n\t\t\tcase \"admin_client_sub_links\":\n\t\t\t\tinbounds, err := t.getInboundsFor(\"get_clients_for_sub\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseInbound\"), inbounds)\n\t\t\tcase \"admin_client_individual_links\":\n\t\t\t\tinbounds, err := t.getInboundsFor(\"get_clients_for_individual\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseInbound\"), inbounds)\n\t\t\tcase \"admin_client_qr_links\":\n\t\t\t\tinbounds, err := t.getInboundsFor(\"get_clients_for_qr\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseInbound\"), inbounds)\n\t\t\t}\n\n\t\t}\n\t}\n\n\tswitch callbackQuery.Data {\n\tcase \"get_usage\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.serverUsage\"))\n\t\tt.getServerUsage(chatId)\n\tcase \"usage_refresh\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\tt.getServerUsage(chatId, callbackQuery.Message.GetMessageID())\n\tcase \"inbounds\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.getInbounds\"))\n\t\tt.SendMsgToTgbot(chatId, t.getInboundUsages())\n\tcase \"deplete_soon\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.depleteSoon\"))\n\t\tt.getExhausted(chatId)\n\tcase \"get_backup\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.dbBackup\"))\n\t\tt.sendBackup(chatId)\n\tcase \"get_banlogs\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.getBanLogs\"))\n\t\tt.sendBanLogs(chatId, true)\n\tcase \"client_traffic\":\n\t\ttgUserID := callbackQuery.From.ID\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.clientUsage\"))\n\t\tt.getClientUsage(chatId, tgUserID)\n\tcase \"client_commands\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.commands\"))\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.commands.helpClientCommands\"))\n\tcase \"client_sub_links\":\n\t\t// show user's own clients to choose one for sub links\n\t\ttgUserID := callbackQuery.From.ID\n\t\ttraffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)\n\t\tif err != nil {\n\t\t\t// fallback to message\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\t\treturn\n\t\t}\n\t\tif len(traffics) == 0 {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.askToAddUserId\", \"TgUserID==\"+strconv.FormatInt(tgUserID, 10)))\n\t\t\treturn\n\t\t}\n\t\tvar buttons []telego.InlineKeyboardButton\n\t\tfor _, tr := range traffics {\n\t\t\tbuttons = append(buttons, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery(\"client_sub_links \"+tr.Email)))\n\t\t}\n\t\tcols := 1\n\t\tif len(buttons) >= 6 {\n\t\t\tcols = 2\n\t\t}\n\t\tkeyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.commands.pleaseChoose\"), keyboard)\n\tcase \"client_individual_links\":\n\t\t// show user's clients to choose for individual links\n\t\ttgUserID := callbackQuery.From.ID\n\t\ttraffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)\n\t\tif err != nil {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\t\treturn\n\t\t}\n\t\tif len(traffics) == 0 {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.askToAddUserId\", \"TgUserID==\"+strconv.FormatInt(tgUserID, 10)))\n\t\t\treturn\n\t\t}\n\t\tvar buttons2 []telego.InlineKeyboardButton\n\t\tfor _, tr := range traffics {\n\t\t\tbuttons2 = append(buttons2, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery(\"client_individual_links \"+tr.Email)))\n\t\t}\n\t\tcols2 := 1\n\t\tif len(buttons2) >= 6 {\n\t\t\tcols2 = 2\n\t\t}\n\t\tkeyboard2 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols2, buttons2...))\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.commands.pleaseChoose\"), keyboard2)\n\tcase \"client_qr_links\":\n\t\t// show user's clients to choose for QR codes\n\t\ttgUserID := callbackQuery.From.ID\n\t\ttraffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)\n\t\tif err != nil {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOccurred\")+\"\\r\\n\"+err.Error())\n\t\t\treturn\n\t\t}\n\t\tif len(traffics) == 0 {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.askToAddUserId\", \"TgUserID==\"+strconv.FormatInt(tgUserID, 10)))\n\t\t\treturn\n\t\t}\n\t\tvar buttons3 []telego.InlineKeyboardButton\n\t\tfor _, tr := range traffics {\n\t\t\tbuttons3 = append(buttons3, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery(\"client_qr_links \"+tr.Email)))\n\t\t}\n\t\tcols3 := 1\n\t\tif len(buttons3) >= 6 {\n\t\t\tcols3 = 2\n\t\t}\n\t\tkeyboard3 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols3, buttons3...))\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.commands.pleaseChoose\"), keyboard3)\n\tcase \"onlines\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.onlines\"))\n\t\tt.onlineClients(chatId)\n\tcase \"onlines_refresh\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.successfulOperation\"))\n\t\tt.onlineClients(chatId, callbackQuery.Message.GetMessageID())\n\tcase \"commands\":\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.commands\"))\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.commands.helpAdminCommands\"))\n\tcase \"add_client\":\n\t\t// assign default values to clients variables\n\t\tclient_Id = uuid.New().String()\n\t\tclient_Flow = \"\"\n\t\tclient_Email = t.randomLowerAndNum(8)\n\t\tclient_LimitIP = 0\n\t\tclient_TotalGB = 0\n\t\tclient_ExpiryTime = 0\n\t\tclient_Enable = true\n\t\tclient_TgID = \"\"\n\t\tclient_SubID = t.randomLowerAndNum(16)\n\t\tclient_Comment = \"\"\n\t\tclient_Reset = 0\n\t\tclient_Security = \"auto\"\n\t\tclient_ShPassword = t.randomShadowSocksPassword()\n\t\tclient_TrPassword = t.randomLowerAndNum(10)\n\t\tclient_Method = \"\"\n\n\t\tinbounds, err := t.getInboundsAddClient()\n\t\tif err != nil {\n\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\treturn\n\t\t}\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.buttons.addClient\"))\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.chooseInbound\"), inbounds)\n\tcase \"add_client_ch_default_email\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\tuserStates[chatId] = \"awaiting_email\"\n\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t),\n\t\t)\n\t\tprompt_message := t.I18nBot(\"tgbot.messages.email_prompt\", \"ClientEmail==\"+client_Email)\n\t\tt.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)\n\tcase \"add_client_ch_default_id\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\tuserStates[chatId] = \"awaiting_id\"\n\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t),\n\t\t)\n\t\tprompt_message := t.I18nBot(\"tgbot.messages.id_prompt\", \"ClientId==\"+client_Id)\n\t\tt.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)\n\tcase \"add_client_ch_default_pass_tr\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\tuserStates[chatId] = \"awaiting_password_tr\"\n\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t),\n\t\t)\n\t\tprompt_message := t.I18nBot(\"tgbot.messages.pass_prompt\", \"ClientPassword==\"+client_TrPassword)\n\t\tt.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)\n\tcase \"add_client_ch_default_pass_sh\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\tuserStates[chatId] = \"awaiting_password_sh\"\n\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t),\n\t\t)\n\t\tprompt_message := t.I18nBot(\"tgbot.messages.pass_prompt\", \"ClientPassword==\"+client_ShPassword)\n\t\tt.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)\n\tcase \"add_client_ch_default_comment\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\tuserStates[chatId] = \"awaiting_comment\"\n\t\tcancel_btn_markup := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.use_default\")).WithCallbackData(\"add_client_default_info\"),\n\t\t\t),\n\t\t)\n\t\tprompt_message := t.I18nBot(\"tgbot.messages.comment_prompt\", \"ClientComment==\"+client_Comment)\n\t\tt.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)\n\tcase \"add_client_ch_default_traffic\":\n\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"add_client_default_traffic_exp\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.unlimited\")).WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 0\")),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.custom\")).WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_in 0\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(\"1 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 1\")),\n\t\t\t\ttu.InlineKeyboardButton(\"5 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 5\")),\n\t\t\t\ttu.InlineKeyboardButton(\"10 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 10\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(\"20 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 20\")),\n\t\t\t\ttu.InlineKeyboardButton(\"30 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 30\")),\n\t\t\t\ttu.InlineKeyboardButton(\"40 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 40\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(\"50 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 50\")),\n\t\t\t\ttu.InlineKeyboardButton(\"60 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 60\")),\n\t\t\t\ttu.InlineKeyboardButton(\"80 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 80\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(\"100 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 100\")),\n\t\t\t\ttu.InlineKeyboardButton(\"150 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 150\")),\n\t\t\t\ttu.InlineKeyboardButton(\"200 GB\").WithCallbackData(t.encodeQuery(\"add_client_limit_traffic_c 200\")),\n\t\t\t),\n\t\t)\n\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\tcase \"add_client_ch_default_exp\":\n\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"add_client_default_traffic_exp\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.unlimited\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 0\")),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.custom\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_in 0\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 7 \"+t.I18nBot(\"tgbot.days\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 7\")),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 10 \"+t.I18nBot(\"tgbot.days\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 10\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 14 \"+t.I18nBot(\"tgbot.days\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 14\")),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 20 \"+t.I18nBot(\"tgbot.days\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 20\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 1 \"+t.I18nBot(\"tgbot.month\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 30\")),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 3 \"+t.I18nBot(\"tgbot.months\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 90\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 6 \"+t.I18nBot(\"tgbot.months\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 180\")),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.add\")+\" 12 \"+t.I18nBot(\"tgbot.months\")).WithCallbackData(t.encodeQuery(\"add_client_reset_exp_c 365\")),\n\t\t\t),\n\t\t)\n\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\tcase \"add_client_ch_default_ip_limit\":\n\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(t.encodeQuery(\"add_client_default_ip_limit\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.unlimited\")).WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 0\")),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.custom\")).WithCallbackData(t.encodeQuery(\"add_client_ip_limit_in 0\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(\"1\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 1\")),\n\t\t\t\ttu.InlineKeyboardButton(\"2\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 2\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(\"3\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 3\")),\n\t\t\t\ttu.InlineKeyboardButton(\"4\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 4\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(\"5\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 5\")),\n\t\t\t\ttu.InlineKeyboardButton(\"6\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 6\")),\n\t\t\t\ttu.InlineKeyboardButton(\"7\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 7\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(\"8\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 8\")),\n\t\t\t\ttu.InlineKeyboardButton(\"9\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 9\")),\n\t\t\t\ttu.InlineKeyboardButton(\"10\").WithCallbackData(t.encodeQuery(\"add_client_ip_limit_c 10\")),\n\t\t\t),\n\t\t)\n\t\tt.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)\n\tcase \"add_client_default_info\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\tt.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot(\"tgbot.messages.using_default_value\"), 3, tu.ReplyKeyboardRemove())\n\t\tdelete(userStates, chatId)\n\t\tinbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\tmessage_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\tt.addClient(chatId, message_text)\n\tcase \"add_client_cancel\":\n\t\tdelete(userStates, chatId)\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\tt.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot(\"tgbot.messages.cancel\"), 3, tu.ReplyKeyboardRemove())\n\tcase \"add_client_default_traffic_exp\":\n\t\tmessageId := callbackQuery.Message.GetMessageID()\n\t\tinbound, err := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\tif err != nil {\n\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\treturn\n\t\t}\n\t\tmessage_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\tif err != nil {\n\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\treturn\n\t\t}\n\t\tt.addClient(chatId, message_text, messageId)\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.canceled\", \"Email==\"+client_Email))\n\tcase \"add_client_default_ip_limit\":\n\t\tmessageId := callbackQuery.Message.GetMessageID()\n\t\tinbound, err := t.inboundService.GetInbound(receiver_inbound_ID)\n\t\tif err != nil {\n\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\treturn\n\t\t}\n\t\tmessage_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)\n\t\tif err != nil {\n\t\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())\n\t\t\treturn\n\t\t}\n\t\tt.addClient(chatId, message_text, messageId)\n\t\tt.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot(\"tgbot.answers.canceled\", \"Email==\"+client_Email))\n\tcase \"add_client_submit_disable\":\n\t\tclient_Enable = false\n\t\t_, err := t.SubmitAddClient()\n\t\tif err != nil {\n\t\t\terrorMessage := fmt.Sprintf(\"%v\", err)\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.messages.error_add_client\", \"error==\"+errorMessage), tu.ReplyKeyboardRemove())\n\t\t} else {\n\t\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.successfulOperation\"), tu.ReplyKeyboardRemove())\n\t\t\tt.sendClientIndividualLinks(chatId, client_Email)\n\t\t\tt.sendClientQRLinks(chatId, client_Email)\n\t\t}\n\tcase \"add_client_submit_enable\":\n\t\tclient_Enable = true\n\t\t_, err := t.SubmitAddClient()\n\t\tif err != nil {\n\t\t\terrorMessage := fmt.Sprintf(\"%v\", err)\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.messages.error_add_client\", \"error==\"+errorMessage), tu.ReplyKeyboardRemove())\n\t\t} else {\n\t\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.successfulOperation\"), tu.ReplyKeyboardRemove())\n\t\t\tt.sendClientIndividualLinks(chatId, client_Email)\n\t\t\tt.sendClientQRLinks(chatId, client_Email)\n\t\t}\n\tcase \"reset_all_traffics_cancel\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\tt.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot(\"tgbot.messages.cancel\"), 1, tu.ReplyKeyboardRemove())\n\tcase \"reset_all_traffics\":\n\t\tinlineKeyboard := tu.InlineKeyboard(\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancelReset\")).WithCallbackData(t.encodeQuery(\"reset_all_traffics_cancel\")),\n\t\t\t),\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.confirmResetTraffic\")).WithCallbackData(t.encodeQuery(\"reset_all_traffics_c\")),\n\t\t\t),\n\t\t)\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.messages.AreYouSure\"), inlineKeyboard)\n\tcase \"reset_all_traffics_c\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\temails, err := t.inboundService.getAllEmails()\n\t\tif err != nil {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\"), tu.ReplyKeyboardRemove())\n\t\t\treturn\n\t\t}\n\n\t\tfor _, email := range emails {\n\t\t\terr := t.inboundService.ResetClientTrafficByEmail(email)\n\t\t\tif err == nil {\n\t\t\t\tmsg := t.I18nBot(\"tgbot.messages.SuccessResetTraffic\", \"ClientEmail==\"+email)\n\t\t\t\tt.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove())\n\t\t\t} else {\n\t\t\t\tmsg := t.I18nBot(\"tgbot.messages.FailedResetTraffic\", \"ClientEmail==\"+email, \"ErrorMessage==\"+err.Error())\n\t\t\t\tt.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove())\n\t\t\t}\n\t\t}\n\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.messages.FinishProcess\"), tu.ReplyKeyboardRemove())\n\tcase \"get_sorted_traffic_usage_report\":\n\t\tt.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())\n\t\temails, err := t.inboundService.getAllEmails()\n\n\t\tif err != nil {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\"), tu.ReplyKeyboardRemove())\n\t\t\treturn\n\t\t}\n\t\tvalid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails)\n\t\tif err != nil {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\"), tu.ReplyKeyboardRemove())\n\t\t\treturn\n\t\t}\n\n\t\tfor _, valid_emails := range valid_emails {\n\t\t\ttraffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warning(err)\n\t\t\t\tmsg := t.I18nBot(\"tgbot.wentWrong\")\n\t\t\t\tt.SendMsgToTgbot(chatId, msg)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif traffic == nil {\n\t\t\t\tmsg := t.I18nBot(\"tgbot.noResult\")\n\t\t\t\tt.SendMsgToTgbot(chatId, msg)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\toutput := t.clientInfoMsg(traffic, false, false, false, false, true, false)\n\t\t\tt.SendMsgToTgbot(chatId, output, tu.ReplyKeyboardRemove())\n\t\t}\n\t\tfor _, extra_emails := range extra_emails {\n\t\t\tmsg := fmt.Sprintf(\"📧 %s\\n%s\", extra_emails, t.I18nBot(\"tgbot.noResult\"))\n\t\t\tt.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove())\n\n\t\t}\n\tdefault:\n\t\tif after, ok := strings.CutPrefix(callbackQuery.Data, \"client_sub_links \"); ok {\n\t\t\temail := after\n\t\t\tt.sendClientSubLinks(chatId, email)\n\t\t\treturn\n\t\t}\n\t\tif after, ok := strings.CutPrefix(callbackQuery.Data, \"client_individual_links \"); ok {\n\t\t\temail := after\n\t\t\tt.sendClientIndividualLinks(chatId, email)\n\t\t\treturn\n\t\t}\n\t\tif after, ok := strings.CutPrefix(callbackQuery.Data, \"client_qr_links \"); ok {\n\t\t\temail := after\n\t\t\tt.sendClientQRLinks(chatId, email)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// BuildInboundClientDataMessage builds a message with client data for the given inbound and protocol.\nfunc (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) {\n\tvar message string\n\n\tcurrentTime := time.Now()\n\ttimestampMillis := currentTime.UnixNano() / int64(time.Millisecond)\n\n\texpiryTime := \"\"\n\tdiff := client_ExpiryTime/1000 - timestampMillis\n\tif client_ExpiryTime == 0 {\n\t\texpiryTime = t.I18nBot(\"tgbot.unlimited\")\n\t} else if diff > 172800 {\n\t\texpiryTime = time.Unix((client_ExpiryTime / 1000), 0).Format(\"2006-01-02 15:04:05\")\n\t} else if client_ExpiryTime < 0 {\n\t\texpiryTime = fmt.Sprintf(\"%d %s\", client_ExpiryTime/-86400000, t.I18nBot(\"tgbot.days\"))\n\t} else {\n\t\texpiryTime = fmt.Sprintf(\"%d %s\", diff/3600, t.I18nBot(\"tgbot.hours\"))\n\t}\n\n\ttraffic_value := \"\"\n\tif client_TotalGB == 0 {\n\t\ttraffic_value = \"♾️ Unlimited(Reset)\"\n\t} else {\n\t\ttraffic_value = common.FormatTraffic(client_TotalGB)\n\t}\n\n\tip_limit := \"\"\n\tif client_LimitIP == 0 {\n\t\tip_limit = \"♾️ Unlimited(Reset)\"\n\t} else {\n\t\tip_limit = fmt.Sprint(client_LimitIP)\n\t}\n\n\tswitch protocol {\n\tcase model.VMESS, model.VLESS:\n\t\tmessage = 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)\n\n\tcase model.Trojan:\n\t\tmessage = 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)\n\n\tcase model.Shadowsocks:\n\t\tmessage = 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)\n\n\tdefault:\n\t\treturn \"\", errors.New(\"unknown protocol\")\n\t}\n\n\treturn message, nil\n}\n\n// BuildJSONForProtocol builds a JSON string for the given protocol with client data.\nfunc (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {\n\tvar jsonString string\n\n\tswitch protocol {\n\tcase model.VMESS:\n\t\tjsonString = fmt.Sprintf(`{\n            \"clients\": [{\n                \"id\": \"%s\",\n                \"security\": \"%s\",\n                \"email\": \"%s\",\n                \"limitIp\": %d,\n                \"totalGB\": %d,\n                \"expiryTime\": %d,\n                \"enable\": %t,\n                \"tgId\": \"%s\",\n                \"subId\": \"%s\",\n                \"comment\": \"%s\",\n                \"reset\": %d\n            }]\n        }`, client_Id, client_Security, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)\n\n\tcase model.VLESS:\n\t\tjsonString = fmt.Sprintf(`{\n            \"clients\": [{\n                \"id\": \"%s\",\n                \"flow\": \"%s\",\n                \"email\": \"%s\",\n                \"limitIp\": %d,\n                \"totalGB\": %d,\n                \"expiryTime\": %d,\n                \"enable\": %t,\n                \"tgId\": \"%s\",\n                \"subId\": \"%s\",\n                \"comment\": \"%s\",\n                \"reset\": %d\n            }]\n        }`, client_Id, client_Flow, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)\n\n\tcase model.Trojan:\n\t\tjsonString = fmt.Sprintf(`{\n            \"clients\": [{\n                \"password\": \"%s\",\n                \"email\": \"%s\",\n                \"limitIp\": %d,\n                \"totalGB\": %d,\n                \"expiryTime\": %d,\n                \"enable\": %t,\n                \"tgId\": \"%s\",\n                \"subId\": \"%s\",\n                \"comment\": \"%s\",\n                \"reset\": %d\n            }]\n        }`, client_TrPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)\n\n\tcase model.Shadowsocks:\n\t\tjsonString = fmt.Sprintf(`{\n            \"clients\": [{\n                \"method\": \"%s\",\n                \"password\": \"%s\",\n                \"email\": \"%s\",\n                \"limitIp\": %d,\n                \"totalGB\": %d,\n                \"expiryTime\": %d,\n                \"enable\": %t,\n                \"tgId\": \"%s\",\n                \"subId\": \"%s\",\n                \"comment\": \"%s\",\n                \"reset\": %d\n            }]\n        }`, client_Method, client_ShPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)\n\n\tdefault:\n\t\treturn \"\", errors.New(\"unknown protocol\")\n\t}\n\n\treturn jsonString, nil\n}\n\n// SubmitAddClient submits the client addition request to the inbound service.\nfunc (t *Tgbot) SubmitAddClient() (bool, error) {\n\n\tinbound, err := t.inboundService.GetInbound(receiver_inbound_ID)\n\tif err != nil {\n\t\tlogger.Warning(\"getIboundClients run failed:\", err)\n\t\treturn false, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\n\tjsonString, err := t.BuildJSONForProtocol(inbound.Protocol)\n\tif err != nil {\n\t\tlogger.Warning(\"BuildJSONForProtocol run failed:\", err)\n\t\treturn false, errors.New(\"failed to build JSON for protocol\")\n\t}\n\n\tnewInbound := &model.Inbound{\n\t\tId:       receiver_inbound_ID,\n\t\tSettings: jsonString,\n\t}\n\n\treturn t.inboundService.AddInboundClient(newInbound)\n}\n\n// checkAdmin checks if the given Telegram ID is an admin.\nfunc checkAdmin(tgId int64) bool {\n\tfor _, adminId := range adminIds {\n\t\tif adminId == tgId {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// SendAnswer sends a response message with an inline keyboard to the specified chat.\nfunc (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {\n\tnumericKeyboard := tu.InlineKeyboard(\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.SortedTrafficUsageReport\")).WithCallbackData(t.encodeQuery(\"get_sorted_traffic_usage_report\")),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.serverUsage\")).WithCallbackData(t.encodeQuery(\"get_usage\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.ResetAllTraffics\")).WithCallbackData(t.encodeQuery(\"reset_all_traffics\")),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.dbBackup\")).WithCallbackData(t.encodeQuery(\"get_backup\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.getBanLogs\")).WithCallbackData(t.encodeQuery(\"get_banlogs\")),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.getInbounds\")).WithCallbackData(t.encodeQuery(\"inbounds\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.depleteSoon\")).WithCallbackData(t.encodeQuery(\"deplete_soon\")),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.commands\")).WithCallbackData(t.encodeQuery(\"commands\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.onlines\")).WithCallbackData(t.encodeQuery(\"onlines\")),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.allClients\")).WithCallbackData(t.encodeQuery(\"get_inbounds\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.addClient\")).WithCallbackData(t.encodeQuery(\"add_client\")),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"pages.settings.subSettings\")).WithCallbackData(t.encodeQuery(\"admin_client_sub_links\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"subscription.individualLinks\")).WithCallbackData(t.encodeQuery(\"admin_client_individual_links\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"qrCode\")).WithCallbackData(t.encodeQuery(\"admin_client_qr_links\")),\n\t\t),\n\t\t// TODOOOOOOOOOOOOOO: Add restart button here.\n\t)\n\tnumericKeyboardClient := tu.InlineKeyboard(\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.clientUsage\")).WithCallbackData(t.encodeQuery(\"client_traffic\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.commands\")).WithCallbackData(t.encodeQuery(\"client_commands\")),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"pages.settings.subSettings\")).WithCallbackData(t.encodeQuery(\"client_sub_links\")),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"subscription.individualLinks\")).WithCallbackData(t.encodeQuery(\"client_individual_links\")),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"qrCode\")).WithCallbackData(t.encodeQuery(\"client_qr_links\")),\n\t\t),\n\t)\n\n\tvar ReplyMarkup telego.ReplyMarkup\n\tif isAdmin {\n\t\tReplyMarkup = numericKeyboard\n\t} else {\n\t\tReplyMarkup = numericKeyboardClient\n\t}\n\tt.SendMsgToTgbot(chatId, msg, ReplyMarkup)\n}\n\n// SendMsgToTgbot sends a message to the Telegram bot with optional reply markup.\nfunc (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {\n\tif !isRunning {\n\t\treturn\n\t}\n\n\tif msg == \"\" {\n\t\tlogger.Info(\"[tgbot] message is empty!\")\n\t\treturn\n\t}\n\n\tvar allMessages []string\n\tlimit := 2000\n\n\t// paging message if it is big\n\tif len(msg) > limit {\n\t\tmessages := strings.Split(msg, \"\\r\\n\\r\\n\")\n\t\tlastIndex := -1\n\n\t\tfor _, message := range messages {\n\t\t\tif (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {\n\t\t\t\tallMessages = append(allMessages, message)\n\t\t\t\tlastIndex++\n\t\t\t} else {\n\t\t\t\tallMessages[lastIndex] += \"\\r\\n\\r\\n\" + message\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(allMessages[len(allMessages)-1]) == \"\" {\n\t\t\tallMessages = allMessages[:len(allMessages)-1]\n\t\t}\n\t} else {\n\t\tallMessages = append(allMessages, msg)\n\t}\n\tfor n, message := range allMessages {\n\t\tparams := telego.SendMessageParams{\n\t\t\tChatID:    tu.ID(chatId),\n\t\t\tText:      message,\n\t\t\tParseMode: \"HTML\",\n\t\t}\n\t\t// only add replyMarkup to last message\n\t\tif len(replyMarkup) > 0 && n == (len(allMessages)-1) {\n\t\t\tparams.ReplyMarkup = replyMarkup[0]\n\t\t}\n\n\t\t// Retry logic with exponential backoff for connection errors\n\t\tmaxRetries := 3\n\t\tfor attempt := range maxRetries {\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\t\t\t_, err := bot.SendMessage(ctx, &params)\n\t\t\tcancel()\n\n\t\t\tif err == nil {\n\t\t\t\tbreak // Success\n\t\t\t}\n\n\t\t\t// Check if error is a connection error\n\t\t\terrStr := err.Error()\n\t\t\tisConnectionError := strings.Contains(errStr, \"connection\") ||\n\t\t\t\tstrings.Contains(errStr, \"timeout\") ||\n\t\t\t\tstrings.Contains(errStr, \"closed\")\n\n\t\t\tif isConnectionError && attempt < maxRetries-1 {\n\t\t\t\t// Exponential backoff: 1s, 2s, 4s\n\t\t\t\tbackoff := time.Duration(1<<uint(attempt)) * time.Second\n\t\t\t\tlogger.Warningf(\"Connection error sending telegram message (attempt %d/%d), retrying in %v: %v\",\n\t\t\t\t\tattempt+1, maxRetries, backoff, err)\n\t\t\t\ttime.Sleep(backoff)\n\t\t\t} else {\n\t\t\t\tlogger.Warning(\"Error sending telegram message:\", err)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Reduced delay to improve performance (only needed for rate limiting)\n\t\tif n < len(allMessages)-1 { // Only delay between messages, not after the last one\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t}\n\t}\n}\n\n// buildSubscriptionURLs builds the HTML sub page URL and JSON subscription URL for a client email\nfunc (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {\n\t// Resolve subId from client email\n\ttraffic, client, err := t.inboundService.GetClientByEmail(email)\n\t_ = traffic\n\tif err != nil || client == nil {\n\t\treturn \"\", \"\", errors.New(\"client not found\")\n\t}\n\n\t// Gather settings to construct absolute URLs\n\tsubURI, _ := t.settingService.GetSubURI()\n\tsubJsonURI, _ := t.settingService.GetSubJsonURI()\n\tsubDomain, _ := t.settingService.GetSubDomain()\n\tsubPort, _ := t.settingService.GetSubPort()\n\tsubPath, _ := t.settingService.GetSubPath()\n\tsubJsonPath, _ := t.settingService.GetSubJsonPath()\n\tsubJsonEnable, _ := t.settingService.GetSubJsonEnable()\n\tsubKeyFile, _ := t.settingService.GetSubKeyFile()\n\tsubCertFile, _ := t.settingService.GetSubCertFile()\n\n\ttls := (subKeyFile != \"\" && subCertFile != \"\")\n\tscheme := \"http\"\n\tif tls {\n\t\tscheme = \"https\"\n\t}\n\n\t// Fallbacks\n\tif subDomain == \"\" {\n\t\t// try panel domain, otherwise OS hostname\n\t\tif d, err := t.settingService.GetWebDomain(); err == nil && d != \"\" {\n\t\t\tsubDomain = d\n\t\t} else if hostname != \"\" {\n\t\t\tsubDomain = hostname\n\t\t} else {\n\t\t\tsubDomain = \"localhost\"\n\t\t}\n\t}\n\n\thost := subDomain\n\tif (subPort == 443 && tls) || (subPort == 80 && !tls) {\n\t\t// standard ports: no port in host\n\t} else {\n\t\thost = fmt.Sprintf(\"%s:%d\", subDomain, subPort)\n\t}\n\n\t// Ensure paths\n\tif !strings.HasPrefix(subPath, \"/\") {\n\t\tsubPath = \"/\" + subPath\n\t}\n\tif !strings.HasSuffix(subPath, \"/\") {\n\t\tsubPath = subPath + \"/\"\n\t}\n\tif !strings.HasPrefix(subJsonPath, \"/\") {\n\t\tsubJsonPath = \"/\" + subJsonPath\n\t}\n\tif !strings.HasSuffix(subJsonPath, \"/\") {\n\t\tsubJsonPath = subJsonPath + \"/\"\n\t}\n\n\tvar subURL string\n\tvar subJsonURL string\n\n\t// If pre-configured URIs are available, use them directly\n\tif subURI != \"\" {\n\t\tif !strings.HasSuffix(subURI, \"/\") {\n\t\t\tsubURI = subURI + \"/\"\n\t\t}\n\t\tsubURL = fmt.Sprintf(\"%s%s\", subURI, client.SubID)\n\t} else {\n\t\tsubURL = fmt.Sprintf(\"%s://%s%s%s\", scheme, host, subPath, client.SubID)\n\t}\n\n\tif subJsonURI != \"\" {\n\t\tif !strings.HasSuffix(subJsonURI, \"/\") {\n\t\t\tsubJsonURI = subJsonURI + \"/\"\n\t\t}\n\t\tsubJsonURL = fmt.Sprintf(\"%s%s\", subJsonURI, client.SubID)\n\t} else {\n\n\t\tsubJsonURL = fmt.Sprintf(\"%s://%s%s%s\", scheme, host, subJsonPath, client.SubID)\n\t}\n\n\tif !subJsonEnable {\n\t\tsubJsonURL = \"\"\n\t}\n\treturn subURL, subJsonURL, nil\n}\n\n// sendClientSubLinks sends the subscription links for the client to the chat.\nfunc (t *Tgbot) sendClientSubLinks(chatId int64, email string) {\n\tsubURL, subJsonURL, err := t.buildSubscriptionURLs(email)\n\tif err != nil {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\treturn\n\t}\n\tmsg := \"Subscription URL:\\r\\n<code>\" + subURL + \"</code>\"\n\tif subJsonURL != \"\" {\n\t\tmsg += \"\\r\\n\\r\\nJSON URL:\\r\\n<code>\" + subJsonURL + \"</code>\"\n\t}\n\tinlineKeyboard := tu.InlineKeyboard(\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"subscription.individualLinks\")).WithCallbackData(t.encodeQuery(\"client_individual_links \"+email)),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"qrCode\")).WithCallbackData(t.encodeQuery(\"client_qr_links \"+email)),\n\t\t),\n\t)\n\tt.SendMsgToTgbot(chatId, msg, inlineKeyboard)\n}\n\n// sendClientIndividualLinks fetches the subscription content (individual links) and sends it to the user\nfunc (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {\n\t// Build the HTML sub page URL; we'll call it with header Accept to get raw content\n\tsubURL, _, err := t.buildSubscriptionURLs(email)\n\tif err != nil {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\treturn\n\t}\n\n\t// Try to fetch raw subscription links. Prefer plain text response.\n\treq, err := http.NewRequest(\"GET\", subURL, nil)\n\tif err != nil {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\treturn\n\t}\n\t// Force plain text to avoid HTML page; controller respects Accept header\n\treq.Header.Set(\"Accept\", \"text/plain, */*;q=0.1\")\n\n\t// Use optimized client with connection pooling\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\treq = req.WithContext(ctx)\n\n\tresp, err := optimizedHTTPClient.Do(req)\n\tif err != nil {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbodyBytes, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\treturn\n\t}\n\n\t// If service is configured to encode (Base64), decode it\n\tencoded, _ := t.settingService.GetSubEncrypt()\n\tvar content string\n\tif encoded {\n\t\tdecoded, err := base64.StdEncoding.DecodeString(string(bodyBytes))\n\t\tif err != nil {\n\t\t\t// fallback to raw text\n\t\t\tcontent = string(bodyBytes)\n\t\t} else {\n\t\t\tcontent = string(decoded)\n\t\t}\n\t} else {\n\t\tcontent = string(bodyBytes)\n\t}\n\n\t// Normalize line endings and trim\n\tlines := strings.Split(strings.ReplaceAll(content, \"\\r\\n\", \"\\n\"), \"\\n\")\n\tvar cleaned []string\n\tfor _, l := range lines {\n\t\tl = strings.TrimSpace(l)\n\t\tif l != \"\" {\n\t\t\tcleaned = append(cleaned, l)\n\t\t}\n\t}\n\tif len(cleaned) == 0 {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.noResult\"))\n\t\treturn\n\t}\n\n\t// Send in chunks to respect message length; use monospace formatting\n\tconst maxPerMessage = 50\n\tfor i := 0; i < len(cleaned); i += maxPerMessage {\n\t\tj := i + maxPerMessage\n\t\tif j > len(cleaned) {\n\t\t\tj = len(cleaned)\n\t\t}\n\t\tchunk := cleaned[i:j]\n\t\tmsg := t.I18nBot(\"subscription.individualLinks\") + \":\\r\\n\"\n\t\tfor _, link := range chunk {\n\t\t\t// wrap each link in <code>\n\t\t\tmsg += \"<code>\" + link + \"</code>\\r\\n\"\n\t\t}\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t}\n}\n\n// sendClientQRLinks generates QR images for subscription URL, JSON URL, and a few individual links, then sends them\nfunc (t *Tgbot) sendClientQRLinks(chatId int64, email string) {\n\tsubURL, subJsonURL, err := t.buildSubscriptionURLs(email)\n\tif err != nil {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\treturn\n\t}\n\n\t// Helper to create QR PNG bytes from content\n\tcreateQR := func(content string, size int) ([]byte, error) {\n\t\tif size <= 0 {\n\t\t\tsize = 256\n\t\t}\n\t\treturn qrcode.Encode(content, qrcode.Medium, size)\n\t}\n\n\t// Inform user\n\tt.SendMsgToTgbot(chatId, \"QRCode\"+\":\")\n\n\t// Send sub URL QR (filename: sub.png)\n\tif png, err := createQR(subURL, 320); err == nil {\n\t\tdocument := tu.Document(\n\t\t\ttu.ID(chatId),\n\t\t\ttu.FileFromBytes(png, \"sub.png\"),\n\t\t)\n\t\t_, _ = bot.SendDocument(context.Background(), document)\n\t} else {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t}\n\n\t// Send JSON URL QR (filename: subjson.png) when available\n\tif subJsonURL != \"\" {\n\t\tif png, err := createQR(subJsonURL, 320); err == nil {\n\t\t\tdocument := tu.Document(\n\t\t\t\ttu.ID(chatId),\n\t\t\t\ttu.FileFromBytes(png, \"subjson.png\"),\n\t\t\t)\n\t\t\t_, _ = bot.SendDocument(context.Background(), document)\n\t\t} else {\n\t\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.errorOperation\")+\"\\r\\n\"+err.Error())\n\t\t}\n\t}\n\n\t// Also generate a few individual links' QRs (first up to 5)\n\tsubPageURL := subURL\n\treq, err := http.NewRequest(\"GET\", subPageURL, nil)\n\tif err == nil {\n\t\treq.Header.Set(\"Accept\", \"text/plain, */*;q=0.1\")\n\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\t\treq = req.WithContext(ctx)\n\t\tif resp, err := optimizedHTTPClient.Do(req); err == nil {\n\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\t_ = resp.Body.Close()\n\t\t\tencoded, _ := t.settingService.GetSubEncrypt()\n\t\t\tvar content string\n\t\t\tif encoded {\n\t\t\t\tif dec, err := base64.StdEncoding.DecodeString(string(body)); err == nil {\n\t\t\t\t\tcontent = string(dec)\n\t\t\t\t} else {\n\t\t\t\t\tcontent = string(body)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcontent = string(body)\n\t\t\t}\n\t\t\tlines := strings.Split(strings.ReplaceAll(content, \"\\r\\n\", \"\\n\"), \"\\n\")\n\t\t\tvar cleaned []string\n\t\t\tfor _, l := range lines {\n\t\t\t\tl = strings.TrimSpace(l)\n\t\t\t\tif l != \"\" {\n\t\t\t\t\tcleaned = append(cleaned, l)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(cleaned) > 0 {\n\t\t\t\tmax := min(len(cleaned), 5)\n\t\t\t\tfor i := range max {\n\t\t\t\t\tif png, err := createQR(cleaned[i], 320); err == nil {\n\t\t\t\t\t\t// Use the email as filename for individual link QR\n\t\t\t\t\t\tfilename := email + \".png\"\n\t\t\t\t\t\tdocument := tu.Document(\n\t\t\t\t\t\t\ttu.ID(chatId),\n\t\t\t\t\t\t\ttu.FileFromBytes(png, filename),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t_, _ = bot.SendDocument(context.Background(), document)\n\t\t\t\t\t\t// Reduced delay for better performance\n\t\t\t\t\t\tif i < max-1 { // Only delay between documents, not after the last one\n\t\t\t\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// SendMsgToTgbotAdmins sends a message to all admin Telegram chats.\nfunc (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {\n\tif len(replyMarkup) > 0 {\n\t\tfor _, adminId := range adminIds {\n\t\t\tt.SendMsgToTgbot(adminId, msg, replyMarkup[0])\n\t\t}\n\t} else {\n\t\tfor _, adminId := range adminIds {\n\t\t\tt.SendMsgToTgbot(adminId, msg)\n\t\t}\n\t}\n}\n\n// SendReport sends a periodic report to admin chats.\nfunc (t *Tgbot) SendReport() {\n\trunTime, err := t.settingService.GetTgbotRuntime()\n\tif err == nil && len(runTime) > 0 {\n\t\tmsg := \"\"\n\t\tmsg += t.I18nBot(\"tgbot.messages.report\", \"RunTime==\"+runTime)\n\t\tmsg += t.I18nBot(\"tgbot.messages.datetime\", \"DateTime==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\t\tt.SendMsgToTgbotAdmins(msg)\n\t}\n\n\tinfo := t.sendServerUsage()\n\tt.SendMsgToTgbotAdmins(info)\n\n\tt.sendExhaustedToAdmins()\n\tt.notifyExhausted()\n\n\tbackupEnable, err := t.settingService.GetTgBotBackup()\n\tif err == nil && backupEnable {\n\t\tt.SendBackupToAdmins()\n\t}\n}\n\n// SendBackupToAdmins sends a database backup to admin chats.\nfunc (t *Tgbot) SendBackupToAdmins() {\n\tif !t.IsRunning() {\n\t\treturn\n\t}\n\tfor i, adminId := range adminIds {\n\t\tt.sendBackup(int64(adminId))\n\t\t// Add delay between sends to avoid Telegram rate limits\n\t\tif i < len(adminIds)-1 {\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}\n\t}\n}\n\n// sendExhaustedToAdmins sends notifications about exhausted clients to admins.\nfunc (t *Tgbot) sendExhaustedToAdmins() {\n\tif !t.IsRunning() {\n\t\treturn\n\t}\n\tfor _, adminId := range adminIds {\n\t\tt.getExhausted(int64(adminId))\n\t}\n}\n\n// getServerUsage retrieves and formats server usage information.\nfunc (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string {\n\tinfo := t.prepareServerUsageInfo()\n\n\tkeyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(\n\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.refresh\")).WithCallbackData(t.encodeQuery(\"usage_refresh\"))))\n\n\tif len(messageID) > 0 {\n\t\tt.editMessageTgBot(chatId, messageID[0], info, keyboard)\n\t} else {\n\t\tt.SendMsgToTgbot(chatId, info, keyboard)\n\t}\n\n\treturn info\n}\n\n// Send server usage without an inline keyboard\nfunc (t *Tgbot) sendServerUsage() string {\n\tinfo := t.prepareServerUsageInfo()\n\treturn info\n}\n\n// prepareServerUsageInfo prepares the server usage information string.\nfunc (t *Tgbot) prepareServerUsageInfo() string {\n\t// Check if we have cached data first\n\tif cachedStats, found := t.getCachedServerStats(); found {\n\t\treturn cachedStats\n\t}\n\n\tinfo, ipv4, ipv6 := \"\", \"\", \"\"\n\n\t// get latest status of server with caching\n\tif cachedStatus, found := t.getCachedStatus(); found {\n\t\tt.lastStatus = cachedStatus\n\t} else {\n\t\tt.lastStatus = t.serverService.GetStatus(t.lastStatus)\n\t\tt.setCachedStatus(t.lastStatus)\n\t}\n\tonlines := p.GetOnlineClients()\n\n\tinfo += t.I18nBot(\"tgbot.messages.hostname\", \"Hostname==\"+hostname)\n\tinfo += t.I18nBot(\"tgbot.messages.version\", \"Version==\"+config.GetVersion())\n\tinfo += t.I18nBot(\"tgbot.messages.xrayVersion\", \"XrayVersion==\"+fmt.Sprint(t.lastStatus.Xray.Version))\n\n\t// get ip address\n\tnetInterfaces, err := net.Interfaces()\n\tif err != nil {\n\t\tlogger.Error(\"net.Interfaces failed, err: \", err.Error())\n\t\tinfo += t.I18nBot(\"tgbot.messages.ip\", \"IP==\"+t.I18nBot(\"tgbot.unknown\"))\n\t\tinfo += \"\\r\\n\"\n\t} else {\n\t\tfor i := range netInterfaces {\n\t\t\tif (netInterfaces[i].Flags & net.FlagUp) != 0 {\n\t\t\t\taddrs, _ := netInterfaces[i].Addrs()\n\n\t\t\t\tfor _, address := range addrs {\n\t\t\t\t\tif ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {\n\t\t\t\t\t\tif ipnet.IP.To4() != nil {\n\t\t\t\t\t\t\tipv4 += ipnet.IP.String() + \" \"\n\t\t\t\t\t\t} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {\n\t\t\t\t\t\t\tipv6 += ipnet.IP.String() + \" \"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tinfo += t.I18nBot(\"tgbot.messages.ipv4\", \"IPv4==\"+ipv4)\n\t\tinfo += t.I18nBot(\"tgbot.messages.ipv6\", \"IPv6==\"+ipv6)\n\t}\n\n\tinfo += t.I18nBot(\"tgbot.messages.serverUpTime\", \"UpTime==\"+strconv.FormatUint(t.lastStatus.Uptime/86400, 10), \"Unit==\"+t.I18nBot(\"tgbot.days\"))\n\tinfo += 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))\n\tinfo += t.I18nBot(\"tgbot.messages.serverMemory\", \"Current==\"+common.FormatTraffic(int64(t.lastStatus.Mem.Current)), \"Total==\"+common.FormatTraffic(int64(t.lastStatus.Mem.Total)))\n\tinfo += t.I18nBot(\"tgbot.messages.onlinesCount\", \"Count==\"+fmt.Sprint(len(onlines)))\n\tinfo += t.I18nBot(\"tgbot.messages.tcpCount\", \"Count==\"+strconv.Itoa(t.lastStatus.TcpCount))\n\tinfo += t.I18nBot(\"tgbot.messages.udpCount\", \"Count==\"+strconv.Itoa(t.lastStatus.UdpCount))\n\tinfo += 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)))\n\tinfo += t.I18nBot(\"tgbot.messages.xrayStatus\", \"State==\"+fmt.Sprint(t.lastStatus.Xray.State))\n\n\t// Cache the complete server stats\n\tt.setCachedServerStats(info)\n\n\treturn info\n}\n\n// UserLoginNotify sends a notification about user login attempts to admins.\nfunc (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) {\n\tif !t.IsRunning() {\n\t\treturn\n\t}\n\n\tif username == \"\" || ip == \"\" || time == \"\" {\n\t\tlogger.Warning(\"UserLoginNotify failed, invalid info!\")\n\t\treturn\n\t}\n\n\tloginNotifyEnabled, err := t.settingService.GetTgBotLoginNotify()\n\tif err != nil || !loginNotifyEnabled {\n\t\treturn\n\t}\n\n\tmsg := \"\"\n\tswitch status {\n\tcase LoginSuccess:\n\t\tmsg += t.I18nBot(\"tgbot.messages.loginSuccess\")\n\t\tmsg += t.I18nBot(\"tgbot.messages.hostname\", \"Hostname==\"+hostname)\n\tcase LoginFail:\n\t\tmsg += t.I18nBot(\"tgbot.messages.loginFailed\")\n\t\tmsg += t.I18nBot(\"tgbot.messages.hostname\", \"Hostname==\"+hostname)\n\t\tmsg += t.I18nBot(\"tgbot.messages.password\", \"Password==\"+password)\n\t}\n\tmsg += t.I18nBot(\"tgbot.messages.username\", \"Username==\"+username)\n\tmsg += t.I18nBot(\"tgbot.messages.ip\", \"IP==\"+ip)\n\tmsg += t.I18nBot(\"tgbot.messages.time\", \"Time==\"+time)\n\tt.SendMsgToTgbotAdmins(msg)\n}\n\n// getInboundUsages retrieves and formats inbound usage information.\nfunc (t *Tgbot) getInboundUsages() string {\n\tvar info strings.Builder\n\t// get traffic\n\tinbounds, err := t.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"GetAllInbounds run failed:\", err)\n\t\tinfo.WriteString(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t} else {\n\t\t// NOTE:If there no any sessions here,need to notify here\n\t\t// TODO:Sub-node push, automatic conversion format\n\t\tfor _, inbound := range inbounds {\n\t\t\tinfo.WriteString(t.I18nBot(\"tgbot.messages.inbound\", \"Remark==\"+inbound.Remark))\n\t\t\tinfo.WriteString(t.I18nBot(\"tgbot.messages.port\", \"Port==\"+strconv.Itoa(inbound.Port)))\n\t\t\tinfo.WriteString(t.I18nBot(\"tgbot.messages.traffic\", \"Total==\"+common.FormatTraffic((inbound.Up+inbound.Down)), \"Upload==\"+common.FormatTraffic(inbound.Up), \"Download==\"+common.FormatTraffic(inbound.Down)))\n\n\t\t\tif inbound.ExpiryTime == 0 {\n\t\t\t\tinfo.WriteString(t.I18nBot(\"tgbot.messages.expire\", \"Time==\"+t.I18nBot(\"tgbot.unlimited\")))\n\t\t\t} else {\n\t\t\t\tinfo.WriteString(t.I18nBot(\"tgbot.messages.expire\", \"Time==\"+time.Unix((inbound.ExpiryTime/1000), 0).Format(\"2006-01-02 15:04:05\")))\n\t\t\t}\n\t\t\tinfo.WriteString(\"\\r\\n\")\n\t\t}\n\t}\n\treturn info.String()\n}\n\n// getInbounds creates an inline keyboard with all inbounds.\nfunc (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {\n\tinbounds, err := t.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"GetAllInbounds run failed:\", err)\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\n\tif len(inbounds) == 0 {\n\t\tlogger.Warning(\"No inbounds found\")\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\n\tvar buttons []telego.InlineKeyboardButton\n\tfor _, inbound := range inbounds {\n\t\tstatus := \"❌\"\n\t\tif inbound.Enable {\n\t\t\tstatus = \"✅\"\n\t\t}\n\t\tcallbackData := t.encodeQuery(fmt.Sprintf(\"%s %d\", \"get_clients\", inbound.Id))\n\t\tbuttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf(\"%v - %v\", inbound.Remark, status)).WithCallbackData(callbackData))\n\t}\n\n\tcols := 1\n\tif len(buttons) >= 6 {\n\t\tcols = 2\n\t}\n\n\tkeyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))\n\treturn keyboard, nil\n}\n\n// getInboundsFor builds an inline keyboard of inbounds for a custom next action.\nfunc (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) {\n\tinbounds, err := t.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"GetAllInbounds run failed:\", err)\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\n\tif len(inbounds) == 0 {\n\t\tlogger.Warning(\"No inbounds found\")\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\n\tvar buttons []telego.InlineKeyboardButton\n\tfor _, inbound := range inbounds {\n\t\tstatus := \"❌\"\n\t\tif inbound.Enable {\n\t\t\tstatus = \"✅\"\n\t\t}\n\t\tcallbackData := t.encodeQuery(fmt.Sprintf(\"%s %d\", nextAction, inbound.Id))\n\t\tbuttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf(\"%v - %v\", inbound.Remark, status)).WithCallbackData(callbackData))\n\t}\n\n\tcols := 1\n\tif len(buttons) >= 6 {\n\t\tcols = 2\n\t}\n\n\tkeyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))\n\treturn keyboard, nil\n}\n\n// getInboundClientsFor lists clients of an inbound with a specific action prefix to be appended with email\nfunc (t *Tgbot) getInboundClientsFor(inboundID int, action string) (*telego.InlineKeyboardMarkup, error) {\n\tinbound, err := t.inboundService.GetInbound(inboundID)\n\tif err != nil {\n\t\tlogger.Warning(\"getInboundClientsFor run failed:\", err)\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\tclients, err := t.inboundService.GetClients(inbound)\n\tvar buttons []telego.InlineKeyboardButton\n\n\tif err != nil {\n\t\tlogger.Warning(\"GetInboundClients run failed:\", err)\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t} else {\n\t\tif len(clients) > 0 {\n\t\t\tfor _, client := range clients {\n\t\t\t\tbuttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery(action+\" \"+client.Email)))\n\t\t\t}\n\n\t\t} else {\n\t\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getClientsFailed\"))\n\t\t}\n\n\t}\n\tcols := 0\n\tif len(buttons) < 6 {\n\t\tcols = 3\n\t} else {\n\t\tcols = 2\n\t}\n\tkeyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))\n\n\treturn keyboard, nil\n}\n\n// getInboundsAddClient creates an inline keyboard for adding clients to inbounds.\nfunc (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {\n\tinbounds, err := t.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"GetAllInbounds run failed:\", err)\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\n\tif len(inbounds) == 0 {\n\t\tlogger.Warning(\"No inbounds found\")\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\n\texcludedProtocols := map[model.Protocol]bool{\n\t\tmodel.Tunnel:    true,\n\t\tmodel.Mixed:     true,\n\t\tmodel.WireGuard: true,\n\t\tmodel.HTTP:      true,\n\t}\n\n\tvar buttons []telego.InlineKeyboardButton\n\tfor _, inbound := range inbounds {\n\t\tif excludedProtocols[inbound.Protocol] {\n\t\t\tcontinue\n\t\t}\n\n\t\tstatus := \"❌\"\n\t\tif inbound.Enable {\n\t\t\tstatus = \"✅\"\n\t\t}\n\t\tcallbackData := t.encodeQuery(fmt.Sprintf(\"%s %d\", \"add_client_to\", inbound.Id))\n\t\tbuttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf(\"%v - %v\", inbound.Remark, status)).WithCallbackData(callbackData))\n\t}\n\n\tcols := 1\n\tif len(buttons) >= 6 {\n\t\tcols = 2\n\t}\n\n\tkeyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))\n\treturn keyboard, nil\n}\n\n// getInboundClients creates an inline keyboard with clients of a specific inbound.\nfunc (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {\n\tinbound, err := t.inboundService.GetInbound(id)\n\tif err != nil {\n\t\tlogger.Warning(\"getIboundClients run failed:\", err)\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t}\n\tclients, err := t.inboundService.GetClients(inbound)\n\tvar buttons []telego.InlineKeyboardButton\n\n\tif err != nil {\n\t\tlogger.Warning(\"GetInboundClients run failed:\", err)\n\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getInboundsFailed\"))\n\t} else {\n\t\tif len(clients) > 0 {\n\t\t\tfor _, client := range clients {\n\t\t\t\tbuttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery(\"client_get_usage \"+client.Email)))\n\t\t\t}\n\n\t\t} else {\n\t\t\treturn nil, errors.New(t.I18nBot(\"tgbot.answers.getClientsFailed\"))\n\t\t}\n\n\t}\n\tcols := 0\n\tif len(buttons) < 6 {\n\t\tcols = 3\n\t} else {\n\t\tcols = 2\n\t}\n\tkeyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))\n\n\treturn keyboard, nil\n}\n\n// clientInfoMsg formats client information message based on traffic and flags.\nfunc (t *Tgbot) clientInfoMsg(\n\ttraffic *xray.ClientTraffic,\n\tprintEnabled bool,\n\tprintOnline bool,\n\tprintActive bool,\n\tprintDate bool,\n\tprintTraffic bool,\n\tprintRefreshed bool,\n) string {\n\tnow := time.Now().Unix()\n\texpiryTime := \"\"\n\tflag := false\n\tdiff := traffic.ExpiryTime/1000 - now\n\tif traffic.ExpiryTime == 0 {\n\t\texpiryTime = t.I18nBot(\"tgbot.unlimited\")\n\t} else if diff > 172800 || !traffic.Enable {\n\t\texpiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format(\"2006-01-02 15:04:05\")\n\t\tif diff > 0 {\n\t\t\tdays := diff / 86400\n\t\t\thours := (diff % 86400) / 3600\n\t\t\tminutes := (diff % 3600) / 60\n\t\t\tremainingTime := \"\"\n\t\t\tif days > 0 {\n\t\t\t\tremainingTime += fmt.Sprintf(\"%d %s \", days, t.I18nBot(\"tgbot.days\"))\n\t\t\t}\n\t\t\tif hours > 0 {\n\t\t\t\tremainingTime += fmt.Sprintf(\"%d %s \", hours, t.I18nBot(\"tgbot.hours\"))\n\t\t\t}\n\t\t\tif minutes > 0 {\n\t\t\t\tremainingTime += fmt.Sprintf(\"%d %s\", minutes, t.I18nBot(\"tgbot.minutes\"))\n\t\t\t}\n\t\t\texpiryTime += fmt.Sprintf(\" (%s)\", remainingTime)\n\t\t}\n\t} else if traffic.ExpiryTime < 0 {\n\t\texpiryTime = fmt.Sprintf(\"%d %s\", traffic.ExpiryTime/-86400000, t.I18nBot(\"tgbot.days\"))\n\t\tflag = true\n\t} else {\n\t\texpiryTime = fmt.Sprintf(\"%d %s\", diff/3600, t.I18nBot(\"tgbot.hours\"))\n\t\tflag = true\n\t}\n\n\ttotal := \"\"\n\tif traffic.Total == 0 {\n\t\ttotal = t.I18nBot(\"tgbot.unlimited\")\n\t} else {\n\t\ttotal = common.FormatTraffic((traffic.Total))\n\t}\n\n\tenabled := \"\"\n\tisEnabled, err := t.inboundService.checkIsEnabledByEmail(traffic.Email)\n\tif err != nil {\n\t\tlogger.Warning(err)\n\t\tenabled = t.I18nBot(\"tgbot.wentWrong\")\n\t} else if isEnabled {\n\t\tenabled = t.I18nBot(\"tgbot.messages.yes\")\n\t} else {\n\t\tenabled = t.I18nBot(\"tgbot.messages.no\")\n\t}\n\n\tactive := \"\"\n\tif traffic.Enable {\n\t\tactive = t.I18nBot(\"tgbot.messages.yes\")\n\t} else {\n\t\tactive = t.I18nBot(\"tgbot.messages.no\")\n\t}\n\n\tstatus := t.I18nBot(\"tgbot.offline\")\n\tisOnline := false\n\tif p.IsRunning() {\n\t\tif slices.Contains(p.GetOnlineClients(), traffic.Email) {\n\t\t\tstatus = t.I18nBot(\"tgbot.online\")\n\t\t\tisOnline = true\n\t\t}\n\t}\n\n\toutput := \"\"\n\toutput += t.I18nBot(\"tgbot.messages.email\", \"Email==\"+traffic.Email)\n\tif printEnabled {\n\t\toutput += t.I18nBot(\"tgbot.messages.enabled\", \"Enable==\"+enabled)\n\t}\n\tif printOnline {\n\t\toutput += t.I18nBot(\"tgbot.messages.online\", \"Status==\"+status)\n\t\tif !isOnline && traffic.LastOnline > 0 {\n\t\t\toutput += t.I18nBot(\"tgbot.messages.lastOnline\", \"Time==\"+time.UnixMilli(traffic.LastOnline).Format(\"2006-01-02 15:04:05\"))\n\t\t}\n\t}\n\tif printActive {\n\t\toutput += t.I18nBot(\"tgbot.messages.active\", \"Enable==\"+active)\n\t}\n\tif printDate {\n\t\tif flag {\n\t\t\toutput += t.I18nBot(\"tgbot.messages.expireIn\", \"Time==\"+expiryTime)\n\t\t} else {\n\t\t\toutput += t.I18nBot(\"tgbot.messages.expire\", \"Time==\"+expiryTime)\n\t\t}\n\t}\n\tif printTraffic {\n\t\toutput += t.I18nBot(\"tgbot.messages.upload\", \"Upload==\"+common.FormatTraffic(traffic.Up))\n\t\toutput += t.I18nBot(\"tgbot.messages.download\", \"Download==\"+common.FormatTraffic(traffic.Down))\n\t\toutput += t.I18nBot(\"tgbot.messages.total\", \"UpDown==\"+common.FormatTraffic((traffic.Up+traffic.Down)), \"Total==\"+total)\n\t}\n\tif printRefreshed {\n\t\toutput += t.I18nBot(\"tgbot.messages.refreshedOn\", \"Time==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\t}\n\n\treturn output\n}\n\n// getClientUsage retrieves and sends client usage information to the chat.\nfunc (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {\n\ttraffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)\n\tif err != nil {\n\t\tlogger.Warning(err)\n\t\tmsg := t.I18nBot(\"tgbot.wentWrong\")\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t\treturn\n\t}\n\n\tif len(traffics) == 0 {\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.answers.askToAddUserId\", \"TgUserID==\"+strconv.FormatInt(tgUserID, 10)))\n\t\treturn\n\t}\n\n\toutput := \"\"\n\n\tif len(traffics) > 0 {\n\t\tif len(email) > 0 {\n\t\t\tfor _, traffic := range traffics {\n\t\t\t\tif traffic.Email == email[0] {\n\t\t\t\t\toutput := t.clientInfoMsg(traffic, true, true, true, true, true, true)\n\t\t\t\t\tt.SendMsgToTgbot(chatId, output)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tmsg := t.I18nBot(\"tgbot.noResult\")\n\t\t\tt.SendMsgToTgbot(chatId, msg)\n\t\t\treturn\n\t\t} else {\n\t\t\tfor _, traffic := range traffics {\n\t\t\t\toutput += t.clientInfoMsg(traffic, true, true, true, true, true, false)\n\t\t\t\toutput += \"\\r\\n\"\n\t\t\t}\n\t\t}\n\t}\n\n\toutput += t.I18nBot(\"tgbot.messages.refreshedOn\", \"Time==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\tt.SendMsgToTgbot(chatId, output)\n\toutput = t.I18nBot(\"tgbot.commands.pleaseChoose\")\n\tt.SendAnswer(chatId, output, false)\n}\n\n// searchClientIps searches and sends client IP addresses for the given email.\nfunc (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {\n\tips, err := t.inboundService.GetInboundClientIps(email)\n\tif err != nil || len(ips) == 0 {\n\t\tips = t.I18nBot(\"tgbot.noIpRecord\")\n\t}\n\n\tformattedIps := ips\n\tif err == nil && len(ips) > 0 {\n\t\ttype ipWithTimestamp struct {\n\t\t\tIP        string `json:\"ip\"`\n\t\t\tTimestamp int64  `json:\"timestamp\"`\n\t\t}\n\n\t\tvar ipsWithTime []ipWithTimestamp\n\t\tif json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 {\n\t\t\tlines := make([]string, 0, len(ipsWithTime))\n\t\t\tfor _, item := range ipsWithTime {\n\t\t\t\tif item.IP == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif item.Timestamp > 0 {\n\t\t\t\t\tts := time.Unix(item.Timestamp, 0).Format(\"2006-01-02 15:04:05\")\n\t\t\t\t\tlines = append(lines, fmt.Sprintf(\"%s (%s)\", item.IP, ts))\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlines = append(lines, item.IP)\n\t\t\t}\n\t\t\tif len(lines) > 0 {\n\t\t\t\tformattedIps = strings.Join(lines, \"\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tvar oldIps []string\n\t\t\tif json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 {\n\t\t\t\tformattedIps = strings.Join(oldIps, \"\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\toutput := \"\"\n\toutput += t.I18nBot(\"tgbot.messages.email\", \"Email==\"+email)\n\toutput += t.I18nBot(\"tgbot.messages.ips\", \"IPs==\"+formattedIps)\n\toutput += t.I18nBot(\"tgbot.messages.refreshedOn\", \"Time==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\n\tinlineKeyboard := tu.InlineKeyboard(\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.refresh\")).WithCallbackData(t.encodeQuery(\"ips_refresh \"+email)),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.clearIPs\")).WithCallbackData(t.encodeQuery(\"clear_ips \"+email)),\n\t\t),\n\t)\n\n\tif len(messageID) > 0 {\n\t\tt.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)\n\t} else {\n\t\tt.SendMsgToTgbot(chatId, output, inlineKeyboard)\n\t}\n}\n\n// clientTelegramUserInfo retrieves and sends Telegram user info for the client.\nfunc (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) {\n\ttraffic, client, err := t.inboundService.GetClientByEmail(email)\n\tif err != nil {\n\t\tlogger.Warning(err)\n\t\tmsg := t.I18nBot(\"tgbot.wentWrong\")\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t\treturn\n\t}\n\tif client == nil {\n\t\tmsg := t.I18nBot(\"tgbot.noResult\")\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t\treturn\n\t}\n\ttgId := \"None\"\n\tif client.TgID != 0 {\n\t\ttgId = strconv.FormatInt(client.TgID, 10)\n\t}\n\n\toutput := \"\"\n\toutput += t.I18nBot(\"tgbot.messages.email\", \"Email==\"+email)\n\toutput += t.I18nBot(\"tgbot.messages.TGUser\", \"TelegramID==\"+tgId)\n\toutput += t.I18nBot(\"tgbot.messages.refreshedOn\", \"Time==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\n\tinlineKeyboard := tu.InlineKeyboard(\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.refresh\")).WithCallbackData(t.encodeQuery(\"tgid_refresh \"+email)),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.removeTGUser\")).WithCallbackData(t.encodeQuery(\"tgid_remove \"+email)),\n\t\t),\n\t)\n\n\tif len(messageID) > 0 {\n\t\tt.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)\n\t} else {\n\t\tt.SendMsgToTgbot(chatId, output, inlineKeyboard)\n\t\trequestUser := telego.KeyboardButtonRequestUsers{\n\t\t\tRequestID: int32(traffic.Id),\n\t\t\tUserIsBot: new(bool),\n\t\t}\n\t\tkeyboard := tu.Keyboard(\n\t\t\ttu.KeyboardRow(\n\t\t\t\ttu.KeyboardButton(t.I18nBot(\"tgbot.buttons.selectTGUser\")).WithRequestUsers(&requestUser),\n\t\t\t),\n\t\t\ttu.KeyboardRow(\n\t\t\t\ttu.KeyboardButton(t.I18nBot(\"tgbot.buttons.closeKeyboard\")),\n\t\t\t),\n\t\t).WithIsPersistent().WithResizeKeyboard()\n\t\tt.SendMsgToTgbot(chatId, t.I18nBot(\"tgbot.buttons.selectOneTGUser\"), keyboard)\n\t}\n}\n\n// searchClient searches for a client by email and sends the information.\nfunc (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {\n\ttraffic, err := t.inboundService.GetClientTrafficByEmail(email)\n\tif err != nil {\n\t\tlogger.Warning(err)\n\t\tmsg := t.I18nBot(\"tgbot.wentWrong\")\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t\treturn\n\t}\n\tif traffic == nil {\n\t\tmsg := t.I18nBot(\"tgbot.noResult\")\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t\treturn\n\t}\n\n\toutput := t.clientInfoMsg(traffic, true, true, true, true, true, true)\n\n\tinlineKeyboard := tu.InlineKeyboard(\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.refresh\")).WithCallbackData(t.encodeQuery(\"client_refresh \"+email)),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.resetTraffic\")).WithCallbackData(t.encodeQuery(\"reset_traffic \"+email)),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.limitTraffic\")).WithCallbackData(t.encodeQuery(\"limit_traffic \"+email)),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.resetExpire\")).WithCallbackData(t.encodeQuery(\"reset_exp \"+email)),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.ipLog\")).WithCallbackData(t.encodeQuery(\"ip_log \"+email)),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.ipLimit\")).WithCallbackData(t.encodeQuery(\"ip_limit \"+email)),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.setTGUser\")).WithCallbackData(t.encodeQuery(\"tg_user \"+email)),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.toggle\")).WithCallbackData(t.encodeQuery(\"toggle_enable \"+email)),\n\t\t),\n\t)\n\tif len(messageID) > 0 {\n\t\tt.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)\n\t} else {\n\t\tt.SendMsgToTgbot(chatId, output, inlineKeyboard)\n\t}\n}\n\n// getCommonClientButtons returns the shared inline keyboard rows for client configuration\nfunc (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {\n\treturn [][]telego.InlineKeyboardButton{\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.limitTraffic\")).WithCallbackData(\"add_client_ch_default_traffic\"),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.resetExpire\")).WithCallbackData(\"add_client_ch_default_exp\"),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.change_comment\")).WithCallbackData(\"add_client_ch_default_comment\"),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.ipLimit\")).WithCallbackData(\"add_client_ch_default_ip_limit\"),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.submitDisable\")).WithCallbackData(\"add_client_submit_disable\"),\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.submitEnable\")).WithCallbackData(\"add_client_submit_enable\"),\n\t\t),\n\t\ttu.InlineKeyboardRow(\n\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.cancel\")).WithCallbackData(\"add_client_cancel\"),\n\t\t),\n\t}\n}\n\n// addClient handles the process of adding a new client to an inbound.\nfunc (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {\n\tinbound, err := t.inboundService.GetInbound(receiver_inbound_ID)\n\tif err != nil {\n\t\tt.SendMsgToTgbot(chatId, err.Error())\n\t\treturn\n\t}\n\n\tprotocol := inbound.Protocol\n\n\tvar protocolRows [][]telego.InlineKeyboardButton\n\tswitch protocol {\n\tcase model.VMESS, model.VLESS:\n\t\tprotocolRows = [][]telego.InlineKeyboardButton{\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.change_email\")).WithCallbackData(\"add_client_ch_default_email\"),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.change_id\")).WithCallbackData(\"add_client_ch_default_id\"),\n\t\t\t),\n\t\t}\n\tcase model.Trojan:\n\t\tprotocolRows = [][]telego.InlineKeyboardButton{\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.change_email\")).WithCallbackData(\"add_client_ch_default_email\"),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.change_password\")).WithCallbackData(\"add_client_ch_default_pass_tr\"),\n\t\t\t),\n\t\t}\n\tcase model.Shadowsocks:\n\t\tprotocolRows = [][]telego.InlineKeyboardButton{\n\t\t\ttu.InlineKeyboardRow(\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.change_email\")).WithCallbackData(\"add_client_ch_default_email\"),\n\t\t\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.change_password\")).WithCallbackData(\"add_client_ch_default_pass_sh\"),\n\t\t\t),\n\t\t}\n\t}\n\n\tcommonRows := t.getCommonClientButtons()\n\tinlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...)\n\n\tif len(messageID) > 0 {\n\t\tt.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)\n\t} else {\n\t\tt.SendMsgToTgbot(chatId, msg, inlineKeyboard)\n\t}\n\n}\n\n// searchInbound searches for inbounds by remark and sends the results.\nfunc (t *Tgbot) searchInbound(chatId int64, remark string) {\n\tinbounds, err := t.inboundService.SearchInbounds(remark)\n\tif err != nil {\n\t\tlogger.Warning(err)\n\t\tmsg := t.I18nBot(\"tgbot.wentWrong\")\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t\treturn\n\t}\n\tif len(inbounds) == 0 {\n\t\tmsg := t.I18nBot(\"tgbot.noInbounds\")\n\t\tt.SendMsgToTgbot(chatId, msg)\n\t\treturn\n\t}\n\n\tfor _, inbound := range inbounds {\n\t\tinfo := \"\"\n\t\tinfo += t.I18nBot(\"tgbot.messages.inbound\", \"Remark==\"+inbound.Remark)\n\t\tinfo += t.I18nBot(\"tgbot.messages.port\", \"Port==\"+strconv.Itoa(inbound.Port))\n\t\tinfo += t.I18nBot(\"tgbot.messages.traffic\", \"Total==\"+common.FormatTraffic((inbound.Up+inbound.Down)), \"Upload==\"+common.FormatTraffic(inbound.Up), \"Download==\"+common.FormatTraffic(inbound.Down))\n\n\t\tif inbound.ExpiryTime == 0 {\n\t\t\tinfo += t.I18nBot(\"tgbot.messages.expire\", \"Time==\"+t.I18nBot(\"tgbot.unlimited\"))\n\t\t} else {\n\t\t\tinfo += t.I18nBot(\"tgbot.messages.expire\", \"Time==\"+time.Unix((inbound.ExpiryTime/1000), 0).Format(\"2006-01-02 15:04:05\"))\n\t\t}\n\t\tt.SendMsgToTgbot(chatId, info)\n\n\t\tif len(inbound.ClientStats) > 0 {\n\t\t\tvar output strings.Builder\n\t\t\tfor _, traffic := range inbound.ClientStats {\n\t\t\t\toutput.WriteString(t.clientInfoMsg(&traffic, true, true, true, true, true, true))\n\t\t\t}\n\t\t\tt.SendMsgToTgbot(chatId, output.String())\n\t\t}\n\t}\n}\n\n// getExhausted retrieves and sends information about exhausted clients.\nfunc (t *Tgbot) getExhausted(chatId int64) {\n\ttrDiff := int64(0)\n\texDiff := int64(0)\n\tnow := time.Now().Unix() * 1000\n\tvar exhaustedInbounds []model.Inbound\n\tvar exhaustedClients []xray.ClientTraffic\n\tvar disabledInbounds []model.Inbound\n\tvar disabledClients []xray.ClientTraffic\n\n\tTrafficThreshold, err := t.settingService.GetTrafficDiff()\n\tif err == nil && TrafficThreshold > 0 {\n\t\ttrDiff = int64(TrafficThreshold) * 1073741824\n\t}\n\tExpireThreshold, err := t.settingService.GetExpireDiff()\n\tif err == nil && ExpireThreshold > 0 {\n\t\texDiff = int64(ExpireThreshold) * 86400000\n\t}\n\tinbounds, err := t.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"Unable to load Inbounds\", err)\n\t}\n\n\tfor _, inbound := range inbounds {\n\t\tif inbound.Enable {\n\t\t\tif (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||\n\t\t\t\t(inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) {\n\t\t\t\texhaustedInbounds = append(exhaustedInbounds, *inbound)\n\t\t\t}\n\t\t\tif len(inbound.ClientStats) > 0 {\n\t\t\t\tfor _, client := range inbound.ClientStats {\n\t\t\t\t\tif client.Enable {\n\t\t\t\t\t\tif (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||\n\t\t\t\t\t\t\t(client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {\n\t\t\t\t\t\t\texhaustedClients = append(exhaustedClients, client)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdisabledClients = append(disabledClients, client)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tdisabledInbounds = append(disabledInbounds, *inbound)\n\t\t}\n\t}\n\n\t// Inbounds\n\toutput := \"\"\n\toutput += t.I18nBot(\"tgbot.messages.exhaustedCount\", \"Type==\"+t.I18nBot(\"tgbot.inbounds\"))\n\toutput += t.I18nBot(\"tgbot.messages.disabled\", \"Disabled==\"+strconv.Itoa(len(disabledInbounds)))\n\toutput += t.I18nBot(\"tgbot.messages.depleteSoon\", \"Deplete==\"+strconv.Itoa(len(exhaustedInbounds)))\n\n\tif len(exhaustedInbounds) > 0 {\n\t\toutput += t.I18nBot(\"tgbot.messages.depleteSoon\", \"Deplete==\"+t.I18nBot(\"tgbot.inbounds\"))\n\n\t\tfor _, inbound := range exhaustedInbounds {\n\t\t\toutput += t.I18nBot(\"tgbot.messages.inbound\", \"Remark==\"+inbound.Remark)\n\t\t\toutput += t.I18nBot(\"tgbot.messages.port\", \"Port==\"+strconv.Itoa(inbound.Port))\n\t\t\toutput += t.I18nBot(\"tgbot.messages.traffic\", \"Total==\"+common.FormatTraffic((inbound.Up+inbound.Down)), \"Upload==\"+common.FormatTraffic(inbound.Up), \"Download==\"+common.FormatTraffic(inbound.Down))\n\t\t\tif inbound.ExpiryTime == 0 {\n\t\t\t\toutput += t.I18nBot(\"tgbot.messages.expire\", \"Time==\"+t.I18nBot(\"tgbot.unlimited\"))\n\t\t\t} else {\n\t\t\t\toutput += t.I18nBot(\"tgbot.messages.expire\", \"Time==\"+time.Unix((inbound.ExpiryTime/1000), 0).Format(\"2006-01-02 15:04:05\"))\n\t\t\t}\n\t\t\toutput += \"\\r\\n\"\n\t\t}\n\t}\n\n\t// Clients\n\texhaustedCC := len(exhaustedClients)\n\toutput += t.I18nBot(\"tgbot.messages.exhaustedCount\", \"Type==\"+t.I18nBot(\"tgbot.clients\"))\n\toutput += t.I18nBot(\"tgbot.messages.disabled\", \"Disabled==\"+strconv.Itoa(len(disabledClients)))\n\toutput += t.I18nBot(\"tgbot.messages.depleteSoon\", \"Deplete==\"+strconv.Itoa(exhaustedCC))\n\n\tif exhaustedCC > 0 {\n\t\toutput += t.I18nBot(\"tgbot.messages.depleteSoon\", \"Deplete==\"+t.I18nBot(\"tgbot.clients\"))\n\t\tvar buttons []telego.InlineKeyboardButton\n\t\tfor _, traffic := range exhaustedClients {\n\t\t\toutput += t.clientInfoMsg(&traffic, true, false, false, true, true, false)\n\t\t\toutput += \"\\r\\n\"\n\t\t\tbuttons = append(buttons, tu.InlineKeyboardButton(traffic.Email).WithCallbackData(t.encodeQuery(\"client_get_usage \"+traffic.Email)))\n\t\t}\n\t\tcols := 0\n\t\tif exhaustedCC < 11 {\n\t\t\tcols = 1\n\t\t} else {\n\t\t\tcols = 2\n\t\t}\n\t\toutput += t.I18nBot(\"tgbot.messages.refreshedOn\", \"Time==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\t\tkeyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))\n\t\tt.SendMsgToTgbot(chatId, output, keyboard)\n\t} else {\n\t\toutput += t.I18nBot(\"tgbot.messages.refreshedOn\", \"Time==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\t\tt.SendMsgToTgbot(chatId, output)\n\t}\n}\n\n// notifyExhausted sends notifications for exhausted clients.\nfunc (t *Tgbot) notifyExhausted() {\n\ttrDiff := int64(0)\n\texDiff := int64(0)\n\tnow := time.Now().Unix() * 1000\n\n\tTrafficThreshold, err := t.settingService.GetTrafficDiff()\n\tif err == nil && TrafficThreshold > 0 {\n\t\ttrDiff = int64(TrafficThreshold) * 1073741824\n\t}\n\tExpireThreshold, err := t.settingService.GetExpireDiff()\n\tif err == nil && ExpireThreshold > 0 {\n\t\texDiff = int64(ExpireThreshold) * 86400000\n\t}\n\tinbounds, err := t.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\tlogger.Warning(\"Unable to load Inbounds\", err)\n\t}\n\n\tvar chatIDsDone []int64\n\tfor _, inbound := range inbounds {\n\t\tif inbound.Enable {\n\t\t\tif len(inbound.ClientStats) > 0 {\n\t\t\t\tclients, err := t.inboundService.GetClients(inbound)\n\t\t\t\tif err == nil {\n\t\t\t\t\tfor _, client := range clients {\n\t\t\t\t\t\tif client.TgID != 0 {\n\t\t\t\t\t\t\tchatID := client.TgID\n\t\t\t\t\t\t\tif !int64Contains(chatIDsDone, chatID) && !checkAdmin(chatID) {\n\t\t\t\t\t\t\t\tvar disabledClients []xray.ClientTraffic\n\t\t\t\t\t\t\t\tvar exhaustedClients []xray.ClientTraffic\n\t\t\t\t\t\t\t\ttraffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID)\n\t\t\t\t\t\t\t\tif err == nil && len(traffics) > 0 {\n\t\t\t\t\t\t\t\t\toutput := t.I18nBot(\"tgbot.messages.exhaustedCount\", \"Type==\"+t.I18nBot(\"tgbot.clients\"))\n\t\t\t\t\t\t\t\t\tfor _, traffic := range traffics {\n\t\t\t\t\t\t\t\t\t\tif traffic.Enable {\n\t\t\t\t\t\t\t\t\t\t\tif (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) ||\n\t\t\t\t\t\t\t\t\t\t\t\t(traffic.Total > 0 && (traffic.Total-(traffic.Up+traffic.Down) < trDiff)) {\n\t\t\t\t\t\t\t\t\t\t\t\texhaustedClients = append(exhaustedClients, *traffic)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tdisabledClients = append(disabledClients, *traffic)\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif len(exhaustedClients) > 0 {\n\t\t\t\t\t\t\t\t\t\toutput += t.I18nBot(\"tgbot.messages.disabled\", \"Disabled==\"+strconv.Itoa(len(disabledClients)))\n\t\t\t\t\t\t\t\t\t\tif len(disabledClients) > 0 {\n\t\t\t\t\t\t\t\t\t\t\toutput += t.I18nBot(\"tgbot.clients\") + \":\\r\\n\"\n\t\t\t\t\t\t\t\t\t\t\tfor _, traffic := range disabledClients {\n\t\t\t\t\t\t\t\t\t\t\t\toutput += \" \" + traffic.Email\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\toutput += \"\\r\\n\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\toutput += \"\\r\\n\"\n\t\t\t\t\t\t\t\t\t\toutput += t.I18nBot(\"tgbot.messages.depleteSoon\", \"Deplete==\"+strconv.Itoa(len(exhaustedClients)))\n\t\t\t\t\t\t\t\t\t\tfor _, traffic := range exhaustedClients {\n\t\t\t\t\t\t\t\t\t\t\toutput += t.clientInfoMsg(&traffic, true, false, false, true, true, false)\n\t\t\t\t\t\t\t\t\t\t\toutput += \"\\r\\n\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tt.SendMsgToTgbot(chatID, output)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tchatIDsDone = append(chatIDsDone, chatID)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// int64Contains checks if an int64 slice contains a specific item.\nfunc int64Contains(slice []int64, item int64) bool {\n\tfor _, s := range slice {\n\t\tif s == item {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// onlineClients retrieves and sends information about online clients.\nfunc (t *Tgbot) onlineClients(chatId int64, messageID ...int) {\n\tif !p.IsRunning() {\n\t\treturn\n\t}\n\n\tonlines := p.GetOnlineClients()\n\tonlinesCount := len(onlines)\n\toutput := t.I18nBot(\"tgbot.messages.onlinesCount\", \"Count==\"+fmt.Sprint(onlinesCount))\n\tkeyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(\n\t\ttu.InlineKeyboardButton(t.I18nBot(\"tgbot.buttons.refresh\")).WithCallbackData(t.encodeQuery(\"onlines_refresh\"))))\n\n\tif onlinesCount > 0 {\n\t\tvar buttons []telego.InlineKeyboardButton\n\t\tfor _, online := range onlines {\n\t\t\tbuttons = append(buttons, tu.InlineKeyboardButton(online).WithCallbackData(t.encodeQuery(\"client_get_usage \"+online)))\n\t\t}\n\t\tcols := 0\n\t\tif onlinesCount < 21 {\n\t\t\tcols = 2\n\t\t} else if onlinesCount < 61 {\n\t\t\tcols = 3\n\t\t} else {\n\t\t\tcols = 4\n\t\t}\n\t\tkeyboard.InlineKeyboard = append(keyboard.InlineKeyboard, tu.InlineKeyboardCols(cols, buttons...)...)\n\t}\n\n\tif len(messageID) > 0 {\n\t\tt.editMessageTgBot(chatId, messageID[0], output, keyboard)\n\t} else {\n\t\tt.SendMsgToTgbot(chatId, output, keyboard)\n\t}\n}\n\n// sendBackup sends a backup of the database and configuration files.\nfunc (t *Tgbot) sendBackup(chatId int64) {\n\toutput := t.I18nBot(\"tgbot.messages.backupTime\", \"Time==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\tt.SendMsgToTgbot(chatId, output)\n\n\t// Update by manually trigger a checkpoint operation\n\terr := database.Checkpoint()\n\tif err != nil {\n\t\tlogger.Error(\"Error in trigger a checkpoint operation: \", err)\n\t}\n\n\t// Send database backup\n\tfile, err := os.Open(config.GetDBPath())\n\tif err == nil {\n\t\tdefer file.Close()\n\t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\t\tdefer cancel()\n\t\tdocument := tu.Document(\n\t\t\ttu.ID(chatId),\n\t\t\ttu.File(file),\n\t\t)\n\t\t_, err = bot.SendDocument(ctx, document)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Error in uploading backup: \", err)\n\t\t}\n\t} else {\n\t\tlogger.Error(\"Error in opening db file for backup: \", err)\n\t}\n\n\t// Small delay between file sends\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Send config.json backup\n\tfile, err = os.Open(xray.GetConfigPath())\n\tif err == nil {\n\t\tdefer file.Close()\n\t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\t\tdefer cancel()\n\t\tdocument := tu.Document(\n\t\t\ttu.ID(chatId),\n\t\t\ttu.File(file),\n\t\t)\n\t\t_, err = bot.SendDocument(ctx, document)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Error in uploading config.json: \", err)\n\t\t}\n\t} else {\n\t\tlogger.Error(\"Error in opening config.json file for backup: \", err)\n\t}\n}\n\n// sendBanLogs sends the ban logs to the specified chat.\nfunc (t *Tgbot) sendBanLogs(chatId int64, dt bool) {\n\tif dt {\n\t\toutput := t.I18nBot(\"tgbot.messages.datetime\", \"DateTime==\"+time.Now().Format(\"2006-01-02 15:04:05\"))\n\t\tt.SendMsgToTgbot(chatId, output)\n\t}\n\n\tfile, err := os.Open(xray.GetIPLimitBannedPrevLogPath())\n\tif err == nil {\n\t\t// Check if the file is non-empty before attempting to upload\n\t\tfileInfo, _ := file.Stat()\n\t\tif fileInfo.Size() > 0 {\n\t\t\tdocument := tu.Document(\n\t\t\t\ttu.ID(chatId),\n\t\t\t\ttu.File(file),\n\t\t\t)\n\t\t\t_, err = bot.SendDocument(context.Background(), document)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(\"Error in uploading IPLimitBannedPrevLog: \", err)\n\t\t\t}\n\t\t} else {\n\t\t\tlogger.Warning(\"IPLimitBannedPrevLog file is empty, not uploading.\")\n\t\t}\n\t\tfile.Close()\n\t} else {\n\t\tlogger.Error(\"Error in opening IPLimitBannedPrevLog file for backup: \", err)\n\t}\n\n\tfile, err = os.Open(xray.GetIPLimitBannedLogPath())\n\tif err == nil {\n\t\t// Check if the file is non-empty before attempting to upload\n\t\tfileInfo, _ := file.Stat()\n\t\tif fileInfo.Size() > 0 {\n\t\t\tdocument := tu.Document(\n\t\t\t\ttu.ID(chatId),\n\t\t\t\ttu.File(file),\n\t\t\t)\n\t\t\t_, err = bot.SendDocument(context.Background(), document)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(\"Error in uploading IPLimitBannedLog: \", err)\n\t\t\t}\n\t\t} else {\n\t\t\tlogger.Warning(\"IPLimitBannedLog file is empty, not uploading.\")\n\t\t}\n\t\tfile.Close()\n\t} else {\n\t\tlogger.Error(\"Error in opening IPLimitBannedLog file for backup: \", err)\n\t}\n}\n\n// sendCallbackAnswerTgBot answers a callback query with a message.\nfunc (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {\n\tparams := telego.AnswerCallbackQueryParams{\n\t\tCallbackQueryID: id,\n\t\tText:            message,\n\t}\n\tif err := bot.AnswerCallbackQuery(context.Background(), &params); err != nil {\n\t\tlogger.Warning(err)\n\t}\n}\n\n// editMessageCallbackTgBot edits the reply markup of a message.\nfunc (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {\n\tparams := telego.EditMessageReplyMarkupParams{\n\t\tChatID:      tu.ID(chatId),\n\t\tMessageID:   messageID,\n\t\tReplyMarkup: inlineKeyboard,\n\t}\n\tif _, err := bot.EditMessageReplyMarkup(context.Background(), &params); err != nil {\n\t\tlogger.Warning(err)\n\t}\n}\n\n// editMessageTgBot edits the text and reply markup of a message.\nfunc (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) {\n\tparams := telego.EditMessageTextParams{\n\t\tChatID:    tu.ID(chatId),\n\t\tMessageID: messageID,\n\t\tText:      text,\n\t\tParseMode: \"HTML\",\n\t}\n\tif len(inlineKeyboard) > 0 {\n\t\tparams.ReplyMarkup = inlineKeyboard[0]\n\t}\n\tif _, err := bot.EditMessageText(context.Background(), &params); err != nil {\n\t\tlogger.Warning(err)\n\t}\n}\n\n// SendMsgToTgbotDeleteAfter sends a message and deletes it after a specified delay.\nfunc (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) {\n\t// Determine if replyMarkup was passed; otherwise, set it to nil\n\tvar replyMarkupParam telego.ReplyMarkup\n\tif len(replyMarkup) > 0 {\n\t\treplyMarkupParam = replyMarkup[0] // Use the first element\n\t}\n\n\t// Send the message\n\tsentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{\n\t\tChatID:      tu.ID(chatId),\n\t\tText:        msg,\n\t\tReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value\n\t})\n\tif err != nil {\n\t\tlogger.Warning(\"Failed to send message:\", err)\n\t\treturn\n\t}\n\n\t// Delete the sent message after the specified number of seconds\n\tgo func() {\n\t\ttime.Sleep(time.Duration(delayInSeconds) * time.Second) // Wait for the specified delay\n\t\tt.deleteMessageTgBot(chatId, sentMsg.MessageID)         // Delete the message\n\t\tdelete(userStates, chatId)\n\t}()\n}\n\n// deleteMessageTgBot deletes a message from the chat.\nfunc (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {\n\tparams := telego.DeleteMessageParams{\n\t\tChatID:    tu.ID(chatId),\n\t\tMessageID: messageID,\n\t}\n\tif err := bot.DeleteMessage(context.Background(), &params); err != nil {\n\t\tlogger.Warning(\"Failed to delete message:\", err)\n\t} else {\n\t\tlogger.Info(\"Message deleted successfully\")\n\t}\n}\n\n// isSingleWord checks if the text contains only a single word.\nfunc (t *Tgbot) isSingleWord(text string) bool {\n\ttext = strings.TrimSpace(text)\n\tre := regexp.MustCompile(`\\s+`)\n\treturn re.MatchString(text)\n}\n"
  },
  {
    "path": "web/service/user.go",
    "content": "package service\n\nimport (\n\t\"errors\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database\"\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/crypto\"\n\tldaputil \"github.com/mhsanaei/3x-ui/v2/util/ldap\"\n\t\"github.com/xlzd/gotp\"\n\t\"gorm.io/gorm\"\n)\n\n// UserService provides business logic for user management and authentication.\n// It handles user creation, login, password management, and 2FA operations.\ntype UserService struct {\n\tsettingService SettingService\n}\n\n// GetFirstUser retrieves the first user from the database.\n// This is typically used for initial setup or when there's only one admin user.\nfunc (s *UserService) GetFirstUser() (*model.User, error) {\n\tdb := database.GetDB()\n\n\tuser := &model.User{}\n\terr := db.Model(model.User{}).\n\t\tFirst(user).\n\t\tError\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn user, nil\n}\n\nfunc (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) {\n\tdb := database.GetDB()\n\n\tuser := &model.User{}\n\n\terr := db.Model(model.User{}).\n\t\tWhere(\"username = ?\", username).\n\t\tFirst(user).\n\t\tError\n\tif err == gorm.ErrRecordNotFound {\n\t\treturn nil, errors.New(\"invalid credentials\")\n\t} else if err != nil {\n\t\tlogger.Warning(\"check user err:\", err)\n\t\treturn nil, err\n\t}\n\n\tif !crypto.CheckPasswordHash(user.Password, password) {\n\t\tldapEnabled, _ := s.settingService.GetLdapEnable()\n\t\tif !ldapEnabled {\n\t\t\treturn nil, errors.New(\"invalid credentials\")\n\t\t}\n\n\t\thost, _ := s.settingService.GetLdapHost()\n\t\tport, _ := s.settingService.GetLdapPort()\n\t\tuseTLS, _ := s.settingService.GetLdapUseTLS()\n\t\tbindDN, _ := s.settingService.GetLdapBindDN()\n\t\tldapPass, _ := s.settingService.GetLdapPassword()\n\t\tbaseDN, _ := s.settingService.GetLdapBaseDN()\n\t\tuserFilter, _ := s.settingService.GetLdapUserFilter()\n\t\tuserAttr, _ := s.settingService.GetLdapUserAttr()\n\n\t\tcfg := ldaputil.Config{\n\t\t\tHost:       host,\n\t\t\tPort:       port,\n\t\t\tUseTLS:     useTLS,\n\t\t\tBindDN:     bindDN,\n\t\t\tPassword:   ldapPass,\n\t\t\tBaseDN:     baseDN,\n\t\t\tUserFilter: userFilter,\n\t\t\tUserAttr:   userAttr,\n\t\t}\n\t\tok, err := ldaputil.AuthenticateUser(cfg, username, password)\n\t\tif err != nil || !ok {\n\t\t\treturn nil, errors.New(\"invalid credentials\")\n\t\t}\n\t}\n\n\ttwoFactorEnable, err := s.settingService.GetTwoFactorEnable()\n\tif err != nil {\n\t\tlogger.Warning(\"check two factor err:\", err)\n\t\treturn nil, err\n\t}\n\n\tif twoFactorEnable {\n\t\ttwoFactorToken, err := s.settingService.GetTwoFactorToken()\n\n\t\tif err != nil {\n\t\t\tlogger.Warning(\"check two factor token err:\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {\n\t\t\treturn nil, errors.New(\"invalid 2fa code\")\n\t\t}\n\t}\n\n\treturn user, nil\n}\n\nfunc (s *UserService) UpdateUser(id int, username string, password string) error {\n\tdb := database.GetDB()\n\thashedPassword, err := crypto.HashPasswordAsBcrypt(password)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttwoFactorEnable, err := s.settingService.GetTwoFactorEnable()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif twoFactorEnable {\n\t\ts.settingService.SetTwoFactorEnable(false)\n\t\ts.settingService.SetTwoFactorToken(\"\")\n\t}\n\n\treturn db.Model(model.User{}).\n\t\tWhere(\"id = ?\", id).\n\t\tUpdates(map[string]any{\"username\": username, \"password\": hashedPassword}).\n\t\tError\n}\n\nfunc (s *UserService) UpdateFirstUser(username string, password string) error {\n\tif username == \"\" {\n\t\treturn errors.New(\"username can not be empty\")\n\t} else if password == \"\" {\n\t\treturn errors.New(\"password can not be empty\")\n\t}\n\thashedPassword, er := crypto.HashPasswordAsBcrypt(password)\n\n\tif er != nil {\n\t\treturn er\n\t}\n\n\tdb := database.GetDB()\n\tuser := &model.User{}\n\terr := db.Model(model.User{}).First(user).Error\n\tif database.IsNotFound(err) {\n\t\tuser.Username = username\n\t\tuser.Password = hashedPassword\n\t\treturn db.Model(model.User{}).Create(user).Error\n\t} else if err != nil {\n\t\treturn err\n\t}\n\tuser.Username = username\n\tuser.Password = hashedPassword\n\treturn db.Save(user).Error\n}\n"
  },
  {
    "path": "web/service/warp.go",
    "content": "package service\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n)\n\n// WarpService provides business logic for Cloudflare WARP integration.\n// It manages WARP configuration and connectivity settings.\ntype WarpService struct {\n\tSettingService\n}\n\nfunc (s *WarpService) GetWarpData() (string, error) {\n\twarp, err := s.SettingService.GetWarp()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn warp, nil\n}\n\nfunc (s *WarpService) DelWarpData() error {\n\terr := s.SettingService.SetWarp(\"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *WarpService) GetWarpConfig() (string, error) {\n\tvar warpData map[string]string\n\twarp, err := s.SettingService.GetWarp()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = json.Unmarshal([]byte(warp), &warpData)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\turl := fmt.Sprintf(\"https://api.cloudflareclient.com/v0a2158/reg/%s\", warpData[\"device_id\"])\n\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+warpData[\"access_token\"])\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\tbuffer := &bytes.Buffer{}\n\t_, err = buffer.ReadFrom(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buffer.String(), nil\n}\n\nfunc (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) {\n\ttos := time.Now().UTC().Format(\"2006-01-02T15:04:05.000Z\")\n\thostName, _ := os.Hostname()\n\tdata := fmt.Sprintf(`{\"key\":\"%s\",\"tos\":\"%s\",\"type\": \"PC\",\"model\": \"x-ui\", \"name\": \"%s\"}`, publicKey, tos, hostName)\n\n\turl := \"https://api.cloudflareclient.com/v0a2158/reg\"\n\n\treq, err := http.NewRequest(\"POST\", url, bytes.NewBuffer([]byte(data)))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq.Header.Add(\"CF-Client-Version\", \"a-7.21-0721\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\tbuffer := &bytes.Buffer{}\n\t_, err = buffer.ReadFrom(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar rspData map[string]any\n\terr = json.Unmarshal(buffer.Bytes(), &rspData)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdeviceId := rspData[\"id\"].(string)\n\ttoken := rspData[\"token\"].(string)\n\tlicense, ok := rspData[\"account\"].(map[string]any)[\"license\"].(string)\n\tif !ok {\n\t\tlogger.Debug(\"Error accessing license value.\")\n\t\treturn \"\", err\n\t}\n\n\twarpData := fmt.Sprintf(\"{\\n  \\\"access_token\\\": \\\"%s\\\",\\n  \\\"device_id\\\": \\\"%s\\\",\", token, deviceId)\n\twarpData += fmt.Sprintf(\"\\n  \\\"license_key\\\": \\\"%s\\\",\\n  \\\"private_key\\\": \\\"%s\\\"\\n}\", license, secretKey)\n\n\ts.SettingService.SetWarp(warpData)\n\n\tresult := fmt.Sprintf(\"{\\n  \\\"data\\\": %s,\\n  \\\"config\\\": %s\\n}\", warpData, buffer.String())\n\n\treturn result, nil\n}\n\nfunc (s *WarpService) SetWarpLicense(license string) (string, error) {\n\tvar warpData map[string]string\n\twarp, err := s.SettingService.GetWarp()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = json.Unmarshal([]byte(warp), &warpData)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\turl := fmt.Sprintf(\"https://api.cloudflareclient.com/v0a2158/reg/%s/account\", warpData[\"device_id\"])\n\tdata := fmt.Sprintf(`{\"license\": \"%s\"}`, license)\n\n\treq, err := http.NewRequest(\"PUT\", url, bytes.NewBuffer([]byte(data)))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+warpData[\"access_token\"])\n\n\tclient := &http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\tbuffer := &bytes.Buffer{}\n\t_, err = buffer.ReadFrom(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar response map[string]any\n\terr = json.Unmarshal(buffer.Bytes(), &response)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif response[\"success\"] == false {\n\t\terrorArr, _ := response[\"errors\"].([]any)\n\t\terrorObj := errorArr[0].(map[string]any)\n\t\treturn \"\", common.NewError(errorObj[\"code\"], errorObj[\"message\"])\n\t}\n\n\twarpData[\"license_key\"] = license\n\tnewWarpData, err := json.MarshalIndent(warpData, \"\", \"  \")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ts.SettingService.SetWarp(string(newWarpData))\n\n\treturn string(newWarpData), nil\n}\n"
  },
  {
    "path": "web/service/xray.go",
    "content": "package service\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"runtime\"\n\t\"sync\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n\n\t\"go.uber.org/atomic\"\n)\n\nvar (\n\tp                 *xray.Process\n\tlock              sync.Mutex\n\tisNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray\n\tisManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel\n\tresult            string\n)\n\n// XrayService provides business logic for Xray process management.\n// It handles starting, stopping, restarting Xray, and managing its configuration.\ntype XrayService struct {\n\tinboundService InboundService\n\tsettingService SettingService\n\txrayAPI        xray.XrayAPI\n}\n\n// IsXrayRunning checks if the Xray process is currently running.\nfunc (s *XrayService) IsXrayRunning() bool {\n\treturn p != nil && p.IsRunning()\n}\n\n// GetXrayErr returns the error from the Xray process, if any.\nfunc (s *XrayService) GetXrayErr() error {\n\tif p == nil {\n\t\treturn nil\n\t}\n\n\terr := p.GetErr()\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tif runtime.GOOS == \"windows\" && err.Error() == \"exit status 1\" {\n\t\t// exit status 1 on Windows means that Xray process was killed\n\t\t// as we kill process to stop in on Windows, this is not an error\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\n// GetXrayResult returns the result string from the Xray process.\nfunc (s *XrayService) GetXrayResult() string {\n\tif result != \"\" {\n\t\treturn result\n\t}\n\tif s.IsXrayRunning() {\n\t\treturn \"\"\n\t}\n\tif p == nil {\n\t\treturn \"\"\n\t}\n\n\tresult = p.GetResult()\n\n\tif runtime.GOOS == \"windows\" && result == \"exit status 1\" {\n\t\t// exit status 1 on Windows means that Xray process was killed\n\t\t// as we kill process to stop in on Windows, this is not an error\n\t\treturn \"\"\n\t}\n\n\treturn result\n}\n\n// GetXrayVersion returns the version of the running Xray process.\nfunc (s *XrayService) GetXrayVersion() string {\n\tif p == nil {\n\t\treturn \"Unknown\"\n\t}\n\treturn p.GetVersion()\n}\n\n// RemoveIndex removes an element at the specified index from a slice.\n// Returns a new slice with the element removed.\nfunc RemoveIndex(s []any, index int) []any {\n\treturn append(s[:index], s[index+1:]...)\n}\n\n// GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds.\nfunc (s *XrayService) GetXrayConfig() (*xray.Config, error) {\n\ttemplateConfig, err := s.settingService.GetXrayConfigTemplate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\txrayConfig := &xray.Config{}\n\terr = json.Unmarshal([]byte(templateConfig), xrayConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts.inboundService.AddTraffic(nil, nil)\n\n\tinbounds, err := s.inboundService.GetAllInbounds()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, inbound := range inbounds {\n\t\tif !inbound.Enable {\n\t\t\tcontinue\n\t\t}\n\t\t// get settings clients\n\t\tsettings := map[string]any{}\n\t\tjson.Unmarshal([]byte(inbound.Settings), &settings)\n\t\tclients, ok := settings[\"clients\"].([]any)\n\t\tif ok {\n\t\t\t// check users active or not\n\t\t\tclientStats := inbound.ClientStats\n\t\t\tfor _, clientTraffic := range clientStats {\n\t\t\t\tindexDecrease := 0\n\t\t\t\tfor index, client := range clients {\n\t\t\t\t\tc := client.(map[string]any)\n\t\t\t\t\tif c[\"email\"] == clientTraffic.Email {\n\t\t\t\t\t\tif !clientTraffic.Enable {\n\t\t\t\t\t\t\tclients = RemoveIndex(clients, index-indexDecrease)\n\t\t\t\t\t\t\tindexDecrease++\n\t\t\t\t\t\t\tlogger.Infof(\"Remove Inbound User %s due to expiration or traffic limit\", c[\"email\"])\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// clear client config for additional parameters\n\t\t\tvar final_clients []any\n\t\t\tfor _, client := range clients {\n\t\t\t\tc := client.(map[string]any)\n\t\t\t\tif c[\"enable\"] != nil {\n\t\t\t\t\tif enable, ok := c[\"enable\"].(bool); ok && !enable {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor key := range c {\n\t\t\t\t\tif key != \"email\" && key != \"id\" && key != \"password\" && key != \"flow\" && key != \"method\" {\n\t\t\t\t\t\tdelete(c, key)\n\t\t\t\t\t}\n\t\t\t\t\tif c[\"flow\"] == \"xtls-rprx-vision-udp443\" {\n\t\t\t\t\t\tc[\"flow\"] = \"xtls-rprx-vision\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfinal_clients = append(final_clients, any(c))\n\t\t\t}\n\n\t\t\tsettings[\"clients\"] = final_clients\n\t\t\tmodifiedSettings, err := json.MarshalIndent(settings, \"\", \"  \")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tinbound.Settings = string(modifiedSettings)\n\t\t}\n\n\t\tif len(inbound.StreamSettings) > 0 {\n\t\t\t// Unmarshal stream JSON\n\t\t\tvar stream map[string]any\n\t\t\tjson.Unmarshal([]byte(inbound.StreamSettings), &stream)\n\n\t\t\t// Remove the \"settings\" field under \"tlsSettings\" and \"realitySettings\"\n\t\t\ttlsSettings, ok1 := stream[\"tlsSettings\"].(map[string]any)\n\t\t\trealitySettings, ok2 := stream[\"realitySettings\"].(map[string]any)\n\t\t\tif ok1 || ok2 {\n\t\t\t\tif ok1 {\n\t\t\t\t\tdelete(tlsSettings, \"settings\")\n\t\t\t\t} else if ok2 {\n\t\t\t\t\tdelete(realitySettings, \"settings\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdelete(stream, \"externalProxy\")\n\n\t\t\tnewStream, err := json.MarshalIndent(stream, \"\", \"  \")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tinbound.StreamSettings = string(newStream)\n\t\t}\n\n\t\tinboundConfig := inbound.GenXrayInboundConfig()\n\t\txrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)\n\t}\n\treturn xrayConfig, nil\n}\n\n// GetXrayTraffic fetches the current traffic statistics from the running Xray process.\nfunc (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {\n\tif !s.IsXrayRunning() {\n\t\terr := errors.New(\"xray is not running\")\n\t\tlogger.Debug(\"Attempted to fetch Xray traffic, but Xray is not running:\", err)\n\t\treturn nil, nil, err\n\t}\n\tapiPort := p.GetAPIPort()\n\ts.xrayAPI.Init(apiPort)\n\tdefer s.xrayAPI.Close()\n\n\ttraffic, clientTraffic, err := s.xrayAPI.GetTraffic(true)\n\tif err != nil {\n\t\tlogger.Debug(\"Failed to fetch Xray traffic:\", err)\n\t\treturn nil, nil, err\n\t}\n\treturn traffic, clientTraffic, nil\n}\n\n// RestartXray restarts the Xray process, optionally forcing a restart even if config unchanged.\nfunc (s *XrayService) RestartXray(isForce bool) error {\n\tlock.Lock()\n\tdefer lock.Unlock()\n\tlogger.Debug(\"restart Xray, force:\", isForce)\n\tisManuallyStopped.Store(false)\n\n\txrayConfig, err := s.GetXrayConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif s.IsXrayRunning() {\n\t\tif !isForce && p.GetConfig().Equals(xrayConfig) && !isNeedXrayRestart.Load() {\n\t\t\tlogger.Debug(\"It does not need to restart Xray\")\n\t\t\treturn nil\n\t\t}\n\t\tp.Stop()\n\t}\n\n\tp = xray.NewProcess(xrayConfig)\n\tresult = \"\"\n\terr = p.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// StopXray stops the running Xray process.\nfunc (s *XrayService) StopXray() error {\n\tlock.Lock()\n\tdefer lock.Unlock()\n\tisManuallyStopped.Store(true)\n\tlogger.Debug(\"Attempting to stop Xray...\")\n\tif s.IsXrayRunning() {\n\t\treturn p.Stop()\n\t}\n\treturn errors.New(\"xray is not running\")\n}\n\n// SetToNeedRestart marks that Xray needs to be restarted.\nfunc (s *XrayService) SetToNeedRestart() {\n\tisNeedXrayRestart.Store(true)\n}\n\n// IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false.\nfunc (s *XrayService) IsNeedRestartAndSetFalse() bool {\n\treturn isNeedXrayRestart.CompareAndSwap(true, false)\n}\n\n// DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped.\nfunc (s *XrayService) DidXrayCrash() bool {\n\treturn !s.IsXrayRunning() && !isManuallyStopped.Load()\n}\n"
  },
  {
    "path": "web/service/xray_setting.go",
    "content": "package service\n\nimport (\n\t_ \"embed\"\n\t\"encoding/json\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/xray\"\n)\n\n// XraySettingService provides business logic for Xray configuration management.\n// It handles validation and storage of Xray template configurations.\ntype XraySettingService struct {\n\tSettingService\n}\n\nfunc (s *XraySettingService) SaveXraySetting(newXraySettings string) error {\n\tif err := s.CheckXrayConfig(newXraySettings); err != nil {\n\t\treturn err\n\t}\n\treturn s.SettingService.saveSetting(\"xrayTemplateConfig\", newXraySettings)\n}\n\nfunc (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {\n\txrayConfig := &xray.Config{}\n\terr := json.Unmarshal([]byte(XrayTemplateConfig), xrayConfig)\n\tif err != nil {\n\t\treturn common.NewError(\"xray template config invalid:\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "web/session/session.go",
    "content": "// Package session provides session management utilities for the 3x-ui web panel.\n// It handles user authentication state, login sessions, and session storage using Gin sessions.\npackage session\n\nimport (\n\t\"encoding/gob\"\n\t\"net/http\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/database/model\"\n\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nconst (\n\tloginUserKey = \"LOGIN_USER\"\n\tdefaultPath  = \"/\"\n)\n\nfunc init() {\n\tgob.Register(model.User{})\n}\n\n// SetLoginUser stores the authenticated user in the session.\n// The user object is serialized and stored for subsequent requests.\nfunc SetLoginUser(c *gin.Context, user *model.User) {\n\tif user == nil {\n\t\treturn\n\t}\n\ts := sessions.Default(c)\n\ts.Set(loginUserKey, *user)\n}\n\n// SetMaxAge configures the session cookie maximum age in seconds.\n// This controls how long the session remains valid before requiring re-authentication.\nfunc SetMaxAge(c *gin.Context, maxAge int) {\n\ts := sessions.Default(c)\n\ts.Options(sessions.Options{\n\t\tPath:     defaultPath,\n\t\tMaxAge:   maxAge,\n\t\tHttpOnly: true,\n\t\tSameSite: http.SameSiteLaxMode,\n\t})\n}\n\n// GetLoginUser retrieves the authenticated user from the session.\n// Returns nil if no user is logged in or if the session data is invalid.\nfunc GetLoginUser(c *gin.Context) *model.User {\n\ts := sessions.Default(c)\n\tobj := s.Get(loginUserKey)\n\tif obj == nil {\n\t\treturn nil\n\t}\n\tuser, ok := obj.(model.User)\n\tif !ok {\n\n\t\ts.Delete(loginUserKey)\n\t\treturn nil\n\t}\n\treturn &user\n}\n\n// IsLogin checks if a user is currently authenticated in the session.\n// Returns true if a valid user session exists, false otherwise.\nfunc IsLogin(c *gin.Context) bool {\n\treturn GetLoginUser(c) != nil\n}\n\n// ClearSession removes all session data and invalidates the session.\n// This effectively logs out the user and clears any stored session information.\nfunc ClearSession(c *gin.Context) {\n\ts := sessions.Default(c)\n\ts.Clear()\n\ts.Options(sessions.Options{\n\t\tPath:     defaultPath,\n\t\tMaxAge:   -1,\n\t\tHttpOnly: true,\n\t\tSameSite: http.SameSiteLaxMode,\n\t})\n}\n"
  },
  {
    "path": "web/translation/translate.ar_EG.toml",
    "content": "\"username\" = \"اسم المستخدم\"\n\"password\" = \"الباسورد\"\n\"login\" = \"تسجيل الدخول\"\n\"confirm\" = \"تأكيد\"\n\"cancel\" = \"إلغاء\"\n\"close\" = \"إغلاق\"\n\"create\" = \"إنشاء\"\n\"update\" = \"تحديث\"\n\"copy\" = \"نسخ\"\n\"copied\" = \"اتنسخ\"\n\"download\" = \"تحميل\"\n\"remark\" = \"ملاحظة\"\n\"enable\" = \"مفعل\"\n\"protocol\" = \"بروتوكول\"\n\"search\" = \"بحث\"\n\"filter\" = \"فلترة\"\n\"loading\" = \"جاري التحميل...\"\n\"second\" = \"ثانية\"\n\"minute\" = \"دقيقة\"\n\"hour\" = \"ساعة\"\n\"day\" = \"يوم\"\n\"check\" = \"شيك\"\n\"indefinite\" = \"غير محدد\"\n\"unlimited\" = \"غير محدود\"\n\"none\" = \"مفيش\"\n\"qrCode\" = \"كود QR\"\n\"info\" = \"معلومات أكتر\"\n\"edit\" = \"تعديل\"\n\"delete\" = \"مسح\"\n\"reset\" = \"إعادة ضبط\"\n\"noData\" = \"لا توجد بيانات.\"\n\"copySuccess\" = \"اتنسخ بنجاح\"\n\"sure\" = \"متأكد؟\"\n\"encryption\" = \"تشفير\"\n\"useIPv4ForHost\" = \"استخدم IPv4 للمضيف\"\n\"transmission\" = \"نقل\"\n\"host\" = \"المستضيف\"\n\"path\" = \"مسار\"\n\"camouflage\" = \"تمويه\"\n\"status\" = \"الحالة\"\n\"enabled\" = \"مفعل\"\n\"disabled\" = \"معطل\"\n\"depleted\" = \"خلص\"\n\"depletingSoon\" = \"هينتهي قريب\"\n\"offline\" = \"أوفلاين\"\n\"online\" = \"أونلاين\"\n\"domainName\" = \"اسم الدومين\"\n\"monitor\" = \"المسمع IP\"\n\"certificate\" = \"شهادة رقمية\"\n\"fail\" = \"فشل\"\n\"comment\" = \"تعليق\"\n\"success\" = \"تم بنجاح\"\n\"lastOnline\" = \"آخر متصل\"\n\"getVersion\" = \"جيب النسخة\"\n\"install\" = \"تثبيت\"\n\"clients\" = \"عملاء\"\n\"usage\" = \"استخدام\"\n\"twoFactorCode\" = \"الكود\"\n\"remained\" = \"المتبقي\"\n\"security\" = \"أمان\"\n\"secAlertTitle\" = \"تنبيه أمني\"\n\"secAlertSsl\" = \"الاتصال ده مش آمن. ابعد عن إدخال معلومات حساسة لغاية ما تشغل TLS لحماية البيانات.\"\n\"secAlertConf\" = \"بعض الإعدادات معرضة لهجمات. ينصح بتعزيز بروتوكولات الأمان عشان تمنع الاختراقات المحتملة.\"\n\"secAlertSSL\" = \"البانل مش مؤمن. حمّل شهادة TLS لحماية البيانات.\"\n\"secAlertPanelPort\" = \"بورت البانل الافتراضي معرض للخطر. ياريت تغير لبورت عشوائي أو محدد.\"\n\"secAlertPanelURI\" = \"مسار URI الافتراضي للبانل مش آمن. ياريت تضبط مسار URI معقد.\"\n\"secAlertSubURI\" = \"مسار URI الافتراضي للاشتراك مش آمن. ياريت تضبط مسار URI معقد.\"\n\"secAlertSubJsonURI\" = \"مسار URI الافتراضي لاشتراك JSON مش آمن. ياريت تضبط مسار URI معقد.\"\n\"emptyDnsDesc\" = \"مفيش سيرفر DNS مضاف.\"\n\"emptyFakeDnsDesc\" = \"مفيش سيرفر Fake DNS مضاف.\"\n\"emptyBalancersDesc\" = \"مفيش موازن تحميل مضاف.\"\n\"emptyReverseDesc\" = \"مفيش بروكسي عكسي مضاف.\"\n\"somethingWentWrong\" = \"حدث خطأ ما\"\n\n[subscription]\n\"title\" = \"معلومات الاشتراك\"\n\"subId\" = \"معرّف الاشتراك\"\n\"status\" = \"الحالة\"\n\"downloaded\" = \"التنزيل\"\n\"uploaded\" = \"الرفع\"\n\"expiry\" = \"تاريخ الانتهاء\"\n\"totalQuota\" = \"الحصة الإجمالية\"\n\"individualLinks\" = \"روابط فردية\"\n\"active\" = \"نشط\"\n\"inactive\" = \"غير نشط\"\n\"unlimited\" = \"غير محدود\"\n\"noExpiry\" = \"بدون انتهاء\"\n\n[menu]\n\"theme\" = \"الثيم\"\n\"dark\" = \"داكن\"\n\"ultraDark\" = \"داكن جدًا\"\n\"dashboard\" = \"نظرة عامة\"\n\"inbounds\" = \"الإدخالات\"\n\"settings\" = \"إعدادات البانل\"\n\"xray\" = \"إعدادات Xray\"\n\"logout\" = \"تسجيل خروج\"\n\"link\" = \"إدارة\"\n\n[pages.login]\n\"hello\" = \"أهلا\"\n\"title\" = \"أهلاً وسهلاً\"\n\"loginAgain\" = \"انتهت صلاحية الجلسة، سجل دخول تاني\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"تنسيق البيانات المدخلة مش صحيح.\"\n\"emptyUsername\" = \"اسم المستخدم مطلوب\"\n\"emptyPassword\" = \"الباسورد مطلوب\"\n\"wrongUsernameOrPassword\" = \"اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح.\"\n\"successLogin\" = \"لقد تم تسجيل الدخول إلى حسابك بنجاح.\"\n\n[pages.index]\n\"title\" = \"نظرة عامة\"\n\"cpu\" = \"المعالج\"\n\"logicalProcessors\" = \"المعالجات المنطقية\"\n\"frequency\" = \"التردد\"\n\"swap\" = \"Swap\"\n\"storage\" = \"تخزين\"\n\"memory\" = \"رام\"\n\"threads\" = \"خيوط المعالجة\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"إيقاف\"\n\"restartXray\" = \"إعادة تشغيل\"\n\"xraySwitch\" = \"النسخة\"\n\"xraySwitchClick\" = \"اختار النسخة اللي عايز تتحول لها.\"\n\"xraySwitchClickDesk\" = \"اختار بحذر، النسخ القديمة ممكن ما تتوافقش مع الإعدادات الحالية.\"\n\"xrayStatusUnknown\" = \"مش معروف\"\n\"xrayStatusRunning\" = \"شغالة\"\n\"xrayStatusStop\" = \"متوقفة\"\n\"xrayStatusError\" = \"فيها غلطة\"\n\"xrayErrorPopoverTitle\" = \"حصل خطأ أثناء تشغيل Xray\"\n\"operationHours\" = \"مدة التشغيل\"\n\"systemLoad\" = \"تحميل النظام\"\n\"systemLoadDesc\" = \"متوسط تحميل النظام في الدقائق 1, 5, و15\"\n\"connectionCount\" = \"إحصائيات الاتصال\"\n\"ipAddresses\" = \"عناوين IP\"\n\"toggleIpVisibility\" = \"بدل إظهار IP\"\n\"overallSpeed\" = \"السرعة الكلية\"\n\"upload\" = \"رفع\"\n\"download\" = \"تنزيل\"\n\"totalData\" = \"إجمالي البيانات\"\n\"sent\" = \"مرسل\"\n\"received\" = \"مستقبل\"\n\"documentation\" = \"التوثيق\"\n\"xraySwitchVersionDialog\" = \"هل تريد حقًا تغيير إصدار Xray؟\"\n\"xraySwitchVersionDialogDesc\" = \"سيؤدي هذا إلى تغيير إصدار Xray إلى #version#.\"\n\"xraySwitchVersionPopover\" = \"تم تحديث Xray بنجاح\"\n\"geofileUpdateDialog\" = \"هل تريد حقًا تحديث ملف الجغرافيا؟\"\n\"geofileUpdateDialogDesc\" = \"سيؤدي هذا إلى تحديث ملف #filename#.\"\n\"geofilesUpdateDialogDesc\" = \"سيؤدي هذا إلى تحديث كافة الملفات.\"\n\"geofilesUpdateAll\" = \"تحديث الكل\"\n\"geofileUpdatePopover\" = \"تم تحديث ملف الجغرافيا بنجاح\"\n\"dontRefresh\" = \"التثبيت شغال، متعملش Refresh للصفحة\"\n\"logs\" = \"السجلات\"\n\"config\" = \"الإعدادات\"\n\"backup\" = \"نسخة احتياطية\"\n\"backupTitle\" = \"نسخة احتياطية واسترجاع قاعدة البيانات\"\n\"exportDatabase\" = \"اخزن نسخة\"\n\"exportDatabaseDesc\" = \"اضغط عشان تحمل ملف .db يحتوي على نسخة احتياطية لقاعدة البيانات الحالية على جهازك.\"\n\"importDatabase\" = \"استرجاع\"\n\"importDatabaseDesc\" = \"اضغط عشان تختار وتحمل ملف .db من جهازك لاسترجاع قاعدة البيانات من نسخة احتياطية.\"\n\"importDatabaseSuccess\" = \"تم استيراد قاعدة البيانات بنجاح\"\n\"importDatabaseError\" = \"حدث خطأ أثناء استيراد قاعدة البيانات\"\n\"readDatabaseError\" = \"حدث خطأ أثناء قراءة قاعدة البيانات\"\n\"getDatabaseError\" = \"حدث خطأ أثناء استرجاع قاعدة البيانات\"\n\"getConfigError\" = \"حدث خطأ أثناء استرجاع ملف الإعدادات\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"إجمالي حركة المرور\"\n\"allTimeTrafficUsage\" = \"إجمالي الاستخدام طوال الوقت\"\n\"title\" = \"الإدخالات\"\n\"totalDownUp\" = \"إجمالي المرسل/المستقبل\"\n\"totalUsage\" = \"إجمالي الاستخدام\"\n\"inboundCount\" = \"عدد الإدخالات\"\n\"operate\" = \"القائمة\"\n\"enable\" = \"مفعل\"\n\"remark\" = \"ملاحظة\"\n\"protocol\" = \"بروتوكول\"\n\"port\" = \"بورت\"\n\"portMap\" = \"خريطة البورت\"\n\"traffic\" = \"الترافيك\"\n\"details\" = \"تفاصيل\"\n\"transportConfig\" = \"نقل\"\n\"expireDate\" = \"المدة\"\n\"createdAt\" = \"تاريخ الإنشاء\"\n\"updatedAt\" = \"تاريخ التحديث\"\n\"resetTraffic\" = \"إعادة ضبط الترافيك\"\n\"addInbound\" = \"أضف إدخال\"\n\"generalActions\" = \"إجراءات عامة\"\n\"autoRefresh\" = \"تحديث تلقائي\"\n\"autoRefreshInterval\" = \"الفاصل\"\n\"modifyInbound\" = \"تعديل الإدخال\"\n\"deleteInbound\" = \"حذف الإدخال\"\n\"deleteInboundContent\" = \"متأكد إنك عايز تحذف الإدخال؟\"\n\"deleteClient\" = \"حذف العميل\"\n\"deleteClientContent\" = \"متأكد إنك عايز تحذف العميل؟\"\n\"resetTrafficContent\" = \"متأكد إنك عايز تعيد ضبط الترافيك؟\"\n\"copyLink\" = \"انسخ الرابط\"\n\"address\" = \"العنوان\"\n\"network\" = \"الشبكة\"\n\"destinationPort\" = \"بورت الوجهة\"\n\"targetAddress\" = \"عنوان الهدف\"\n\"monitorDesc\" = \"سيبها فاضية لو عايز تستمع على كل الـ IPs\"\n\"meansNoLimit\" = \"= غير محدود. (الوحدة: جيجابايت)\"\n\"totalFlow\" = \"إجمالي التدفق\"\n\"leaveBlankToNeverExpire\" = \"سيبها فاضية عشان ماتنتهيش\"\n\"noRecommendKeepDefault\" = \"ننصح باستخدام الافتراضي\"\n\"certificatePath\" = \"مسار الملف\"\n\"certificateContent\" = \"محتوى الملف\"\n\"publicKey\" = \"المفتاح العام\"\n\"privatekey\" = \"المفتاح الخاص\"\n\"clickOnQRcode\" = \"اضغط على كود QR للنسخ\"\n\"client\" = \"عميل\"\n\"export\" = \"تصدير كل الروابط\"\n\"clone\" = \"استنساخ\"\n\"cloneInbound\" = \"استنساخ الإدخال\"\n\"cloneInboundContent\" = \"كل إعدادات الإدخال ده، غير البورت، IP الاستماع، والعملاء، هتتطبق على الاستنساخ.\"\n\"cloneInboundOk\" = \"استنساخ\"\n\"resetAllTraffic\" = \"إعادة ضبط ترافيك كل الإدخالات\"\n\"resetAllTrafficTitle\" = \"إعادة ضبط ترافيك كل الإدخالات\"\n\"resetAllTrafficContent\" = \"متأكد إنك عايز تعيد ضبط الترافيك لكل الإدخالات؟\"\n\"resetInboundClientTraffics\" = \"إعادة ضبط ترافيك العملاء\"\n\"resetInboundClientTrafficTitle\" = \"إعادة ضبط ترافيك العملاء\"\n\"resetInboundClientTrafficContent\" = \"متأكد إنك عايز تعيد ضبط ترافيك عملاء الإدخال ده؟\"\n\"resetAllClientTraffics\" = \"إعادة ضبط ترافيك كل العملاء\"\n\"resetAllClientTrafficTitle\" = \"إعادة ضبط ترافيك كل العملاء\"\n\"resetAllClientTrafficContent\" = \"متأكد إنك عايز تعيد ضبط ترافيك كل العملاء؟\"\n\"delDepletedClients\" = \"حذف العملاء اللي خلصت\"\n\"delDepletedClientsTitle\" = \"حذف العملاء اللي خلصت\"\n\"delDepletedClientsContent\" = \"متأكد إنك عايز تحذف كل العملاء اللي خلصت؟\"\n\"email\" = \"الإيميل\"\n\"emailDesc\" = \"ادخل إيميل فريد.\"\n\"IPLimit\" = \"تحديد IP\"\n\"IPLimitDesc\" = \"بيعطل الإدخال لو العدد زاد عن القيمة المحددة. (0 = تعطيل)\"\n\"IPLimitlog\" = \"سجل IP\"\n\"IPLimitlogDesc\" = \"سجل تاريخ الـ IPs. (عشان تفعل الإدخال بعد التعطيل، امسح السجل)\"\n\"IPLimitlogclear\" = \"امسح السجل\"\n\"setDefaultCert\" = \"استخدم شهادة البانل\"\n\"telegramDesc\" = \"ادخل ID شات Telegram. (استخدم '/id' في البوت) أو (@userinfobot)\"\n\"subscriptionDesc\" = \"عشان تلاقي رابط الاشتراك، ادخل على 'التفاصيل'. وكمان ممكن تستخدم نفس الاسم لعدة عملاء.\"\n\"info\" = \"معلومات\"\n\"same\" = \"نفسه\"\n\"inboundData\" = \"بيانات الإدخال\"\n\"exportInbound\" = \"تصدير الإدخال\"\n\"import\" = \"استيراد\"\n\"importInbound\" = \"استيراد إدخال\"\n\"periodicTrafficResetTitle\" = \"إعادة تعيين حركة المرور\"\n\"periodicTrafficResetDesc\" = \"إعادة تعيين عداد حركة المرور تلقائيًا في فترات محددة\"\n\"lastReset\" = \"آخر إعادة تعيين\"\n\n[pages.client]\n\"add\" = \"أضف عميل\"\n\"edit\" = \"تعديل عميل\"\n\"submitAdd\" = \"أضف العميل\"\n\"submitEdit\" = \"احفظ التعديلات\"\n\"clientCount\" = \"عدد العملاء\"\n\"bulk\" = \"إضافة بالجملة\"\n\"method\" = \"طريقة\"\n\"first\" = \"أول واحد\"\n\"last\" = \"آخر واحد\"\n\"prefix\" = \"بادئة\"\n\"postfix\" = \"لاحقة\"\n\"delayedStart\" = \"ابدأ بعد أول استخدام\"\n\"expireDays\" = \"المدة\"\n\"days\" = \"يوم/أيام\"\n\"renew\" = \"تجديد تلقائي\"\n\"renewDesc\" = \"تجديد تلقائي بعد انتهاء الصلاحية. (0 = تعطيل)(الوحدة: يوم)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"أبداً\"\n\"daily\" = \"يومياً\"\n\"weekly\" = \"أسبوعياً\"\n\"monthly\" = \"شهرياً\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"تم الحصول عليه\"\n\"updateSuccess\" = \"تم التحديث بنجاح\"\n\"logCleanSuccess\" = \"تم مسح السجل\"\n\"inboundsUpdateSuccess\" = \"تم تحديث الواردات بنجاح\"\n\"inboundUpdateSuccess\" = \"تم تحديث الوارد بنجاح\"\n\"inboundCreateSuccess\" = \"تم إنشاء الوارد بنجاح\"\n\"inboundDeleteSuccess\" = \"تم حذف الوارد بنجاح\"\n\"inboundClientAddSuccess\" = \"تمت إضافة عميل(عملاء) وارد\"\n\"inboundClientDeleteSuccess\" = \"تم حذف عميل وارد\"\n\"inboundClientUpdateSuccess\" = \"تم تحديث عميل وارد\"\n\"delDepletedClientsSuccess\" = \"تم حذف جميع العملاء المستنفذين\"\n\"resetAllClientTrafficSuccess\" = \"تم إعادة تعيين كل حركة المرور من العميل\"\n\"resetAllTrafficSuccess\" = \"تم إعادة تعيين كل حركة المرور\"\n\"resetInboundClientTrafficSuccess\" = \"تم إعادة تعيين حركة المرور\"\n\"trafficGetError\" = \"خطأ في الحصول على حركات المرور\"\n\"getNewX25519CertError\" = \"حدث خطأ أثناء الحصول على شهادة X25519.\"\n\"getNewmldsa65Error\" = \"حدث خطاء في الحصول على mldsa65.\"\n\"getNewVlessEncError\" = \"حدث خطأ أثناء الحصول على VlessEnc.\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"طلب\"\n\"response\" = \"رد\"\n\"name\" = \"اسم\"\n\"value\" = \"قيمة\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"نسخة\"\n\"method\" = \"طريقة\"\n\"path\" = \"مسار\"\n\"status\" = \"الحالة\"\n\"statusDescription\" = \"وصف الحالة\"\n\"requestHeader\" = \"رأس الطلب\"\n\"responseHeader\" = \"رأس الرد\"\n\n[pages.settings]\n\"title\" = \"إعدادات البانل\"\n\"save\" = \"حفظ\"\n\"infoDesc\" = \"كل تغيير هتعمله هنا لازم يتخزن. ياريت تعيد تشغيل البانل عشان التعديلات تتفعل.\"\n\"restartPanel\" = \"إعادة تشغيل البانل\"\n\"restartPanelDesc\" = \"متأكد إنك عايز تعيد تشغيل البانل؟ لو ماقدرتش تدخل بعد إعادة التشغيل، شوف سجل البانل على السيرفر.\"\n\"restartPanelSuccess\" = \"تم إعادة تشغيل اللوحة بنجاح\"\n\"actions\" = \"إجراءات\"\n\"resetDefaultConfig\" = \"استرجاع الافتراضي\"\n\"panelSettings\" = \"عام\"\n\"securitySettings\" = \"المصادقة\"\n\"TGBotSettings\" = \"بوت Telegram\"\n\"panelListeningIP\" = \"IP الاستماع\"\n\"panelListeningIPDesc\" = \"عنوان IP للبانل. (سيبه فاضي عشان يستمع على كل الـ IPs)\"\n\"panelListeningDomain\" = \"دومين الاستماع\"\n\"panelListeningDomainDesc\" = \"اسم الدومين للبانل. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)\"\n\"panelPort\" = \"بورت الاستماع\"\n\"panelPortDesc\" = \"رقم البورت للبانل. (لازم يكون بورت فاضي)\"\n\"publicKeyPath\" = \"مسار المفتاح العام\"\n\"publicKeyPathDesc\" = \"مسار ملف المفتاح العام للبانل. (يبدأ بـ '/')\"\n\"privateKeyPath\" = \"مسار المفتاح الخاص\"\n\"privateKeyPathDesc\" = \"مسار ملف المفتاح الخاص للبانل. (يبدأ بـ '/')\"\n\"panelUrlPath\" = \"مسار URI\"\n\"panelUrlPathDesc\" = \"مسار URI للبانل. (يبدأ بـ '/' وبينتهي بـ '/')\"\n\"pageSize\" = \"حجم الصفحة\"\n\"pageSizeDesc\" = \"حدد حجم الصفحة لجدول الإدخالات. (0 = تعطيل)\"\n\"remarkModel\" = \"نموذج الملاحظة وحرف الفصل\"\n\"datepicker\" = \"نوع التقويم\"\n\"datepickerPlaceholder\" = \"اختار التاريخ\"\n\"datepickerDescription\" = \"المهام المجدولة هتشتغل بناءً على التقويم ده.\"\n\"sampleRemark\" = \"مثال للملاحظة\"\n\"oldUsername\" = \"اسم المستخدم الحالي\"\n\"currentPassword\" = \"الباسورد الحالي\"\n\"newUsername\" = \"اسم المستخدم الجديد\"\n\"newPassword\" = \"الباسورد الجديد\"\n\"telegramBotEnable\" = \"تفعيل بوت Telegram\"\n\"telegramBotEnableDesc\" = \"يفعل بوت Telegram.\"\n\"telegramToken\" = \"توكن Telegram\"\n\"telegramTokenDesc\" = \"توكن البوت اللي جبت من '@BotFather'.\"\n\"telegramProxy\" = \"بروكسي SOCKS\"\n\"telegramProxyDesc\" = \"يفعل بروكسي SOCKS5 للاتصال بـ Telegram. (اضبط الإعدادات حسب الدليل)\"\n\"telegramAPIServer\" = \"سيرفر Telegram API\"\n\"telegramAPIServerDesc\" = \"سيرفر Telegram API المستخدم. سيبه فاضي لاستخدام الافتراضي.\"\n\"telegramChatId\" = \"ID شات الأدمن\"\n\"telegramChatIdDesc\" = \"ID شات الأدمن في Telegram. (مفصول بفواصل)(تقدر تجيبه من @userinfobot) أو (استخدم '/id' في البوت)\"\n\"telegramNotifyTime\" = \"وقت الإشعار\"\n\"telegramNotifyTimeDesc\" = \"وقت إشعار البوت للتقارير الدورية. (استخدم صيغة وقت crontab)\"\n\"tgNotifyBackup\" = \"نسخة احتياطية لقاعدة البيانات\"\n\"tgNotifyBackupDesc\" = \"ابعت ملف النسخة الاحتياطية لقاعدة البيانات مع التقرير.\"\n\"tgNotifyLogin\" = \"إشعار بتسجيل الدخول\"\n\"tgNotifyLoginDesc\" = \"استقبل إشعار بكل محاولة تسجيل دخول للبانل مع اسم المستخدم، الـ IP، والوقت.\"\n\"sessionMaxAge\" = \"مدة الجلسة\"\n\"sessionMaxAgeDesc\" = \"المدة اللي تفضل فيها مسجل دخول. (الوحدة: دقيقة)\"\n\"expireTimeDiff\" = \"تنبيه بتاريخ الانتهاء\"\n\"expireTimeDiffDesc\" = \"استقبل تنبيه قبل ما توصل لتاريخ الانتهاء بالمدة المحددة. (الوحدة: يوم)\"\n\"trafficDiff\" = \"تنبيه حد الترافيك\"\n\"trafficDiffDesc\" = \"استقبل تنبيه عند وصول الترافيك للحد المحدد. (الوحدة: جيجابايت)\"\n\"tgNotifyCpu\" = \"تنبيه حمل المعالج\"\n\"tgNotifyCpuDesc\" = \"استقبل تنبيه لو حمل المعالج عدى الحد المحدد. (الوحدة: %)\"\n\"timeZone\" = \"المنطقة الزمنية\"\n\"timeZoneDesc\" = \"المهام المجدولة هتشتغل بناءً على المنطقة الزمنية دي.\"\n\"subSettings\" = \"الاشتراك\"\n\"subEnable\" = \"تفعيل خدمة الاشتراك\"\n\"subEnableDesc\" = \"يفعل خدمة الاشتراك.\"\n\"subJsonEnable\" = \"تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل.\"\n\"subTitle\" = \"عنوان الاشتراك\"\n\"subTitleDesc\" = \"العنوان اللي هيظهر في عميل VPN\"\n\"subSupportUrl\" = \"رابط الدعم\"\n\"subSupportUrlDesc\" = \"رابط الدعم الفني المعروض في عميل VPN\"\n\"subProfileUrl\" = \"رابط الملف الشخصي\"\n\"subProfileUrlDesc\" = \"رابط لموقعك الإلكتروني يظهر في عميل VPN\"\n\"subAnnounce\" = \"إعلان\"\n\"subAnnounceDesc\" = \"نص الإعلان المعروض في عميل VPN\"\n\"subEnableRouting\" = \"تفعيل التوجيه\"\n\"subEnableRoutingDesc\" = \"إعداد عام لتمكين التوجيه (Routing) في عميل VPN. (فقط لـ Happ)\"\n\"subRoutingRules\" = \"قواعد التوجيه\"\n\"subRoutingRulesDesc\" = \"قواعد التوجيه العامة لعميل VPN. (فقط لـ Happ)\"\n\"subListen\" = \"IP الاستماع\"\n\"subListenDesc\" = \"عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)\"\n\"subPort\" = \"بورت الاستماع\"\n\"subPortDesc\" = \"رقم البورت لخدمة الاشتراك. (لازم يكون بورت فاضي)\"\n\"subCertPath\" = \"مسار المفتاح العام\"\n\"subCertPathDesc\" = \"مسار ملف المفتاح العام لخدمة الاشتراك. (يبدأ بـ '/')\"\n\"subKeyPath\" = \"مسار المفتاح الخاص\"\n\"subKeyPathDesc\" = \"مسار ملف المفتاح الخاص لخدمة الاشتراك. (يبدأ بـ '/')\"\n\"subPath\" = \"مسار URI\"\n\"subPathDesc\" = \"مسار URI لخدمة الاشتراك. (يبدأ بـ '/' وبينتهي بـ '/')\"\n\"subDomain\" = \"دومين الاستماع\"\n\"subDomainDesc\" = \"اسم الدومين لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)\"\n\"subUpdates\" = \"فترات التحديث\"\n\"subUpdatesDesc\" = \"فترات تحديث رابط الاشتراك في تطبيقات العملاء. (الوحدة: ساعة)\"\n\"subEncrypt\" = \"تشفير\"\n\"subEncryptDesc\" = \"المحتوى اللي هيترجع من خدمة الاشتراك هيكون مشفر بـ Base64.\"\n\"subShowInfo\" = \"اظهر معلومات الاستخدام\"\n\"subShowInfoDesc\" = \"هيظهر الترافيك المتبقي والتاريخ في تطبيقات العملاء.\"\n\"subURI\" = \"مسار البروكسي العكسي\"\n\"subURIDesc\" = \"مسار URI لرابط الاشتراك عشان تستخدمه ورا البروكسي.\"\n\"externalTrafficInformEnable\" = \"تنبيه الترافيك الخارجي\"\n\"externalTrafficInformEnableDesc\" = \"يبعت تنبيه لـ API خارجي مع كل تحديث للترافيك.\"\n\"externalTrafficInformURI\" = \"مسار تنبيه الترافيك الخارجي\"\n\"externalTrafficInformURIDesc\" = \"تحديثات الترافيك هتتبعت للمسار ده.\"\n\"fragment\" = \"تجزئة\"\n\"fragmentDesc\" = \"يفعل تجزئة لحزمة TLS hello.\"\n\"fragmentSett\" = \"إعدادات التجزئة\"\n\"noisesDesc\" = \"يفعل التشويش.\"\n\"noisesSett\" = \"إعدادات التشويش\"\n\"mux\" = \"MUX\"\n\"muxDesc\" = \"ينقل أكثر من تيار بيانات مستقل خلال تيار بيانات واحد قائم.\"\n\"muxSett\" = \"إعدادات MUX\"\n\"direct\" = \"اتصال مباشر\"\n\"directDesc\" = \"ينشئ اتصال مباشر مع الدومينات أو نطاقات IP لدولة معينة.\"\n\"notifications\" = \"الإشعارات\"\n\"certs\" = \"الشهادات\"\n\"externalTraffic\" = \"الترافيك الخارجي\"\n\"dateAndTime\" = \"التاريخ والوقت\"\n\"proxyAndServer\" = \"البروكسي والسيرفر\"\n\"intervals\" = \"الفترات\"\n\"information\" = \"المعلومات\"\n\"language\" = \"اللغة\"\n\"telegramBotLanguage\" = \"لغة بوت Telegram\"\n\n[pages.xray]\n\"title\" = \"إعدادات Xray\"\n\"save\" = \"احفظ\"\n\"restart\" = \"أعد تشغيل Xray\"\n\"restartSuccess\" = \"تم إعادة تشغيل Xray بنجاح\"\n\"stopSuccess\" = \"تم إيقاف Xray بنجاح\"\n\"restartError\" = \"حدث خطأ أثناء إعادة تشغيل Xray.\"\n\"stopError\" = \"حدث خطأ أثناء إيقاف Xray.\"\n\"basicTemplate\" = \"أساسي\"\n\"advancedTemplate\" = \"متقدم\"\n\"generalConfigs\" = \"إعدادات عامة\"\n\"generalConfigsDesc\" = \"الخيارات دي هتحدد التعديلات العامة.\"\n\"logConfigs\" = \"السجلات\"\n\"logConfigsDesc\" = \"السجلات ممكن تأثر على كفاءة السيرفر. ننصح بتفعيلها بحكمة لما تكون محتاجها.\"\n\"blockConfigsDesc\" = \"الخيارات دي هتحجب الترافيك بناءً على بروتوكولات ومواقع محددة.\"\n\"basicRouting\" = \"توجيه أساسي\"\n\"blockConnectionsConfigsDesc\" = \"الخيارات دي هتحجب الترافيك بناءً على الدولة المطلوبة.\"\n\"directConnectionsConfigsDesc\" = \"الاتصال المباشر بيضمن إن الترافيك المعين مايمرش من سيرفر تاني.\"\n\"blockips\" = \"حظر IPs\"\n\"blockdomains\" = \"حظر دومينات\"\n\"directips\" = \"اتصالات مباشرة لـ IPs\"\n\"directdomains\" = \"اتصالات مباشرة للدومينات\"\n\"ipv4Routing\" = \"توجيه IPv4\"\n\"ipv4RoutingDesc\" = \"الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر IPv4.\"\n\"warpRouting\" = \"توجيه WARP\"\n\"warpRoutingDesc\" = \"الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر WARP.\"\n\"Template\" = \"قالب إعدادات Xray المتقدم\"\n\"TemplateDesc\" = \"ملف إعدادات Xray النهائي هيتولد بناءً على القالب ده.\"\n\"FreedomStrategy\" = \"استراتيجية بروتوكول الحرية\"\n\"FreedomStrategyDesc\" = \"اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية.\"\n\"RoutingStrategy\" = \"استراتيجية التوجيه العامة\"\n\"RoutingStrategyDesc\" = \"حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات.\"\n\"outboundTestUrl\" = \"رابط اختبار المخرج\"\n\"outboundTestUrlDesc\" = \"الرابط المستخدم عند اختبار اتصال المخرج\"\n\"Torrent\" = \"حظر بروتوكول التورنت\"\n\"Inbounds\" = \"الإدخالات\"\n\"InboundsDesc\" = \"قبول العملاء المعينين.\"\n\"Outbounds\" = \"المخرجات\"\n\"Balancers\" = \"موازنات التحميل\"\n\"OutboundsDesc\" = \"حدد مسار الترافيك الصادر.\"\n\"Routings\" = \"قواعد التوجيه\"\n\"RoutingsDesc\" = \"أولوية كل قاعدة مهمة جداً!\"\n\"completeTemplate\" = \"الكل\"\n\"logLevel\" = \"مستوى السجلات\"\n\"logLevelDesc\" = \"مستوى السجل الخاص بالأخطاء، اللي بيوضح المعلومات المطلوبة للتسجيل.\"\n\"accessLog\" = \"سجل الوصول\"\n\"accessLogDesc\" = \"مسار ملف سجل الوصول. القيمة الخاصة 'none' بتعطل سجل الوصول.\"\n\"errorLog\" = \"سجل الأخطاء\"\n\"errorLogDesc\" = \"مسار ملف سجل الأخطاء. القيمة الخاصة 'none' بتعطل سجل الأخطاء.\"\n\"dnsLog\" = \"سجل DNS\"\n\"dnsLogDesc\" = \"لو هتسجل استعلامات DNS.\"\n\"maskAddress\" = \"إخفاء العنوان\"\n\"maskAddressDesc\" = \"إخفاء عنوان الـ IP؛ لو مفعل، هيستبدل تلقائياً عنوان IP اللي بيظهر في السجل.\"\n\"statistics\" = \"إحصائيات\"\n\"statsInboundUplink\" = \"إحصائيات رفع الإدخال\"\n\"statsInboundUplinkDesc\" = \"تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من الإدخالات.\"\n\"statsInboundDownlink\" = \"إحصائيات تنزيل الإدخال\"\n\"statsInboundDownlinkDesc\" = \"تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من الإدخالات.\"\n\"statsOutboundUplink\" = \"إحصائيات رفع المخرجات\"\n\"statsOutboundUplinkDesc\" = \"تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من المخرجات.\"\n\"statsOutboundDownlink\" = \"إحصائيات تنزيل المخرجات\"\n\"statsOutboundDownlinkDesc\" = \"تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من المخرجات.\"\n\n[pages.xray.rules]\n\"first\" = \"أول\"\n\"last\" = \"آخر\"\n\"up\" = \"فوق\"\n\"down\" = \"تحت\"\n\"source\" = \"المصدر\"\n\"dest\" = \"الوجهة\"\n\"inbound\" = \"إدخال\"\n\"outbound\" = \"مخرج\"\n\"balancer\" = \"موازن\"\n\"info\" = \"معلومات\"\n\"add\" = \"أضف قاعدة\"\n\"edit\" = \"عدل القاعدة\"\n\"useComma\" = \"عناصر مفصولة بفواصل\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"أضف مخرج\"\n\"addReverse\" = \"أضف عكسي\"\n\"editOutbound\" = \"عدل المخرج\"\n\"editReverse\" = \"عدل العكسي\"\n\"tag\" = \"تاج\"\n\"tagDesc\" = \"تاج فريد\"\n\"address\" = \"العنوان\"\n\"reverse\" = \"عكسي\"\n\"domain\" = \"دومين\"\n\"type\" = \"النوع\"\n\"bridge\" = \"جسر\"\n\"portal\" = \"بوابة\"\n\"link\" = \"رابط\"\n\"intercon\" = \"تواصل\"\n\"settings\" = \"إعدادات\"\n\"accountInfo\" = \"معلومات الحساب\"\n\"outboundStatus\" = \"حالة المخرج\"\n\"sendThrough\" = \"أرسل من خلال\"\n\"test\" = \"اختبار\"\n\"testResult\" = \"نتيجة الاختبار\"\n\"testing\" = \"جاري اختبار الاتصال...\"\n\"testSuccess\" = \"الاختبار ناجح\"\n\"testFailed\" = \"فشل الاختبار\"\n\"testError\" = \"فشل اختبار المخرج\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"أضف موازن تحميل\"\n\"editBalancer\" = \"عدل موازن التحميل\"\n\"balancerStrategy\" = \"استراتيجية الموازن\"\n\"balancerSelectors\" = \"المحددات\"\n\"tag\" = \"تاج\"\n\"tagDesc\" = \"تاج فريد\"\n\"balancerDesc\" = \"ماينفعش تستخدم balancerTag و outboundTag مع بعض. لو اتستخدموا مع بعض، outboundTag هو اللي هيشتغل.\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"المفتاح السري\"\n\"publicKey\" = \"المفتاح العام\"\n\"allowedIPs\" = \"عناوين IP المسموح بها\"\n\"endpoint\" = \"النهاية\"\n\"psk\" = \"المفتاح المشترك\"\n\"domainStrategy\" = \"استراتيجية الدومين\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"اسم واجهة TUN. القيمة الافتراضية هي 'xray0'\"\n\"mtuDesc\" = \"وحدة النقل الأقصى. الحد الأقصى لحجم حزم البيانات. القيمة الافتراضية هي 1500\"\n\"userLevel\" = \"مستوى المستخدم\"\n\"userLevelDesc\" = \"ستستخدم جميع الاتصالات المُرسلة عبر هذا الإدخال مستوى المستخدم هذا. القيمة الافتراضية هي 0\"\n\n[pages.xray.dns]\n\"enable\" = \"فعل DNS\"\n\"enableDesc\" = \"فعل سيرفر DNS المدمج\"\n\"tag\" = \"تاج إدخال DNS\"\n\"tagDesc\" = \"التاج ده هيبقى متاح كإدخال في قواعد التوجيه.\"\n\"clientIp\" = \"IP العميل\"\n\"clientIpDesc\" = \"بيحدد موقع العميل خلال استعلامات DNS\"\n\"disableCache\" = \"تعطيل الكاش\"\n\"disableCacheDesc\" = \"بيعطل تخزين نتائج DNS مؤقتاً\"\n\"disableFallback\" = \"تعطيل النسخ الاحتياطي\"\n\"disableFallbackDesc\" = \"بيعطل استعلامات DNS الاحتياطية\"\n\"disableFallbackIfMatch\" = \"تعطيل النسخ الاحتياطي عند التطابق\"\n\"disableFallbackIfMatchDesc\" = \"بيعطل استعلامات DNS الاحتياطية لما يتحقق تطابق مع قائمة الدومينات\"\n\"enableParallelQuery\" = \"تفعيل الاستعلام المتوازي\"\n\"enableParallelQueryDesc\" = \"تفعيل استعلامات DNS المتوازية لعدة خوادم لحل أسرع\"\n\"strategy\" = \"استراتيجية الاستعلام\"\n\"strategyDesc\" = \"الاستراتيجية العامة لحل أسماء الدومين\"\n\"add\" = \"أضف سيرفر\"\n\"edit\" = \"عدل السيرفر\"\n\"domains\" = \"الدومينات\"\n\"expectIPs\" = \"العناوين المتوقعة\"\n\"unexpectIPs\" = \"عناوين IP غير متوقعة\"\n\"useSystemHosts\" = \"استخدام ملف Hosts الخاص بالنظام\"\n\"useSystemHostsDesc\" = \"استخدام ملف hosts من نظام مثبت\"\n\"usePreset\" = \"استخدام النموذج\"\n\"dnsPresetTitle\" = \"قوالب DNS\"\n\"dnsPresetFamily\" = \"العائلي\"\n\n[pages.xray.fakedns]\n\"add\" = \"أضف Fake DNS\"\n\"edit\" = \"عدل Fake DNS\"\n\"ipPool\" = \"نطاق IP Pool\"\n\"poolSize\" = \"حجم المجموعة\"\n\n[pages.settings.security]\n\"admin\" = \"بيانات الأدمن\"\n\"twoFactor\" = \"المصادقة الثنائية\"\n\"twoFactorEnable\" = \"تفعيل المصادقة الثنائية\"\n\"twoFactorEnableDesc\" = \"يضيف طبقة إضافية من المصادقة لتعزيز الأمان.\"\n\"twoFactorModalSetTitle\" = \"تفعيل المصادقة الثنائية\"\n\"twoFactorModalDeleteTitle\" = \"تعطيل المصادقة الثنائية\"\n\"twoFactorModalSteps\" = \"لإعداد المصادقة الثنائية، قم ببعض الخطوات:\"\n\"twoFactorModalFirstStep\" = \"1. امسح رمز QR هذا في تطبيق المصادقة أو انسخ الرمز الموجود بجانب رمز QR والصقه في التطبيق\"\n\"twoFactorModalSecondStep\" = \"2. أدخل الرمز من التطبيق\"\n\"twoFactorModalRemoveStep\" = \"أدخل الرمز من التطبيق لإزالة المصادقة الثنائية.\"\n\"twoFactorModalChangeCredentialsTitle\" = \"تغيير بيانات الاعتماد\"\n\"twoFactorModalChangeCredentialsStep\" = \"أدخل الرمز من التطبيق لتغيير بيانات اعتماد المسؤول.\"\n\"twoFactorModalSetSuccess\" = \"تم إنشاء المصادقة الثنائية بنجاح\"\n\"twoFactorModalDeleteSuccess\" = \"تم حذف المصادقة الثنائية بنجاح\"\n\"twoFactorModalError\" = \"رمز خاطئ\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"تم تغيير المعلمات.\"\n\"getSettings\" = \"حدث خطأ أثناء استرداد المعلمات.\"\n\"modifyUserError\" = \"حدث خطأ أثناء تغيير بيانات اعتماد المسؤول.\"\n\"modifyUser\" = \"لقد قمت بتغيير بيانات اعتماد المسؤول بنجاح.\"\n\"originalUserPassIncorrect\" = \"اسم المستخدم أو الباسورد الحالي غير صحيح\"\n\"userPassMustBeNotEmpty\" = \"اسم المستخدم والباسورد الجديدين فاضيين\"\n\"getOutboundTrafficError\" = \"خطأ في الحصول على حركات المرور الصادرة\"\n\"resetOutboundTrafficError\" = \"خطأ في إعادة تعيين حركات المرور الصادرة\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ لوحة المفاتيح مغلقة!\"\n\"noResult\" = \"❗ لا يوجد نتائج!\"\n\"noQuery\" = \"❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!\"\n\"wentWrong\" = \"❌ حدث خطأ ما!\"\n\"noIpRecord\" = \"❗ لا يوجد سجل IP!\"\n\"noInbounds\" = \"❗ لم يتم العثور على أي وارد!\"\n\"unlimited\" = \"♾ غير محدود (إعادة تعيين)\"\n\"add\" = \"إضافة\"\n\"month\" = \"شهر\"\n\"months\" = \"أشهر\"\n\"day\" = \"يوم\"\n\"days\" = \"أيام\"\n\"hours\" = \"ساعات\"\n\"minutes\" = \"دقائق\"\n\"unknown\" = \"غير معروف\"\n\"inbounds\" = \"الواردات\"\n\"clients\" = \"العملاء\"\n\"offline\" = \"🔴 غير متصل\"\n\"online\" = \"🟢 متصل\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ أمر مش معروف.\"\n\"pleaseChoose\" = \"👇 من فضلك اختار:\\r\\n\"\n\"help\" = \"🤖 أهلا بيك في البوت! البوت ده معمول عشان يديك بيانات معينة من البانل ويسمحلك بالتعديلات.\"\n\"start\" = \"👋 أهلا <i>{{ .Firstname }}</i>.\\r\\n\"\n\"welcome\" = \"🤖 أهلا بيك في بوت إدارة <b>{{ .Hostname }}</b>.\\r\\n\"\n\"status\" = \"✅ البوت شغال!\"\n\"usage\" = \"❗ من فضلك ادخل نص للتبحث عنه!\"\n\"getID\" = \"🆔 الـ ID بتاعك: <code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"عشان تعيد تشغيل Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\nعشان تدور على إيميل عميل:\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nعشان تدور على إدخالات (مع إحصائيات العملاء):\\r\\n<code>/inbound [Remark]</code>\\r\\n\\r\\nID شات Telegram:\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"عشان تدور على الإحصائيات، استخدم الأمر ده:\\r\\n\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nID شات Telegram:\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ العملية نجحت!\"\n\"restartFailed\" = \"❗ حصل خطأ في العملية.\\r\\n\\r\\n<code>Error: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core مش شغال.\"\n\"startDesc\" = \"عرض القائمة الرئيسية\"\n\"helpDesc\" = \"مساعدة البوت\"\n\"statusDesc\" = \"التحقق من حالة البوت\"\n\"idDesc\" = \"عرض معرف Telegram الخاص بك\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 حمل المعالج {{ .Percent }}% عدى الحد المسموح ({{ .Threshold }}%)\"\n\"selectUserFailed\" = \"❌ حصل خطأ في اختيار المستخدم!\"\n\"userSaved\" = \"✅ حفظت بيانات مستخدم Telegram.\"\n\"loginSuccess\" = \"✅ تسجيل الدخول للبانل تم بنجاح.\\r\\n\"\n\"loginFailed\" = \"❗️فشل محاولة تسجيل الدخول للبانل.\\r\\n\"\n\"2faFailed\" = \"فشل 2FA\"\n\"report\" = \"🕰 التقارير المجدولة: {{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ التاريخ والوقت: {{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 السيرفر: {{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 نسخة 3X-UI: {{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 نسخة Xray: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 عناوين IP:\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ وقت التشغيل: {{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 تحميل النظام: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 الرام: {{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP: {{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP: {{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 الترافيك: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ الحالة: {{ .State }}\\r\\n\"\n\"username\" = \"👤 اسم المستخدم: {{ .Username }}\\r\\n\"\n\"password\" = \"👤 الباسورد: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ الوقت: {{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 الإدخال: {{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 البورت: {{ .Port }}\\r\\n\"\n\"expire\" = \"📅 تاريخ الانتهاء: {{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 هيخلص بعد: {{ .Time }}\\r\\n\"\n\"active\" = \"💡 مفعل: {{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 مفعل: {{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 حالة الاتصال: {{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 آخر متصل: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 الإيميل: {{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 رفع: ↑{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 تنزيل: ↓{{ .Download }}\\r\\n\"\n\"total\" = \"📊 الإجمالي: ↑↓{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 مستخدم Telegram: {{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 نفذ {{ .Type }}:\\r\\n\"\n\"exhaustedCount\" = \"🚨 عدد النفاذ لـ {{ .Type }}:\\r\\n\"\n\"onlinesCount\" = \"🌐 العملاء الأونلاين: {{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 معطل: {{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 هينتهي قريب: {{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 وقت النسخة الاحتياطية: {{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 اتحدّث في: {{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ أيوه\"\n\"no\" = \"❌ لأ\"\n\"received_id\" = \"🔑📥 الـ ID اتحدث.\"\n\"received_password\" = \"🔑📥 الباسورد اتحدث.\"\n\"received_email\" = \"📧📥 الإيميل اتحدث.\"\n\"received_comment\" = \"💬📥 التعليق اتحدث.\"\n\"id_prompt\" = \"🔑 الـ ID الافتراضي: {{ .ClientId }}\\n\\nادخل الـ ID بتاعك.\"\n\"pass_prompt\" = \"🔑 الباسورد الافتراضي: {{ .ClientPassword }}\\n\\nادخل الباسورد بتاعك.\"\n\"email_prompt\" = \"📧 الإيميل الافتراضي: {{ .ClientEmail }}\\n\\nادخل الإيميل بتاعك.\"\n\"comment_prompt\" = \"💬 التعليق الافتراضي: {{ .ClientComment }}\\n\\nادخل تعليقك.\"\n\"inbound_client_data_id\" = \"🔄 الدخول: {{ .InboundRemark }}\\n\\n🔑 المعرف: {{ .ClientId }}\\n📧 البريد الإلكتروني: {{ .ClientEmail }}\\n📊 الترافيك: {{ .ClientTraffic }}\\n📅 تاريخ الانتهاء: {{ .ClientExp }}\\n🌐 حدّ IP: {{ .IpLimit }}\\n💬 تعليق: {{ .ClientComment }}\\n\\nدلوقتي تقدر تضيف العميل على الدخول!\"\n\"inbound_client_data_pass\" = \"🔄 الدخول: {{ .InboundRemark }}\\n\\n🔑 كلمة المرور: {{ .ClientPass }}\\n📧 البريد الإلكتروني: {{ .ClientEmail }}\\n📊 الترافيك: {{ .ClientTraffic }}\\n📅 تاريخ الانتهاء: {{ .ClientExp }}\\n🌐 حدّ IP: {{ .IpLimit }}\\n💬 تعليق: {{ .ClientComment }}\\n\\nدلوقتي تقدر تضيف العميل على الدخول!\"\n\"cancel\" = \"❌ العملية اتلغت! \\n\\nممكن تبدأ من /start في أي وقت. 🔄\"\n\"error_add_client\" = \"⚠️ حصل خطأ:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"تمام، هشيل على القيمة الافتراضية. 😊\"\n\"incorrect_input\" = \"المدخلات مش صحيحة.\\nالكلمات لازم تكون متصلة من غير فراغات.\\nمثال صحيح: aaaaaa\\nمثال غلط: aaa aaa 🚫\"\n\"AreYouSure\" = \"إنت متأكد؟ 🤔\"\n\"SuccessResetTraffic\" = \"📧 البريد الإلكتروني: {{ .ClientEmail }}\\n🏁 النتيجة: ✅ تم بنجاح\"\n\"FailedResetTraffic\" = \"📧 البريد الإلكتروني: {{ .ClientEmail }}\\n🏁 النتيجة: ❌ فشل \\n\\n🛠️ الخطأ: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء.\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ اقفل الكيبورد\"\n\"cancel\" = \"❌ إلغاء\"\n\"cancelReset\" = \"❌ إلغاء إعادة الضبط\"\n\"cancelIpLimit\" = \"❌ إلغاء حد الـ IP\"\n\"confirmResetTraffic\" = \"✅ تأكيد إعادة ضبط الترافيك؟\"\n\"confirmClearIps\" = \"✅ تأكيد مسح الـ IPs؟\"\n\"confirmRemoveTGUser\" = \"✅ تأكيد حذف مستخدم Telegram؟\"\n\"confirmToggle\" = \"✅ تأكيد تفعيل/تعطيل المستخدم؟\"\n\"dbBackup\" = \"احصل على نسخة DB\"\n\"serverUsage\" = \"استخدام السيرفر\"\n\"getInbounds\" = \"احصل على الإدخالات\"\n\"depleteSoon\" = \"هينتهي قريب\"\n\"clientUsage\" = \"استخدام العميل\"\n\"onlines\" = \"العملاء الأونلاين\"\n\"commands\" = \"الأوامر\"\n\"refresh\" = \"🔄 تجديد\"\n\"clearIPs\" = \"❌ مسح الـ IPs\"\n\"removeTGUser\" = \"❌ حذف مستخدم Telegram\"\n\"selectTGUser\" = \"👤 اختار مستخدم Telegram\"\n\"selectOneTGUser\" = \"👤 اختار مستخدم Telegram:\"\n\"resetTraffic\" = \"📈 إعادة ضبط الترافيك\"\n\"resetExpire\" = \"📅 تغيير تاريخ الانتهاء\"\n\"ipLog\" = \"🔢 سجل الـ IP\"\n\"ipLimit\" = \"🔢 حد الـ IP\"\n\"setTGUser\" = \"👤 ضبط مستخدم Telegram\"\n\"toggle\" = \"🔘 تفعيل / تعطيل\"\n\"custom\" = \"🔢 مخصص\"\n\"confirmNumber\" = \"✅ تأكيد: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ تأكيد إضافة: {{ .Num }}\"\n\"limitTraffic\" = \"🚧 حد الترافيك\"\n\"getBanLogs\" = \"احصل على سجلات الحظر\"\n\"allClients\" = \"كل العملاء\"\n\"addClient\" = \"إضافة عميل\"\n\"submitDisable\" = \"إرسال كمعطّل ☑️\"\n\"submitEnable\" = \"إرسال كمفعّل ✅\"\n\"use_default\" = \"🏷️ استخدام الإعدادات الافتراضية\"\n\"change_id\" = \"⚙️🔑 المعرّف\"\n\"change_password\" = \"⚙️🔑 كلمة السر\"\n\"change_email\" = \"⚙️📧 البريد الإلكتروني\"\n\"change_comment\" = \"⚙️💬 تعليق\"\n\"ResetAllTraffics\" = \"إعادة ضبط جميع الترافيك\"\n\"SortedTrafficUsageReport\" = \"تقرير استخدام الترافيك المرتب\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ العملية نجحت!\"\n\"errorOperation\" = \"❗ حصل خطأ في العملية.\"\n\"getInboundsFailed\" = \"❌ فشل الحصول على الإدخالات.\"\n\"getClientsFailed\" = \"❌ فشل الحصول على العملاء.\"\n\"canceled\" = \"❌ {{ .Email }}: العملية اتلغت.\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}: العميل اتحدث بنجاح.\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}: الـ IPs اتحدثت بنجاح.\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}: مستخدم Telegram اتحدث بنجاح.\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}: الترافيك اتظبط بنجاح.\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: حد الترافيك اتسجل بنجاح.\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}: أيام الانتهاء اتظبطت بنجاح.\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}: حد الـ IP ({{ .Count }}) اتسجل بنجاح.\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}: الـ IPs اتمسحت بنجاح.\"\n\"getIpLog\" = \"✅ {{ .Email }}: سجل الـ IP اتجاب.\"\n\"getUserInfo\" = \"✅ {{ .Email }}: بيانات مستخدم Telegram اتجاب.\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}: مستخدم Telegram اتحذف بنجاح.\"\n\"enableSuccess\" = \"✅ {{ .Email }}: اتفعل بنجاح.\"\n\"disableSuccess\" = \"✅ {{ .Email }}: اتعطل بنجاح.\"\n\"askToAddUserId\" = \"مافيش إعدادات ليك!\\r\\nاطلب من الأدمن يضيف الـ Telegram ChatID الخاص بيك في إعداداتك.\\r\\n\\r\\nالـ ChatID بتاعك: <code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"اختار عميل للإدخال {{ .Inbound }}\"\n\"chooseInbound\" = \"اختار الإدخال\"\n"
  },
  {
    "path": "web/translation/translate.en_US.toml",
    "content": "\"username\" = \"Username\"\n\"password\" = \"Password\"\n\"login\" = \"Log In\"\n\"confirm\" = \"Confirm\"\n\"cancel\" = \"Cancel\"\n\"close\" = \"Close\"\n\"create\" = \"Create\"\n\"update\" = \"Update\"\n\"copy\" = \"Copy\"\n\"copied\" = \"Copied\"\n\"download\" = \"Download\"\n\"remark\" = \"Remark\"\n\"enable\" = \"Enabled\"\n\"protocol\" = \"Protocol\"\n\"search\" = \"Search\"\n\"filter\" = \"Filter\"\n\"loading\" = \"Loading...\"\n\"second\" = \"Second\"\n\"minute\" = \"Minute\"\n\"hour\" = \"Hour\"\n\"day\" = \"Day\"\n\"check\" = \"Check\"\n\"indefinite\" = \"Indefinite\"\n\"unlimited\" = \"Unlimited\"\n\"none\" = \"None\"\n\"qrCode\" = \"QR Code\"\n\"info\" = \"More Information\"\n\"edit\" = \"Edit\"\n\"delete\" = \"Delete\"\n\"reset\" = \"Reset\"\n\"noData\" = \"No data.\"\n\"copySuccess\" = \"Copied Successful\"\n\"sure\" = \"Sure\"\n\"encryption\" = \"Encryption\"\n\"useIPv4ForHost\" = \"Use IPv4 for host\"\n\"transmission\" = \"Transmission\"\n\"host\" = \"Host\"\n\"path\" = \"Path\"\n\"camouflage\" = \"Obfuscation\"\n\"status\" = \"Status\"\n\"enabled\" = \"Enabled\"\n\"disabled\" = \"Disabled\"\n\"depleted\" = \"Ended\"\n\"depletingSoon\" = \"Depleting\"\n\"offline\" = \"Offline\"\n\"online\" = \"Online\"\n\"domainName\" = \"Domain Name\"\n\"monitor\" = \"Listen IP\"\n\"certificate\" = \"Digital Certificate\"\n\"fail\" = \"Failed\"\n\"comment\" = \"Comment\"\n\"success\" = \"Successfully\"\n\"lastOnline\" = \"Last Online\"\n\"getVersion\" = \"Get Version\"\n\"install\" = \"Install\"\n\"clients\" = \"Clients\"\n\"usage\" = \"Usage\"\n\"twoFactorCode\" = \"Code\"\n\"remained\" = \"Remained\"\n\"security\" = \"Security\"\n\"secAlertTitle\" = \"Security Alert\"\n\"secAlertSsl\" = \"This connection is not secure. Please avoid entering sensitive information until TLS is activated for data protection.\"\n\"secAlertConf\" = \"Certain settings are vulnerable to attacks. It is recommended to reinforce security protocols to prevent potential breaches.\"\n\"secAlertSSL\" = \"Panel lacks secure connection. Please install TLS certificate for data protection.\"\n\"secAlertPanelPort\" = \"Panel default port is vulnerable. Please configure a random or specific port.\"\n\"secAlertPanelURI\" = \"Panel default URI path is insecure. Please configure a complex URI path.\"\n\"secAlertSubURI\" = \"Subscription default URI path is insecure. Please configure a complex URI path.\"\n\"secAlertSubJsonURI\" = \"Subscription JSON default URI path is insecure. Please configure a complex URI path.\"\n\"emptyDnsDesc\" = \"No added DNS servers.\"\n\"emptyFakeDnsDesc\" = \"No added Fake DNS servers.\"\n\"emptyBalancersDesc\" = \"No added balancers.\"\n\"emptyReverseDesc\" = \"No added reverse proxies.\"\n\"somethingWentWrong\" = \"Something went wrong\"\n\n[subscription]\n\"title\" = \"Subscription info\"\n\"subId\" = \"Subscription ID\"\n\"status\" = \"Status\"\n\"downloaded\" = \"Downloaded\"\n\"uploaded\" = \"Uploaded\"\n\"expiry\" = \"Expiry\"\n\"totalQuota\" = \"Total quota\"\n\"individualLinks\" = \"Individual links\"\n\"active\" = \"Active\"\n\"inactive\" = \"Inactive\"\n\"unlimited\" = \"Unlimited\"\n\"noExpiry\" = \"No expiry\"\n\n[menu]\n\"theme\" = \"Theme\"\n\"dark\" = \"Dark\"\n\"ultraDark\" = \"Ultra Dark\"\n\"dashboard\" = \"Overview\"\n\"inbounds\" = \"Inbounds\"\n\"settings\" = \"Panel Settings\"\n\"xray\" = \"Xray Configs\"\n\"logout\" = \"Log Out\"\n\"link\" = \"Manage\"\n\n[pages.login]\n\"hello\" = \"Hello\"\n\"title\" = \"Welcome\"\n\"loginAgain\" = \"Your session has expired, please log in again\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"The Input data format is invalid.\"\n\"emptyUsername\" = \"Username is required\"\n\"emptyPassword\" = \"Password is required\"\n\"wrongUsernameOrPassword\" = \"Invalid username or password or two-factor code.\"\n\"successLogin\" = \" You have successfully logged into your account.\"\n\n[pages.index]\n\"title\" = \"Overview\"\n\"cpu\" = \"CPU\"\n\"logicalProcessors\" = \"Logical Processors\"\n\"frequency\" = \"Frequency\"\n\"swap\" = \"Swap\"\n\"storage\" = \"Storage\"\n\"memory\" = \"RAM\"\n\"threads\" = \"Threads\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"Stop\"\n\"restartXray\" = \"Restart\"\n\"xraySwitch\" = \"Version\"\n\"xraySwitchClick\" = \"Choose the version you want to switch to.\"\n\"xraySwitchClickDesk\" = \"Choose carefully, as older versions may not be compatible with current configurations.\"\n\"xrayStatusUnknown\" = \"Unknown\"\n\"xrayStatusRunning\" = \"Running\"\n\"xrayStatusStop\" = \"Stop\"\n\"xrayStatusError\" = \"Error\"\n\"xrayErrorPopoverTitle\" = \"An error occurred while running Xray\"\n\"operationHours\" = \"Uptime\"\n\"systemLoad\" = \"System Load\"\n\"systemLoadDesc\" = \"System load average for the past 1, 5, and 15 minutes\"\n\"connectionCount\" = \"Connection Stats\"\n\"ipAddresses\" = \"IP Addresses\"\n\"toggleIpVisibility\" = \"Toggle visibility of the IP\"\n\"overallSpeed\" = \"Overall Speed\"\n\"upload\" = \"Upload\"\n\"download\" = \"Download\"\n\"totalData\" = \"Total Data\"\n\"sent\" = \"Sent\"\n\"received\" = \"Received\"\n\"documentation\" = \"Documentation\"\n\"xraySwitchVersionDialog\" = \"Do you really want to change the Xray version?\"\n\"xraySwitchVersionDialogDesc\" = \"This will change the Xray version to #version#.\"\n\"xraySwitchVersionPopover\" = \"Xray updated successfully\"\n\"geofileUpdateDialog\" = \"Do you really want to update the geofile?\"\n\"geofileUpdateDialogDesc\" = \"This will update the #filename# file.\"\n\"geofilesUpdateDialogDesc\" = \"This will update all geofiles.\"\n\"geofilesUpdateAll\" = \"Update all\"\n\"geofileUpdatePopover\" = \"Geofile updated successfully\"\n\"dontRefresh\" = \"Installation is in progress, please do not refresh this page\"\n\"logs\" = \"Logs\"\n\"config\" = \"Config\"\n\"backup\" = \"Backup\"\n\"backupTitle\" = \"Database Backup & Restore\"\n\"exportDatabase\" = \"Back Up\"\n\"exportDatabaseDesc\" = \"Click to download a .db file containing a backup of your current database to your device.\"\n\"importDatabase\" = \"Restore\"\n\"importDatabaseDesc\" = \"Click to select and upload a .db file from your device to restore your database from a backup.\"\n\"importDatabaseSuccess\" = \"The database has been successfully imported.\"\n\"importDatabaseError\" = \"An error occurred while importing the database.\"\n\"readDatabaseError\" = \"An error occurred while reading the database.\"\n\"getDatabaseError\" = \"An error occurred while retrieving the database.\"\n\"getConfigError\" = \"An error occurred while retrieving the config file.\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"All-time Traffic\"\n\"allTimeTrafficUsage\" = \"All Time Total Usage\"\n\"title\" = \"Inbounds\"\n\"totalDownUp\" = \"Total Sent/Received\"\n\"totalUsage\" = \"Total Usage\"\n\"inboundCount\" = \"Total Inbounds\"\n\"operate\" = \"Menu\"\n\"enable\" = \"Enabled\"\n\"remark\" = \"Remark\"\n\"protocol\" = \"Protocol\"\n\"port\" = \"Port\"\n\"portMap\" = \"Port Mapping\"\n\"traffic\" = \"Traffic\"\n\"details\" = \"Details\"\n\"transportConfig\" = \"Transport\"\n\"expireDate\" = \"Duration\"\n\"createdAt\" = \"Created\"\n\"updatedAt\" = \"Updated\"\n\"resetTraffic\" = \"Reset Traffic\"\n\"addInbound\" = \"Add Inbound\"\n\"generalActions\" = \"General Actions\"\n\"autoRefresh\" = \"Auto-refresh\"\n\"autoRefreshInterval\" = \"Interval\"\n\"modifyInbound\" = \"Modify Inbound\"\n\"deleteInbound\" = \"Delete Inbound\"\n\"deleteInboundContent\" = \"Are you sure you want to delete inbound?\"\n\"deleteClient\" = \"Delete Client\"\n\"deleteClientContent\" = \"Are you sure you want to delete client?\"\n\"resetTrafficContent\" = \"Are you sure you want to reset traffic?\"\n\"copyLink\" = \"Copy URL\"\n\"address\" = \"Address\"\n\"network\" = \"Network\"\n\"destinationPort\" = \"Destination Port\"\n\"targetAddress\" = \"Target Address\"\n\"monitorDesc\" = \"Leave blank to listen on all IPs\"\n\"meansNoLimit\" = \"= Unlimited. (unit: GB)\"\n\"totalFlow\" = \"Total Flow\"\n\"leaveBlankToNeverExpire\" = \"Leave blank to never expire\"\n\"noRecommendKeepDefault\" = \"It is recommended to keep the default\"\n\"certificatePath\" = \"File Path\"\n\"certificateContent\" = \"File Content\"\n\"publicKey\" = \"Public Key\"\n\"privatekey\" = \"Private Key\"\n\"clickOnQRcode\" = \"Click on QR Code to Copy\"\n\"client\" = \"Client\"\n\"export\" = \"Export All URLs\"\n\"clone\" = \"Clone\"\n\"cloneInbound\" = \"Clone\"\n\"cloneInboundContent\" = \"All settings of this inbound, except Port, Listening IP, and Clients, will be applied to the clone.\"\n\"cloneInboundOk\" = \"Clone\"\n\"resetAllTraffic\" = \"Reset All Inbounds Traffic\"\n\"resetAllTrafficTitle\" = \"Reset All Inbounds Traffic\"\n\"resetAllTrafficContent\" = \"Are you sure you want to reset the traffic of all inbounds?\"\n\"resetInboundClientTraffics\" = \"Reset Clients Traffic\"\n\"resetInboundClientTrafficTitle\" = \"Reset Clients Traffic\"\n\"resetInboundClientTrafficContent\" = \"Are you sure you want to reset the traffic of this inbound's clients?\"\n\"resetAllClientTraffics\" = \"Reset All Clients Traffic\"\n\"resetAllClientTrafficTitle\" = \"Reset All Clients Traffic\"\n\"resetAllClientTrafficContent\" = \"Are you sure you want to reset the traffic of all clients?\"\n\"delDepletedClients\" = \"Delete Depleted Clients\"\n\"delDepletedClientsTitle\" = \"Delete Depleted Clients\"\n\"delDepletedClientsContent\" = \"Are you sure you want to delete all the depleted clients?\"\n\"email\" = \"Email\"\n\"emailDesc\" = \"Please provide a unique email address.\"\n\"IPLimit\" = \"IP Limit\"\n\"IPLimitDesc\" = \"Disables inbound if the count exceeds the set value. (0 = disable)\"\n\"IPLimitlog\" = \"IP Log\"\n\"IPLimitlogDesc\" = \"The IPs history log. (to enable inbound after disabling, clear the log)\"\n\"IPLimitlogclear\" = \"Clear The Log\"\n\"setDefaultCert\" = \"Set Cert from Panel\"\n\"telegramDesc\" = \"Please provide Telegram Chat ID. (use '/id' command in the bot) or (@userinfobot)\"\n\"subscriptionDesc\" = \"To find your subscription URL, navigate to the 'Details'. Additionally, you can use the same name for several clients.\"\n\"info\" = \"Info\"\n\"same\" = \"Same\"\n\"inboundData\" = \"Inbound's Data\"\n\"exportInbound\" = \"Export Inbound\"\n\"import\" = \"Import\"\n\"importInbound\" = \"Import an Inbound\"\n\"periodicTrafficResetTitle\" = \"Traffic Reset\"\n\"periodicTrafficResetDesc\" = \"Automatically reset traffic counter at specified intervals\"\n\"lastReset\" = \"Last Reset\"\n\n[pages.client]\n\"add\" = \"Add Client\"\n\"edit\" = \"Edit Client\"\n\"submitAdd\" = \"Add Client\"\n\"submitEdit\" = \"Save Changes\"\n\"clientCount\" = \"Number of Clients\"\n\"bulk\" = \"Add Bulk\"\n\"method\" = \"Method\"\n\"first\" = \"First\"\n\"last\" = \"Last\"\n\"prefix\" = \"Prefix\"\n\"postfix\" = \"Postfix\"\n\"delayedStart\" = \"Start After First Use\"\n\"expireDays\" = \"Duration\"\n\"days\" = \"Day(s)\"\n\"renew\" = \"Auto Renew\"\n\"renewDesc\" = \"Auto-renewal after expiration. (0 = disable)(unit: day)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"Never\"\n\"daily\" = \"Daily\"\n\"weekly\" = \"Weekly\"\n\"monthly\" = \"Monthly\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"Obtain\"\n\"updateSuccess\" = \"The update was successful.\"\n\"logCleanSuccess\" = \"The log has been cleared.\"\n\"inboundsUpdateSuccess\" = \"Inbounds have been successfully updated.\"\n\"inboundUpdateSuccess\" = \"Inbound has been successfully updated.\"\n\"inboundCreateSuccess\" = \"Inbound has been successfully created.\"\n\"inboundDeleteSuccess\" = \"Inbound has been successfully deleted.\"\n\"inboundClientAddSuccess\" = \"Inbound client(s) have been added.\"\n\"inboundClientDeleteSuccess\" = \"Inbound client has been deleted.\"\n\"inboundClientUpdateSuccess\" = \"Inbound client has been updated.\"\n\"delDepletedClientsSuccess\" = \"All depleted clients are deleted.\"\n\"resetAllClientTrafficSuccess\" = \"All traffic from the client has been reset.\"\n\"resetAllTrafficSuccess\" = \"All traffic has been reset.\"\n\"resetInboundClientTrafficSuccess\" = \"Traffic has been reset.\"\n\"trafficGetError\" = \"Error getting traffics.\"\n\"getNewX25519CertError\" = \"Error while obtaining the X25519 certificate.\"\n\"getNewmldsa65Error\" = \"Error while obtaining mldsa65.\"\n\"getNewVlessEncError\" = \"Error while obtaining VlessEnc.\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"Request\"\n\"response\" = \"Response\"\n\"name\" = \"Name\"\n\"value\" = \"Value\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"Version\"\n\"method\" = \"Method\"\n\"path\" = \"Path\"\n\"status\" = \"Status\"\n\"statusDescription\" = \"Status Desc\"\n\"requestHeader\" = \"Request Header\"\n\"responseHeader\" = \"Response Header\"\n\n[pages.settings]\n\"title\" = \"Panel Settings\"\n\"save\" = \"Save\"\n\"infoDesc\" = \"Every change made here needs to be saved. Please restart the panel to apply changes.\"\n\"restartPanel\" = \"Restart Panel\"\n\"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.\"\n\"restartPanelSuccess\" = \"The panel was successfully restarted.\"\n\"actions\" = \"Actions\"\n\"resetDefaultConfig\" = \"Reset to Default\"\n\"panelSettings\" = \"General\"\n\"securitySettings\" = \"Authentication\"\n\"TGBotSettings\" = \"Telegram Bot\"\n\"panelListeningIP\" = \"Listen IP\"\n\"panelListeningIPDesc\" = \"The IP address for the web panel. (leave blank to listen on all IPs)\"\n\"panelListeningDomain\" = \"Listen Domain\"\n\"panelListeningDomainDesc\" = \"The domain name for the web panel. (leave blank to listen on all domains and IPs)\"\n\"panelPort\" = \"Listen Port\"\n\"panelPortDesc\" = \"The port number for the web panel. (must be an unused port)\"\n\"publicKeyPath\" = \"Public Key Path\"\n\"publicKeyPathDesc\" = \"The public key file path for the web panel. (begins with ‘/‘)\"\n\"privateKeyPath\" = \"Private Key Path\"\n\"privateKeyPathDesc\" = \"The private key file path for the web panel. (begins with ‘/‘)\"\n\"panelUrlPath\" = \"URI Path\"\n\"panelUrlPathDesc\" = \"The URI path for the web panel. (begins with ‘/‘ and concludes with ‘/‘)\"\n\"pageSize\" = \"Pagination Size\"\n\"pageSizeDesc\" = \"Define page size for inbounds table. (0 = disable)\"\n\"remarkModel\" = \"Remark Model & Separation Character\"\n\"datepicker\" = \"Calendar Type\"\n\"datepickerPlaceholder\" = \"Select date\"\n\"datepickerDescription\" = \"Scheduled tasks will run based on this calendar.\"\n\"sampleRemark\" = \"Sample Remark\"\n\"oldUsername\" = \"Current Username\"\n\"currentPassword\" = \"Current Password\"\n\"newUsername\" = \"New Username\"\n\"newPassword\" = \"New Password\"\n\"telegramBotEnable\" = \"Enable Telegram Bot\"\n\"telegramBotEnableDesc\" = \"Enables the Telegram bot.\"\n\"telegramToken\" = \"Telegram Token\"\n\"telegramTokenDesc\" = \"The Telegram bot token obtained from '@BotFather'.\"\n\"telegramProxy\" = \"SOCKS Proxy\"\n\"telegramProxyDesc\" = \"Enables SOCKS5 proxy for connecting to Telegram. (adjust settings as per guide)\"\n\"telegramAPIServer\" = \"Telegram API Server\"\n\"telegramAPIServerDesc\" = \"The Telegram API server to use. Leave blank to use the default server.\"\n\"telegramChatId\" = \"Admin Chat ID\"\n\"telegramChatIdDesc\" = \"The Telegram Admin Chat ID(s). (comma-separated)(get it here @userinfobot) or (use '/id' command in the bot)\"\n\"telegramNotifyTime\" = \"Notification Time\"\n\"telegramNotifyTimeDesc\" = \"The Telegram bot notification time set for periodic reports. (use the crontab time format)\"\n\"tgNotifyBackup\" = \"Database Backup\"\n\"tgNotifyBackupDesc\" = \"Send a database backup file with a report.\"\n\"tgNotifyLogin\" = \"Login Notification\"\n\"tgNotifyLoginDesc\" = \"Get notified about the username, IP address, and time whenever someone attempts to log into your web panel.\"\n\"sessionMaxAge\" = \"Session Duration\"\n\"sessionMaxAgeDesc\" = \"The duration for which you can stay logged in. (unit: minute)\"\n\"expireTimeDiff\" = \"Expiration Date Notification\"\n\"expireTimeDiffDesc\" = \"Get notified about expiration date when reaching this threshold. (unit: day)\"\n\"trafficDiff\" = \"Traffic Cap Notification\"\n\"trafficDiffDesc\" = \"Get notified about traffic cap when reaching this threshold. (unit: GB)\"\n\"tgNotifyCpu\" = \"CPU Load Notification\"\n\"tgNotifyCpuDesc\" = \"Get notified if CPU load exceeds this threshold. (unit: %)\"\n\"timeZone\" = \"Time Zone\"\n\"timeZoneDesc\" = \"Scheduled tasks will run based on this time zone.\"\n\"subSettings\" = \"Subscription\"\n\"subEnable\" = \"Subscription Service\"\n\"subEnableDesc\" = \"Enable/Disable the subscription service.\"\n\"subJsonEnable\" = \"Enable/Disable the JSON subscription endpoint independently.\"\n\"subTitle\" = \"Subscription Title\"\n\"subTitleDesc\" = \"Title shown in VPN client\"\n\"subSupportUrl\" = \"Support URL\"\n\"subSupportUrlDesc\" = \"Technical support link shown in the VPN client\"\n\"subProfileUrl\" = \"Profile URL\"\n\"subProfileUrlDesc\" = \"A link to your website displayed in the VPN client\"\n\"subAnnounce\" = \"Announce\"\n\"subAnnounceDesc\" = \"The text of the announce displayed in the VPN client\"\n\"subEnableRouting\" = \"Enable routing\"\n\"subEnableRoutingDesc\" = \"Global setting to enable routing in the VPN client. (Only for Happ)\"\n\"subRoutingRules\" = \"Routing rules\"\n\"subRoutingRulesDesc\" = \"Global routing rules for the VPN client. (Only for Happ)\"\n\"subListen\" = \"Listen IP\"\n\"subListenDesc\" = \"The IP address for the subscription service. (leave blank to listen on all IPs)\"\n\"subPort\" = \"Listen Port\"\n\"subPortDesc\" = \"The port number for the subscription service. (must be an unused port)\"\n\"subCertPath\" = \"Public Key Path\"\n\"subCertPathDesc\" = \"The public key file path for the subscription service. (begins with ‘/‘)\"\n\"subKeyPath\" = \"Private Key Path\"\n\"subKeyPathDesc\" = \"The private key file path for the subscription service. (begins with ‘/‘)\"\n\"subPath\" = \"URI Path\"\n\"subPathDesc\" = \"The URI path for the subscription service. (begins with ‘/‘ and concludes with ‘/‘)\"\n\"subDomain\" = \"Listen Domain\"\n\"subDomainDesc\" = \"The domain name for the subscription service. (leave blank to listen on all domains and IPs)\"\n\"subUpdates\" = \"Update Intervals\"\n\"subUpdatesDesc\" = \"The update intervals of the subscription URL in the client apps. (unit: hour)\"\n\"subEncrypt\" = \"Encode\"\n\"subEncryptDesc\" = \"The returned content of subscription service will be Base64 encoded.\"\n\"subShowInfo\" = \"Show Usage Info\"\n\"subShowInfoDesc\" = \"The remaining traffic and date will be displayed in the client apps.\"\n\"subURI\" = \"Reverse Proxy URI\"\n\"subURIDesc\" = \"The URI path of the subscription URL for use behind proxies.\"\n\"externalTrafficInformEnable\" = \"External Traffic Inform\"\n\"externalTrafficInformEnableDesc\" = \"Inform external API on every traffic update.\"\n\"externalTrafficInformURI\" = \"External Traffic Inform URI\"\n\"externalTrafficInformURIDesc\" = \"Traffic updates are sent to this URI.\"\n\"fragment\" = \"Fragmentation\"\n\"fragmentDesc\" = \"Enable fragmentation for TLS hello packet.\"\n\"fragmentSett\" = \"Fragmentation Settings\"\n\"noisesDesc\" = \"Enable Noises.\"\n\"noisesSett\" = \"Noises Settings\"\n\"mux\" = \"Mux\"\n\"muxDesc\" = \"Transmit multiple independent data streams within an established data stream.\"\n\"muxSett\" = \"Mux Settings\"\n\"direct\" = \"Direct Connection\"\n\"directDesc\" = \"Directly establishes connections with domains or IP ranges of a specific country.\"\n\"notifications\" = \"Notifications\"\n\"certs\" = \"Certificaties\"\n\"externalTraffic\" = \"External Traffic\"\n\"dateAndTime\" = \"Date and Time\"\n\"proxyAndServer\" = \"Proxy and Server\"\n\"intervals\" = \"Intervals\"\n\"information\" = \"Information\"\n\"language\" = \"Language\"\n\"telegramBotLanguage\" = \"Telegram Bot Language\"\n\n[pages.xray]\n\"title\" = \"Xray Configs\"\n\"save\" = \"Save\"\n\"restart\" = \"Restart Xray\"\n\"restartSuccess\" = \"Xray has been successfully relaunched.\"\n\"stopSuccess\" = \"Xray has been successfully stopped.\"\n\"restartError\" = \"There was an error when rebooting the Xray.\"\n\"stopError\" = \"There was an error when stopping the Xray.\"\n\"basicTemplate\" = \"Basics\"\n\"advancedTemplate\" = \"Advanced\"\n\"generalConfigs\" = \"General\"\n\"generalConfigsDesc\" = \"These options will determine general adjustments.\"\n\"logConfigs\" = \"Log\"\n\"logConfigsDesc\" = \"Logs may affect your server's efficiency. It is recommended to enable it wisely only in case of your needs\"\n\"blockConfigsDesc\" = \"These options will block traffic based on specific requested protocols and websites.\"\n\"basicRouting\" = \"Basic Routing\"\n\"blockConnectionsConfigsDesc\" = \"These options will block traffic based on the specific requested country.\"\n\"directConnectionsConfigsDesc\" = \"A direct connection ensures that specific traffic is not routed through another server.\"\n\"blockips\" = \"Block IPs\"\n\"blockdomains\" = \"Block Domains\"\n\"directips\" = \"Direct IPs\"\n\"directdomains\" = \"Direct Domains\"\n\"ipv4Routing\" = \"IPv4 Routing\"\n\"ipv4RoutingDesc\" = \"These options will route traffic based on a specific destination via IPv4.\"\n\"warpRouting\" = \"WARP Routing\"\n\"warpRoutingDesc\" = \"These options will route traffic based on a specific destination via WARP.\"\n\"Template\" = \"Advanced Xray Configuration Template\"\n\"TemplateDesc\" = \"The final Xray config file will be generated based on this template.\"\n\"FreedomStrategy\" = \"Freedom Protocol Strategy\"\n\"FreedomStrategyDesc\" = \"Set the output strategy for the network in the Freedom Protocol.\"\n\"RoutingStrategy\" = \"Overall Routing Strategy\"\n\"RoutingStrategyDesc\" = \"Set the overall traffic routing strategy for resolving all requests.\"\n\"outboundTestUrl\" = \"Outbound Test URL\"\n\"outboundTestUrlDesc\" = \"URL used when testing outbound connectivity.\"\n\"Torrent\" = \"Block BitTorrent Protocol\"\n\"Inbounds\" = \"Inbounds\"\n\"InboundsDesc\" = \"Accepting the specific clients.\"\n\"Outbounds\" = \"Outbounds\"\n\"Balancers\" = \"Balancers\"\n\"OutboundsDesc\" = \"Set the outgoing traffic pathway.\"\n\"Routings\" = \"Routing Rules\"\n\"RoutingsDesc\" = \"The priority of each rule is important!\"\n\"completeTemplate\" = \"All\"\n\"logLevel\" = \"Log Level\"\n\"logLevelDesc\" = \"The log level for error logs, indicating the information that needs to be recorded.\"\n\"accessLog\" = \"Access Log\"\n\"accessLogDesc\" = \"The file path for the access log. The special value 'none' disabled access logs\"\n\"errorLog\" = \"Error Log\"\n\"errorLogDesc\" = \"The file path for the error log. The special value 'none' disabled error logs\"\n\"dnsLog\" = \"DNS Log\"\n\"dnsLogDesc\" = \"Whether to enable DNS query logs\"\n\"maskAddress\" = \"Mask Address\"\n\"maskAddressDesc\" = \"IP address mask, when enabled, will automatically replace the IP address that appears in the log.\"\n\"statistics\" = \"Statistics\"\n\"statsInboundUplink\" = \"Inbound Upload Statistics\"\n\"statsInboundUplinkDesc\" = \"Enables the statistics collection for upstream traffic of all inbound proxies.\"\n\"statsInboundDownlink\" = \"Inbound Download Statistics\"\n\"statsInboundDownlinkDesc\" = \"Enables the statistics collection for downstream traffic of all inbound proxies.\"\n\"statsOutboundUplink\" = \"Outbound Upload Statistics\"\n\"statsOutboundUplinkDesc\" = \"Enables the statistics collection for upstream traffic of all outbound proxies.\"\n\"statsOutboundDownlink\" = \"Outbound Download Statistics\"\n\"statsOutboundDownlinkDesc\" = \"Enables the statistics collection for downstream traffic of all outbound proxies.\"\n\n[pages.xray.rules]\n\"first\" = \"First\"\n\"last\" = \"Last\"\n\"up\" = \"Up\"\n\"down\" = \"Down\"\n\"source\" = \"Source\"\n\"dest\" = \"Destination\"\n\"inbound\" = \"Inbound\"\n\"outbound\" = \"Outbound\"\n\"balancer\" = \"Balancer\"\n\"info\" = \"Info\"\n\"add\" = \"Add Rule\"\n\"edit\" = \"Edit Rule\"\n\"useComma\" = \"Comma-separated items\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"Add Outbound\"\n\"addReverse\" = \"Add Reverse\"\n\"editOutbound\" = \"Edit Outbound\"\n\"editReverse\" = \"Edit Reverse\"\n\"tag\" = \"Tag\"\n\"tagDesc\" = \"Unique Tag\"\n\"address\" = \"Address\"\n\"reverse\" = \"Reverse\"\n\"domain\" = \"Domain\"\n\"type\" = \"Type\"\n\"bridge\" = \"Bridge\"\n\"portal\" = \"Portal\"\n\"link\" = \"Link\"\n\"intercon\" = \"Interconnection\"\n\"settings\" = \"Settings\"\n\"accountInfo\" = \"Account Information\"\n\"outboundStatus\" = \"Outbound Status\"\n\"sendThrough\" = \"Send Through\"\n\"test\" = \"Test\"\n\"testResult\" = \"Test Result\"\n\"testing\" = \"Testing connection...\"\n\"testSuccess\" = \"Test successful\"\n\"testFailed\" = \"Test failed\"\n\"testError\" = \"Failed to test outbound\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"Add Balancer\"\n\"editBalancer\" = \"Edit Balancer\"\n\"balancerStrategy\" = \"Strategy\"\n\"balancerSelectors\" = \"Selectors\"\n\"tag\" = \"Tag\"\n\"tagDesc\" = \"Unique Tag\"\n\"balancerDesc\" = \"It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work.\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"Secret Key\"\n\"publicKey\" = \"Public Key\"\n\"allowedIPs\" = \"Allowed IPs\"\n\"endpoint\" = \"Endpoint\"\n\"psk\" = \"PreShared Key\"\n\"domainStrategy\" = \"Domain Strategy\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"The name of the TUN interface. Default is 'xray0'\"\n\"mtuDesc\" = \"Maximum Transmission Unit. The maximum size of data packets. Default is 1500\"\n\"userLevel\" = \"User Level\"\n\"userLevelDesc\" = \"All connections made through this inbound will use this user level. Default is 0\"\n\n[pages.xray.dns]\n\"enable\" = \"Enable DNS\"\n\"enableDesc\" = \"Enable built-in DNS server\"\n\"tag\" = \"DNS Inbound Tag\"\n\"tagDesc\" = \"This tag will be available as an Inbound tag in routing rules.\"\n\"clientIp\" = \"Client IP\"\n\"clientIpDesc\" = \"Used to notify the server of the specified IP location during DNS queries\"\n\"disableCache\" = \"Disable cache\"\n\"disableCacheDesc\" = \"Disables DNS caching\"\n\"disableFallback\" = \"Disable Fallback\"\n\"disableFallbackDesc\" = \"Disables fallback DNS queries\"\n\"disableFallbackIfMatch\" = \"Disable Fallback If Match\"\n\"disableFallbackIfMatchDesc\" = \"Disables fallback DNS queries when the matching domain list of the DNS server is hit\"\n\"enableParallelQuery\" = \"Enable Parallel Query\"\n\"enableParallelQueryDesc\" = \"Enable parallel DNS queries to multiple servers for faster resolution\"\n\"strategy\" = \"Query Strategy\"\n\"strategyDesc\" = \"Overall strategy to resolve domain names\"\n\"add\" = \"Add Server\"\n\"edit\" = \"Edit Server\"\n\"domains\" = \"Domains\"\n\"expectIPs\" = \"Expect IPs\"\n\"unexpectIPs\" = \"Unexpect IPs\"\n\"useSystemHosts\" = \"Use System Hosts\"\n\"useSystemHostsDesc\" = \"Use the hosts file from an installed system\"\n\"usePreset\" = \"Use Preset\"\n\"dnsPresetTitle\" = \"DNS Presets\"\n\"dnsPresetFamily\" = \"Family\"\n\n[pages.xray.fakedns]\n\"add\" = \"Add Fake DNS\"\n\"edit\" = \"Edit Fake DNS\"\n\"ipPool\" = \"IP Pool Subnet\"\n\"poolSize\" = \"Pool Size\"\n\n[pages.settings.security]\n\"admin\" = \"Admin credentials\"\n\"twoFactor\" = \"Two-factor authentication\"\n\"twoFactorEnable\" = \"Enable 2FA\"\n\"twoFactorEnableDesc\" = \"Adds an additional layer of authentication to provide more security.\"\n\"twoFactorModalSetTitle\" = \"Enable two-factor authentication\"\n\"twoFactorModalDeleteTitle\" = \"Disable two-factor authentication\"\n\"twoFactorModalSteps\" = \"To set up two-factor authentication, perform a few steps:\"\n\"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\"\n\"twoFactorModalSecondStep\" = \"2. Enter the code from the app\"\n\"twoFactorModalRemoveStep\" = \"Enter the code from the application to remove two-factor authentication.\"\n\"twoFactorModalChangeCredentialsTitle\" = \"Change credentials\"\n\"twoFactorModalChangeCredentialsStep\" = \"Enter the code from the application to change administrator credentials.\"\n\"twoFactorModalSetSuccess\" = \"Two-factor authentication has been successfully established\"\n\"twoFactorModalDeleteSuccess\" = \"Two-factor authentication has been successfully deleted\"\n\"twoFactorModalError\" = \"Wrong code\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"The parameters have been changed.\"\n\"getSettings\" = \"An error occurred while retrieving parameters.\"\n\"modifyUserError\" = \"An error occurred while changing administrator credentials.\"\n\"modifyUser\" = \"You have successfully changed the credentials of the administrator.\"\n\"originalUserPassIncorrect\" = \"The сurrent username or password is invalid\"\n\"userPassMustBeNotEmpty\" = \"The new username and password is empty\"\n\"getOutboundTrafficError\" = \"Error getting traffics\"\n\"resetOutboundTrafficError\" = \"Error in reset outbound traffics\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ Custom keyboard closed!\"\n\"noResult\" = \"❗ No result!\"\n\"noQuery\" = \"❌ Query not found! Please use the command again!\"\n\"wentWrong\" = \"❌ Something went wrong!\"\n\"noIpRecord\" = \"❗ No IP Record!\"\n\"noInbounds\" = \"❗ No inbound found!\"\n\"unlimited\" = \"♾ Unlimited(Reset)\"\n\"add\" = \"Add\"\n\"month\" = \"Month\"\n\"months\" = \"Months\"\n\"day\" = \"Day\"\n\"days\" = \"Days\"\n\"hours\" = \"Hours\"\n\"minutes\" = \"Minutes\"\n\"unknown\" = \"Unknown\"\n\"inbounds\" = \"Inbounds\"\n\"clients\" = \"Clients\"\n\"offline\" = \"🔴 Offline\"\n\"online\" = \"🟢 Online\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ Unknown command.\"\n\"pleaseChoose\" = \"👇 Please choose:\\r\\n\"\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\"\n\"start\" = \"👋 Hello <i>{{ .Firstname }}</i>.\\r\\n\"\n\"welcome\" = \"🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\\r\\n\"\n\"status\" = \"✅ Bot is OK!\"\n\"usage\" = \"❗ Please provide a text to search!\"\n\"getID\" = \"🆔 Your ID: <code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"To restart Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\nTo search for a client email:\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nTo search for inbounds (with client stats):\\r\\n<code>/inbound [Remark]</code>\\r\\n\\r\\nTelegram Chat ID:\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"To search for statistics, use the following command:\\r\\n\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nTelegram Chat ID:\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ Operation successful!\"\n\"restartFailed\" = \"❗ Error in operation.\\r\\n\\r\\n<code>Error: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core is not running.\"\n\"startDesc\" = \"Show the main menu\"\n\"helpDesc\" = \"Bot help\"\n\"statusDesc\" = \"Check bot status\"\n\"idDesc\" = \"Show your Telegram ID\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%\"\n\"selectUserFailed\" = \"❌ Error in user selection!\"\n\"userSaved\" = \"✅ Telegram User saved.\"\n\"loginSuccess\" = \"✅ Logged in to the panel successfully.\\r\\n\"\n\"loginFailed\" = \"❗️Login attempt to the panel failed.\\r\\n\"\n\"2faFailed\" = \"2FA Failed\"\n\"report\" = \"🕰 Scheduled Reports: {{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ Date&Time: {{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 Host: {{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 3X-UI Version: {{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Xray Version: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IPs:\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ Uptime: {{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 System Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 RAM: {{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP: {{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP: {{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Status: {{ .State }}\\r\\n\"\n\"username\" = \"👤 Username: {{ .Username }}\\r\\n\"\n\"password\" = \"👤 Password: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ Time: {{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 Inbound: {{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 Port: {{ .Port }}\\r\\n\"\n\"expire\" = \"📅 Expire Date: {{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 Expire In: {{ .Time }}\\r\\n\"\n\"active\" = \"💡 Active: {{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 Enabled: {{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 Connection status: {{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 Last online: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 Email: {{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 Upload: ↑{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 Download: ↓{{ .Download }}\\r\\n\"\n\"total\" = \"📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 Telegram User: {{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 Exhausted {{ .Type }}:\\r\\n\"\n\"exhaustedCount\" = \"🚨 Exhausted {{ .Type }} count:\\r\\n\"\n\"onlinesCount\" = \"🌐 Online Clients: {{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 Disabled: {{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 Deplete Soon: {{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 Backup Time: {{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 Refreshed On: {{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ Yes\"\n\"no\" = \"❌ No\"\n\"received_id\" = \"🔑📥 ID updated.\"\n\"received_password\" = \"🔑📥 Password updated.\"\n\"received_email\" = \"📧📥 Email updated.\"\n\"received_comment\" = \"💬📥 Comment updated.\"\n\"id_prompt\" = \"🔑 Default ID: {{ .ClientId }}\\n\\nEnter your id.\"\n\"pass_prompt\" = \"🔑 Default Password: {{ .ClientPassword }}\\n\\nEnter your password.\"\n\"email_prompt\" = \"📧 Default Email: {{ .ClientEmail }}\\n\\nEnter your email.\"\n\"comment_prompt\" = \"💬 Default Comment: {{ .ClientComment }}\\n\\nEnter your Comment.\"\n\"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!\"\n\"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!\"\n\"cancel\" = \"❌ Process Canceled! \\n\\nYou can /start again anytime. 🔄\"\n\"error_add_client\" = \"⚠️ Error:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"Okay, I'll stick with the default value. 😊\"\n\"incorrect_input\" = \"Your input is not valid.\\nThe phrases should be continuous without spaces.\\nCorrect example: aaaaaa\\nIncorrect example: aaa aaa 🚫\"\n\"AreYouSure\" = \"Are you sure? 🤔\"\n\"SuccessResetTraffic\" = \"📧 Email: {{ .ClientEmail }}\\n🏁 Result: ✅ Success\"\n\"FailedResetTraffic\" = \"📧 Email: {{ .ClientEmail }}\\n🏁 Result: ❌ Failed \\n\\n🛠️ Error: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 Traffic reset process finished for all clients.\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ Close Keyboard\"\n\"cancel\" = \"❌ Cancel\"\n\"cancelReset\" = \"❌ Cancel Reset\"\n\"cancelIpLimit\" = \"❌ Cancel IP Limit\"\n\"confirmResetTraffic\" = \"✅ Confirm Reset Traffic?\"\n\"confirmClearIps\" = \"✅ Confirm Clear IPs?\"\n\"confirmRemoveTGUser\" = \"✅ Confirm Remove Telegram User?\"\n\"confirmToggle\" = \"✅ Confirm Enable/Disable User?\"\n\"dbBackup\" = \"Get DB Backup\"\n\"serverUsage\" = \"Server Usage\"\n\"getInbounds\" = \"Get Inbounds\"\n\"depleteSoon\" = \"Deplete Soon\"\n\"clientUsage\" = \"Get Usage\"\n\"onlines\" = \"Online Clients\"\n\"commands\" = \"Commands\"\n\"refresh\" = \"🔄 Refresh\"\n\"clearIPs\" = \"❌ Clear IPs\"\n\"removeTGUser\" = \"❌ Remove Telegram User\"\n\"selectTGUser\" = \"👤 Select Telegram User\"\n\"selectOneTGUser\" = \"👤 Select a Telegram User:\"\n\"resetTraffic\" = \"📈 Reset Traffic\"\n\"resetExpire\" = \"📅 Change Expiry Date\"\n\"ipLog\" = \"🔢 IP Log\"\n\"ipLimit\" = \"🔢 IP Limit\"\n\"setTGUser\" = \"👤 Set Telegram User\"\n\"toggle\" = \"🔘 Enable / Disable\"\n\"custom\" = \"🔢 Custom\"\n\"confirmNumber\" = \"✅ Confirm: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ Confirm adding: {{ .Num }}\"\n\"limitTraffic\" = \"🚧 Traffic Limit\"\n\"getBanLogs\" = \"Get Ban Logs\"\n\"allClients\" = \"All Clients\"\n\"addClient\" = \"Add Client\"\n\"submitDisable\" = \"Submit As Disable ☑️\"\n\"submitEnable\" = \"Submit As Enable ✅\"\n\"use_default\" = \"🏷️ Use default\"\n\"change_id\" = \"⚙️🔑 ID\"\n\"change_password\" = \"⚙️🔑 Password\"\n\"change_email\" = \"⚙️📧 Email\"\n\"change_comment\" = \"⚙️💬 Comment\"\n\"ResetAllTraffics\" = \"Reset All Traffics\"\n\"SortedTrafficUsageReport\" = \"Sorted Traffic Usage Report\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ Operation successful!\"\n\"errorOperation\" = \"❗ Error in operation.\"\n\"getInboundsFailed\" = \"❌ Failed to get inbounds.\"\n\"getClientsFailed\" = \"❌ Failed to get clients.\"\n\"canceled\" = \"❌ {{ .Email }}: Operation canceled.\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}: Client refreshed successfully.\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}: IPs refreshed successfully.\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}: Client's Telegram User refreshed successfully.\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}: Traffic reset successfully.\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: Traffic limit saved successfully.\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}: Expire days reset successfully.\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}: IP limit {{ .Count }} saved successfully.\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}: IPs cleared successfully.\"\n\"getIpLog\" = \"✅ {{ .Email }}: Get IP Log.\"\n\"getUserInfo\" = \"✅ {{ .Email }}: Get Telegram User Info.\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}: Telegram User removed successfully.\"\n\"enableSuccess\" = \"✅ {{ .Email }}: Enabled successfully.\"\n\"disableSuccess\" = \"✅ {{ .Email }}: Disabled successfully.\"\n\"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: <code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"Choose a Client for Inbound {{ .Inbound }}\"\n\"chooseInbound\" = \"Choose an Inbound\"\n"
  },
  {
    "path": "web/translation/translate.es_ES.toml",
    "content": "\"username\" = \"Nombre de Usuario\"\r\n\"password\" = \"Contraseña\"\r\n\"login\" = \"Acceder\"\r\n\"confirm\" = \"Confirmar\"\r\n\"cancel\" = \"Cancelar\"\r\n\"close\" = \"Cerrar\"\r\n\"create\" = \"Crear\"\r\n\"update\" = \"Actualizar\"\r\n\"copy\" = \"Copiar\"\r\n\"copied\" = \"Copiado\"\r\n\"download\" = \"Descargar\"\r\n\"remark\" = \"Notas\"\r\n\"enable\" = \"Habilitar\"\r\n\"protocol\" = \"Protocolo\"\r\n\"search\" = \"Buscar\"\r\n\"filter\" = \"Filtrar\"\r\n\"loading\" = \"Cargando...\"\r\n\"second\" = \"Segundo\"\r\n\"minute\" = \"Minuto\"\r\n\"hour\" = \"Hora\"\r\n\"day\" = \"Día\"\r\n\"check\" = \"Verificar\"\r\n\"indefinite\" = \"Indefinido\"\r\n\"unlimited\" = \"Ilimitado\"\r\n\"none\" = \"None\"\r\n\"qrCode\" = \"Código QR\"\r\n\"info\" = \"Más Información\"\r\n\"edit\" = \"Editar\"\r\n\"delete\" = \"Eliminar\"\r\n\"reset\" = \"Restablecer\"\r\n\"noData\" = \"Sin datos\"\r\n\"copySuccess\" = \"Copiado exitosamente\"\r\n\"sure\" = \"Seguro\"\r\n\"encryption\" = \"Encriptación\"\r\n\"useIPv4ForHost\" = \"Usar IPv4 para el host\"\r\n\"transmission\" = \"Transmisión\"\r\n\"host\" = \"Host\"\r\n\"path\" = \"Path\"\r\n\"camouflage\" = \"Camuflaje\"\r\n\"status\" = \"Estado\"\r\n\"enabled\" = \"Habilitado\"\r\n\"disabled\" = \"Deshabilitado\"\r\n\"depleted\" = \"Agotado\"\r\n\"depletingSoon\" = \"Agotándose\"\r\n\"offline\" = \"fuera de línea\"\r\n\"online\" = \"en línea\"\r\n\"domainName\" = \"Nombre de dominio\"\r\n\"monitor\" = \"Listening IP\"\r\n\"certificate\" = \"Certificado Digital\"\r\n\"fail\" = \"Falló\"\r\n\"comment\" = \"Comentario\"\r\n\"success\" = \"Éxito\"\r\n\"lastOnline\" = \"Última conexión\"\r\n\"getVersion\" = \"Obtener versión\"\r\n\"install\" = \"Instalar\"\r\n\"clients\" = \"Clientes\"\r\n\"usage\" = \"Uso\"\r\n\"twoFactorCode\" = \"Código\"\r\n\"remained\" = \"Restante\"\r\n\"security\" = \"Seguridad\"\r\n\"secAlertTitle\" = \"Alerta de Seguridad\"\r\n\"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.\"\r\n\"secAlertConf\" = \"Ciertas configuraciones son vulnerables a ataques. Se recomienda reforzar los protocolos de seguridad para prevenir posibles violaciones.\"\r\n\"secAlertSSL\" = \"El panel carece de una conexión segura. Por favor, instale un certificado TLS para la protección de datos.\"\r\n\"secAlertPanelPort\" = \"El puerto predeterminado del panel es vulnerable. Por favor, configure un puerto aleatorio o específico.\"\r\n\"secAlertPanelURI\" = \"La ruta URI predeterminada del panel no es segura. Por favor, configure una ruta URI compleja.\"\r\n\"secAlertSubURI\" = \"La ruta URI predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja.\"\r\n\"secAlertSubJsonURI\" = \"La ruta URI JSON predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja.\"\r\n\"emptyDnsDesc\" = \"No hay servidores DNS añadidos.\"\r\n\"emptyFakeDnsDesc\" = \"No hay servidores Fake DNS añadidos.\"\r\n\"emptyBalancersDesc\" = \"No hay balanceadores añadidos.\"\r\n\"emptyReverseDesc\" = \"No hay proxies inversos añadidos.\"\r\n\"somethingWentWrong\" = \"Algo salió mal\"\r\n\r\n[subscription]\r\n\"title\" = \"Información de suscripción\"\r\n\"subId\" = \"ID de suscripción\"\r\n\"status\" = \"Estado\"\r\n\"downloaded\" = \"Descargado\"\r\n\"uploaded\" = \"Subido\"\r\n\"expiry\" = \"Caducidad\"\r\n\"totalQuota\" = \"Cuota total\"\r\n\"individualLinks\" = \"Enlaces individuales\"\r\n\"active\" = \"Activo\"\r\n\"inactive\" = \"Inactivo\"\r\n\"unlimited\" = \"Ilimitado\"\r\n\"noExpiry\" = \"Sin caducidad\"\r\n\r\n[menu]\r\n\"theme\" = \"Tema\"\r\n\"dark\" = \"Oscuro\"\r\n\"ultraDark\" = \"Ultra Oscuro\"\r\n\"dashboard\" = \"Estado del Sistema\"\r\n\"inbounds\" = \"Entradas\"\r\n\"settings\" = \"Configuraciones\"\r\n\"xray\" = \"Ajustes Xray\"\r\n\"logout\" = \"Cerrar Sesión\"\r\n\"link\" = \"Gestionar\"\r\n\r\n[pages.login]\r\n\"hello\" = \"Hola\"\r\n\"title\" = \"Bienvenido\"\r\n\"loginAgain\" = \"El límite de tiempo de inicio de sesión ha expirado. Por favor, inicia sesión nuevamente.\"\r\n\r\n[pages.login.toasts]\r\n\"invalidFormData\" = \"El formato de los datos de entrada es inválido.\"\r\n\"emptyUsername\" = \"Por favor ingresa el nombre de usuario.\"\r\n\"emptyPassword\" = \"Por favor ingresa la contraseña.\"\r\n\"wrongUsernameOrPassword\" = \"Nombre de usuario, contraseña o código de dos factores incorrecto.\"\r\n\"successLogin\" = \"Has iniciado sesión en tu cuenta correctamente.\"\r\n\r\n[pages.index]\r\n\"title\" = \"Estado del Sistema\"\r\n\"cpu\" = \"CPU\"\r\n\"logicalProcessors\" = \"Procesadores lógicos\"\r\n\"frequency\" = \"Frecuencia\"\r\n\"swap\" = \"Memoria Virtual\"\r\n\"storage\" = \"Almacenamiento\"\r\n\"memory\" = \"RAM\"\r\n\"threads\" = \"Hilos\"\r\n\"xrayStatus\" = \"Xray\"\r\n\"stopXray\" = \"Detener\"\r\n\"restartXray\" = \"Reiniciar\"\r\n\"xraySwitch\" = \"Versión\"\r\n\"xraySwitchClick\" = \"Elige la versión a la que deseas cambiar.\"\r\n\"xraySwitchClickDesk\" = \"Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales.\"\r\n\"xrayStatusUnknown\" = \"Desconocido\"\r\n\"xrayStatusRunning\" = \"En ejecución\"\r\n\"xrayStatusStop\" = \"Detenido\"\r\n\"xrayStatusError\" = \"Error\"\r\n\"xrayErrorPopoverTitle\" = \"Se produjo un error al ejecutar Xray\"\r\n\"operationHours\" = \"Tiempo de Funcionamiento\"\r\n\"systemLoad\" = \"Carga del Sistema\"\r\n\"systemLoadDesc\" = \"promedio de carga del sistema en los últimos 1, 5 y 15 minutos\"\r\n\"connectionCount\" = \"Número de Conexiones\"\r\n\"ipAddresses\" = \"Direcciones IP\"\r\n\"toggleIpVisibility\" = \"Alternar visibilidad de la IP\"\r\n\"overallSpeed\" = \"Velocidad general\"\r\n\"upload\" = \"Subida\"\r\n\"download\" = \"Descarga\"\r\n\"totalData\" = \"Datos totales\"\r\n\"sent\" = \"Enviado\"\r\n\"received\" = \"Recibido\"\r\n\"documentation\" = \"Documentación\"\r\n\"xraySwitchVersionDialog\" = \"¿Realmente deseas cambiar la versión de Xray?\"\r\n\"xraySwitchVersionDialogDesc\" = \"Esto cambiará la versión de Xray a #version#.\"\r\n\"xraySwitchVersionPopover\" = \"Xray se actualizó correctamente\"\r\n\"geofileUpdateDialog\" = \"¿Realmente deseas actualizar el geofichero?\"\r\n\"geofileUpdateDialogDesc\" = \"Esto actualizará el archivo #filename#.\"\r\n\"geofilesUpdateDialogDesc\" = \"Esto actualizará todos los archivos.\"\r\n\"geofilesUpdateAll\" = \"Actualizar todo\"\r\n\"geofileUpdatePopover\" = \"Geofichero actualizado correctamente\"\r\n\"dontRefresh\" = \"La instalación está en progreso, por favor no actualices esta página.\"\r\n\"logs\" = \"Registros\"\r\n\"config\" = \"Configuración\"\r\n\"backup\" = \"Сopia de Seguridad\"\r\n\"backupTitle\" = \"Copia de Seguridad y Restauración de la Base de Datos\"\r\n\"exportDatabase\" = \"Copia de seguridad\"\r\n\"exportDatabaseDesc\" = \"Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo.\"\r\n\"importDatabase\" = \"Restaurar\"\r\n\"importDatabaseDesc\" = \"Haz clic para seleccionar y cargar un archivo .db desde tu dispositivo para restaurar tu base de datos desde una copia de seguridad.\"\r\n\"importDatabaseSuccess\" = \"La base de datos se ha importado correctamente\"\r\n\"importDatabaseError\" = \"Ocurrió un error al importar la base de datos\"\r\n\"readDatabaseError\" = \"Ocurrió un error al leer la base de datos\"\r\n\"getDatabaseError\" = \"Ocurrió un error al obtener la base de datos\"\r\n\"getConfigError\" = \"Ocurrió un error al obtener el archivo de configuración\"\r\n\r\n[pages.inbounds]\r\n\"allTimeTraffic\" = \"Tráfico Total\"\r\n\"allTimeTrafficUsage\" = \"Uso de datos histórico\"\r\n\"title\" = \"Entradas\"\r\n\"totalDownUp\" = \"Subidas/Descargas Totales\"\r\n\"totalUsage\" = \"Uso Total\"\r\n\"inboundCount\" = \"Número de Entradas\"\r\n\"operate\" = \"Menú\"\r\n\"enable\" = \"Habilitar\"\r\n\"remark\" = \"Notas\"\r\n\"protocol\" = \"Protocolo\"\r\n\"port\" = \"Puerto\"\r\n\"portMap\" = \"Puertos de Destino\"\r\n\"traffic\" = \"Tráfico\"\r\n\"details\" = \"Detalles\"\r\n\"transportConfig\" = \"Transporte\"\r\n\"expireDate\" = \"Fecha de Expiración\"\r\n\"createdAt\" = \"Creado\"\r\n\"updatedAt\" = \"Actualizado\"\r\n\"resetTraffic\" = \"Restablecer Tráfico\"\r\n\"addInbound\" = \"Agregar Entrada\"\r\n\"generalActions\" = \"Acciones Generales\"\r\n\"autoRefresh\" = \"Auto-actualizar\"\r\n\"autoRefreshInterval\" = \"Intervalo\"\r\n\"modifyInbound\" = \"Modificar Entrada\"\r\n\"deleteInbound\" = \"Eliminar Entrada\"\r\n\"deleteInboundContent\" = \"¿Confirmar eliminación de entrada?\"\r\n\"deleteClient\" = \"Eliminar cliente\"\r\n\"deleteClientContent\" = \"¿Está seguro de que desea eliminar el cliente?\"\r\n\"resetTrafficContent\" = \"¿Confirmar restablecimiento de tráfico?\"\r\n\"copyLink\" = \"Copiar Enlace\"\r\n\"address\" = \"Dirección\"\r\n\"network\" = \"Red\"\r\n\"destinationPort\" = \"Puerto de Destino\"\r\n\"targetAddress\" = \"Dirección de Destino\"\r\n\"monitorDesc\" = \"Dejar en blanco por defecto\"\r\n\"meansNoLimit\" = \" = illimitata. (unidad: GB)\"\r\n\"totalFlow\" = \"Flujo Total\"\r\n\"leaveBlankToNeverExpire\" = \"Dejar en Blanco para Nunca Expirar\"\r\n\"noRecommendKeepDefault\" = \"No hay requisitos especiales para mantener la configuración predeterminada\"\r\n\"certificatePath\" = \"Ruta Cert\"\r\n\"certificateContent\" = \"Datos Cert\"\r\n\"publicKey\" = \"Clave Pública\"\r\n\"privatekey\" = \"Clave Privada\"\r\n\"clickOnQRcode\" = \"Haz clic en el Código QR para Copiar\"\r\n\"client\" = \"Cliente\"\r\n\"export\" = \"Exportar Enlaces\"\r\n\"clone\" = \"Clonar\"\r\n\"cloneInbound\" = \"Clonar Entradas\"\r\n\"cloneInboundContent\" = \"Se aplicarán todas las configuraciones de esta entrada, excepto el Puerto, la IP de Escucha y los Clientes, al clon.\"\r\n\"cloneInboundOk\" = \"Clonar\"\r\n\"resetAllTraffic\" = \"Restablecer Tráfico de Todas las Entradas\"\r\n\"resetAllTrafficTitle\" = \"Restablecer tráfico de todas las entradas\"\r\n\"resetAllTrafficContent\" = \"¿Estás seguro de que deseas restablecer el tráfico de todas las entradas?\"\r\n\"resetInboundClientTraffics\" = \"Restablecer Tráfico de Clientes\"\r\n\"resetInboundClientTrafficTitle\" = \"Restablecer todo el tráfico de clientes\"\r\n\"resetInboundClientTrafficContent\" = \"¿Estás seguro de que deseas restablecer todo el tráfico para los clientes de esta entrada?\"\r\n\"resetAllClientTraffics\" = \"Restablecer Tráfico de Todos los Clientes\"\r\n\"resetAllClientTrafficTitle\" = \"Restablecer todo el tráfico de clientes\"\r\n\"resetAllClientTrafficContent\" = \"¿Estás seguro de que deseas restablecer todo el tráfico para todos los clientes?\"\r\n\"delDepletedClients\" = \"Eliminar Clientes Agotados\"\r\n\"delDepletedClientsTitle\" = \"Eliminar clientes agotados\"\r\n\"delDepletedClientsContent\" = \"¿Estás seguro de que deseas eliminar todos los clientes agotados?\"\r\n\"email\" = \"Email\"\r\n\"emailDesc\" = \"Por favor proporciona una dirección de correo electrónico única.\"\r\n\"IPLimit\" = \"Límite de IP\"\r\n\"IPLimitDesc\" = \"Desactiva la entrada si la cantidad supera el valor ingresado (ingresa 0 para desactivar el límite de IP).\"\r\n\"IPLimitlog\" = \"Registro de IP\"\r\n\"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).\"\r\n\"IPLimitlogclear\" = \"Limpiar el Registro\"\r\n\"setDefaultCert\" = \"Establecer certificado desde el panel\"\r\n\"telegramDesc\" = \"Por favor, proporciona el ID de Chat de Telegram. (usa el comando '/id' en el bot) o (@userinfobot)\"\r\n\"subscriptionDesc\" = \"Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones.\"\r\n\"info\" = \"Info\"\r\n\"same\" = \"misma\"\r\n\"inboundData\" = \"Datos de entrada\"\r\n\"exportInbound\" = \"Exportación entrante\"\r\n\"import\" = \"Importar\"\r\n\"importInbound\" = \"Importar un entrante\"\r\n\"periodicTrafficResetTitle\" = \"Reset de Tráfico\"\r\n\"periodicTrafficResetDesc\" = \"Reiniciar automáticamente el contador de tráfico en intervalos especificados\"\r\n\"lastReset\" = \"Último reinicio\"\r\n\r\n[pages.client]\r\n\"add\" = \"Agregar Cliente\"\r\n\"edit\" = \"Editar Cliente\"\r\n\"submitAdd\" = \"Agregar Cliente\"\r\n\"submitEdit\" = \"Guardar Cambios\"\r\n\"clientCount\" = \"Número de Clientes\"\r\n\"bulk\" = \"Agregar en Lote\"\r\n\"method\" = \"Método\"\r\n\"first\" = \"Primero\"\r\n\"last\" = \"Último\"\r\n\"prefix\" = \"Prefijo\"\r\n\"postfix\" = \"Sufijo\"\r\n\"delayedStart\" = \"Iniciar después del primer uso\"\r\n\"expireDays\" = \"Duración\"\r\n\"days\" = \"Día(s)\"\r\n\"renew\" = \"Renovación automática\"\r\n\"renewDesc\" = \"Renovación automática después de la expiración. (0 = desactivar) (unidad: día)\"\r\n\r\n[pages.inbounds.periodicTrafficReset]\r\n\"never\" = \"Nunca\"\r\n\"daily\" = \"Diariamente\"\r\n\"weekly\" = \"Semanalmente\"\r\n\"monthly\" = \"Mensualmente\"\r\n\r\n[pages.inbounds.toasts]\r\n\"obtain\" = \"Recibir\"\r\n\"updateSuccess\" = \"La actualización fue exitosa\"\r\n\"logCleanSuccess\" = \"El registro ha sido limpiado\"\r\n\"inboundsUpdateSuccess\" = \"Entradas actualizadas correctamente\"\r\n\"inboundUpdateSuccess\" = \"Entrada actualizada correctamente\"\r\n\"inboundCreateSuccess\" = \"Entrada creada correctamente\"\r\n\"inboundDeleteSuccess\" = \"Entrada eliminada correctamente\"\r\n\"inboundClientAddSuccess\" = \"Cliente(s) de entrada añadido(s)\"\r\n\"inboundClientDeleteSuccess\" = \"Cliente de entrada eliminado\"\r\n\"inboundClientUpdateSuccess\" = \"Cliente de entrada actualizado\"\r\n\"delDepletedClientsSuccess\" = \"Todos los clientes con tráfico agotado fueron eliminados\"\r\n\"resetAllClientTrafficSuccess\" = \"Todo el tráfico del cliente ha sido reiniciado\"\r\n\"resetAllTrafficSuccess\" = \"Todo el tráfico ha sido reiniciado\"\r\n\"resetInboundClientTrafficSuccess\" = \"El tráfico ha sido reiniciado\"\r\n\"trafficGetError\" = \"Error al obtener los tráficos\"\r\n\"getNewX25519CertError\" = \"Error al obtener el certificado X25519.\"\r\n\"getNewmldsa65Error\" = \"Error al obtener el certificado mldsa65.\"\r\n\"getNewVlessEncError\" = \"Error al obtener el certificado VlessEnc.\"\r\n\r\n[pages.inbounds.stream.general]\r\n\"request\" = \"Pedido\"\r\n\"response\" = \"Respuesta\"\r\n\"name\" = \"Nombre\"\r\n\"value\" = \"Valor\"\r\n\r\n[pages.inbounds.stream.tcp]\r\n\"version\" = \"Versión\"\r\n\"method\" = \"Método\"\r\n\"path\" = \"Camino\"\r\n\"status\" = \"Estado\"\r\n\"statusDescription\" = \"Descripción de la Situación\"\r\n\"requestHeader\" = \"Encabezado de solicitud\"\r\n\"responseHeader\" = \"Encabezado de respuesta\"\r\n\r\n[pages.settings]\r\n\"title\" = \"Configuraciones\"\r\n\"save\" = \"Guardar\"\r\n\"infoDesc\" = \"Cada cambio realizado aquí debe ser guardado. Por favor, reinicie el panel para aplicar los cambios.\"\r\n\"restartPanel\" = \"Reiniciar Panel\"\r\n\"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.\"\r\n\"restartPanelSuccess\" = \"El panel se reinició correctamente\"\r\n\"actions\" = \"Acciones\"\r\n\"resetDefaultConfig\" = \"Restablecer a Configuración Predeterminada\"\r\n\"panelSettings\" = \"Configuraciones del Panel\"\r\n\"securitySettings\" = \"Configuraciones de Seguridad\"\r\n\"TGBotSettings\" = \"Configuraciones de Bot de Telegram\"\r\n\"panelListeningIP\" = \"IP de Escucha del Panel\"\r\n\"panelListeningIPDesc\" = \"Dejar en blanco por defecto para monitorear todas las IPs.\"\r\n\"panelListeningDomain\" = \"Dominio de Escucha del Panel\"\r\n\"panelListeningDomainDesc\" = \"Dejar en blanco por defecto para monitorear todos los dominios e IPs.\"\r\n\"panelPort\" = \"Puerto del Panel\"\r\n\"panelPortDesc\" = \"El puerto utilizado para mostrar este panel.\"\r\n\"publicKeyPath\" = \"Ruta del Archivo de Clave Pública del Certificado del Panel\"\r\n\"publicKeyPathDesc\" = \"Complete con una ruta absoluta que comience con.\"\r\n\"privateKeyPath\" = \"Ruta del Archivo de Clave Privada del Certificado del Panel\"\r\n\"privateKeyPathDesc\" = \"Complete con una ruta absoluta que comience con.\"\r\n\"panelUrlPath\" = \"Ruta Raíz de la URL del Panel\"\r\n\"panelUrlPathDesc\" = \"Debe empezar con '/' y terminar con.\"\r\n\"pageSize\" = \"Tamaño de paginación\"\r\n\"pageSizeDesc\" = \"Defina el tamaño de página para la tabla de entradas. Establezca 0 para desactivar\"\r\n\"remarkModel\" = \"Modelo de observación y carácter de separación\"\r\n\"datepicker\" = \"selector de fechas\"\r\n\"datepickerPlaceholder\" = \"Seleccionar fecha\"\r\n\"datepickerDescription\" = \"El tipo de calendario selector especifica la fecha de vencimiento\"\r\n\"sampleRemark\" = \"Observación de muestra\"\r\n\"oldUsername\" = \"Nombre de Usuario Actual\"\r\n\"currentPassword\" = \"Contraseña Actual\"\r\n\"newUsername\" = \"Nuevo Nombre de Usuario\"\r\n\"newPassword\" = \"Nueva Contraseña\"\r\n\"telegramBotEnable\" = \"Habilitar bot de Telegram\"\r\n\"telegramBotEnableDesc\" = \"Conéctese a las funciones de este panel a través del bot de Telegram.\"\r\n\"telegramToken\" = \"Token de Telegram\"\r\n\"telegramTokenDesc\" = \"Debe obtener el token del administrador de bots de Telegram @botfather.\"\r\n\"telegramProxy\" = \"Socks5 Proxy\"\r\n\"telegramProxyDesc\" = \"Si necesita el proxy Socks5 para conectarse a Telegram. Ajuste su configuración según la guía.\"\r\n\"telegramAPIServer\" = \"API Server de Telegram\"\r\n\"telegramAPIServerDesc\" = \"El servidor API de Telegram a utilizar. Déjelo en blanco para utilizar el servidor predeterminado.\"\r\n\"telegramChatId\" = \"IDs de Chat de Telegram para Administradores\"\r\n\"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.\"\r\n\"telegramNotifyTime\" = \"Hora de Notificación del Bot de Telegram\"\r\n\"telegramNotifyTimeDesc\" = \"Usar el formato de tiempo de Crontab.\"\r\n\"tgNotifyBackup\" = \"Respaldo de Base de Datos\"\r\n\"tgNotifyBackupDesc\" = \"Incluir archivo de respaldo de base de datos con notificación de informe.\"\r\n\"tgNotifyLogin\" = \"Notificación de Inicio de Sesión\"\r\n\"tgNotifyLoginDesc\" = \"Muestra el nombre de usuario, dirección IP y hora cuando alguien intenta iniciar sesión en su panel.\"\r\n\"sessionMaxAge\" = \"Edad Máxima de Sesión\"\r\n\"sessionMaxAgeDesc\" = \"La duración de una sesión de inicio de sesión (unidad: minutos).\"\r\n\"expireTimeDiff\" = \"Umbral de Expiración para Notificación\"\r\n\"expireTimeDiffDesc\" = \"Reciba notificaciones sobre la expiración de la cuenta antes del umbral (unidad: días).\"\r\n\"trafficDiff\" = \"Umbral de Tráfico para Notificación\"\r\n\"trafficDiffDesc\" = \"Reciba notificaciones sobre el agotamiento del tráfico antes de alcanzar el umbral (unidad: GB).\"\r\n\"tgNotifyCpu\" = \"Umbral de Alerta de Porcentaje de CPU\"\r\n\"tgNotifyCpuDesc\" = \"Reciba notificaciones si el uso de la CPU supera este umbral (unidad: %).\"\r\n\"timeZone\" = \"Zona Horaria\"\r\n\"timeZoneDesc\" = \"Las tareas programadas se ejecutan de acuerdo con la hora en esta zona horaria.\"\r\n\"subSettings\" = \"Suscripción\"\r\n\"subEnable\" = \"Habilitar Servicio\"\r\n\"subEnableDesc\" = \"Función de suscripción con configuración separada.\"\r\n\"subJsonEnable\" = \"Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente.\"\r\n\"subTitle\" = \"Título de la Suscripción\"\r\n\"subTitleDesc\" = \"Título mostrado en el cliente VPN\"\r\n\"subSupportUrl\" = \"URL de soporte\"\r\n\"subSupportUrlDesc\" = \"Enlace de soporte técnico mostrado en el cliente VPN\"\r\n\"subProfileUrl\" = \"URL del perfil\"\r\n\"subProfileUrlDesc\" = \"Un enlace a tu sitio web mostrado en el cliente VPN\"\r\n\"subAnnounce\" = \"Anuncio\"\r\n\"subAnnounceDesc\" = \"El texto del anuncio mostrado en el cliente VPN\"\r\n\"subEnableRouting\" = \"Habilitar enrutamiento\"\r\n\"subEnableRoutingDesc\" = \"Configuración global para habilitar el enrutamiento en el cliente VPN. (Solo para Happ)\"\r\n\"subRoutingRules\" = \"Reglas de enrutamiento\"\r\n\"subRoutingRulesDesc\" = \"Reglas de enrutamiento globales para el cliente VPN. (Solo para Happ)\"\r\n\"subListen\" = \"Listening IP\"\r\n\"subListenDesc\" = \"Dejar en blanco por defecto para monitorear todas las IPs.\"\r\n\"subPort\" = \"Puerto de Suscripción\"\r\n\"subPortDesc\" = \"El número de puerto para el servicio de suscripción debe estar sin usar en el servidor.\"\r\n\"subCertPath\" = \"Ruta del Archivo de Clave Pública del Certificado de Suscripción\"\r\n\"subCertPathDesc\" = \"Complete con una ruta absoluta que comience con '/'\"\r\n\"subKeyPath\" = \"Ruta del Archivo de Clave Privada del Certificado de Suscripción\"\r\n\"subKeyPathDesc\" = \"Complete con una ruta absoluta que comience con '/'\"\r\n\"subPath\" = \"Ruta Raíz de la URL de Suscripción\"\r\n\"subPathDesc\" = \"Debe empezar con '/' y terminar con '/'\"\r\n\"subDomain\" = \"Dominio de Escucha\"\r\n\"subDomainDesc\" = \"Dejar en blanco por defecto para monitorear todos los dominios e IPs.\"\r\n\"subUpdates\" = \"Intervalos de Actualización de Suscripción\"\r\n\"subUpdatesDesc\" = \"Horas de intervalo entre actualizaciones en la aplicación del cliente.\"\r\n\"subEncrypt\" = \"Encriptar configuraciones\"\r\n\"subEncryptDesc\" = \"Encriptar las configuraciones devueltas en la suscripción.\"\r\n\"subShowInfo\" = \"Mostrar información de uso\"\r\n\"subShowInfoDesc\" = \"Mostrar tráfico restante y fecha después del nombre de configuración.\"\r\n\"subURI\" = \"URI de proxy inverso\"\r\n\"externalTrafficInformEnable\" = \"Informe de tráfico externo\"\r\n\"externalTrafficInformEnableDesc\" = \"Informar a la API externa sobre cada actualización de tráfico.\"\r\n\"externalTrafficInformURI\" = \"URI de información de tráfico externo\"\r\n\"externalTrafficInformURIDesc\" = \"Las actualizaciones de tráfico se envían a este URI.\"\r\n\"subURIDesc\" = \"Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy\"\r\n\"fragment\" = \"Fragmentación\"\r\n\"fragmentDesc\" = \"Habilitar la fragmentación para el paquete de saludo de TLS\"\r\n\"fragmentSett\" = \"Configuración de Fragmentación\"\r\n\"noisesDesc\" = \"Activar Sonidos\"\r\n\"noisesSett\" = \"Configuración de Sonidos\"\r\n\"mux\" = \"Mux\"\r\n\"muxDesc\" = \"Transmite múltiples flujos de datos independientes dentro de un flujo de datos establecido.\"\r\n\"muxSett\" = \"Configuración Mux\"\r\n\"direct\" = \"Conexión Directa\"\r\n\"directDesc\" = \"Establece conexiones directas con dominios o rangos de IP de un país específico.\"\r\n\"notifications\" = \"Notificaciones\"\r\n\"certs\" = \"Certificados\"\r\n\"externalTraffic\" = \"Tráfico Externo\"\r\n\"dateAndTime\" = \"Fecha y Hora\"\r\n\"proxyAndServer\" = \"Proxy y Servidor\"\r\n\"intervals\" = \"Intervalos\"\r\n\"information\" = \"Información\"\r\n\"language\" = \"Idioma\"\r\n\"telegramBotLanguage\" = \"Idioma del Bot de Telegram\"\r\n\r\n[pages.xray]\r\n\"title\" = \"Xray Configuración\"\r\n\"save\" = \"Guardar configuración\"\r\n\"restart\" = \"Reiniciar Xray\"\r\n\"restartSuccess\" = \"Xray se ha reiniciado correctamente\"\r\n\"stopSuccess\" = \"Xray se ha detenido correctamente\"\r\n\"restartError\" = \"Ocurrió un error al reiniciar Xray.\"\r\n\"stopError\" = \"Ocurrió un error al detener Xray.\"\r\n\"basicTemplate\" = \"Perfil Básico\"\r\n\"advancedTemplate\" = \"Perfil Avanzado\"\r\n\"generalConfigs\" = \"Configuraciones Generales\"\r\n\"generalConfigsDesc\" = \"Estas opciones proporcionarán ajustes generales.\"\r\n\"logConfigs\" = \"Registro\"\r\n\"logConfigsDesc\" = \"Los registros pueden afectar la eficiencia de su servidor. Se recomienda habilitarlos sabiamente solo en caso de sus necesidades.\"\r\n\"blockConfigsDesc\" = \"Estas opciones evitarán que los usuarios se conecten a protocolos y sitios web específicos.\"\r\n\"basicRouting\" = \"Enrutamiento Básico\"\r\n\"blockConnectionsConfigsDesc\" = \"Estas opciones bloquearán el tráfico según el país solicitado específico.\"\r\n\"directConnectionsConfigsDesc\" = \"Una conexión directa asegura que el tráfico específico no sea enrutado a través de otro servidor.\"\r\n\"blockips\" = \"Bloquear IPs\"\r\n\"blockdomains\" = \"Bloquear Dominios\"\r\n\"directips\" = \"IPs Directas\"\r\n\"directdomains\" = \"Dominios Directos\"\r\n\"ipv4Routing\" = \"Enrutamiento IPv4\"\r\n\"ipv4RoutingDesc\" = \"Estas opciones solo enrutarán a los dominios objetivo a través de IPv4.\"\r\n\"warpRouting\" = \"Enrutamiento WARP\"\r\n\"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.\"\r\n\"Template\" = \"Plantilla de Configuración de Xray\"\r\n\"TemplateDesc\" = \"Genera el archivo de configuración final de Xray basado en esta plantilla.\"\r\n\"FreedomStrategy\" = \"Configurar Estrategia para el Protocolo Freedom\"\r\n\"FreedomStrategyDesc\" = \"Establece la estrategia de salida de la red en el Protocolo Freedom.\"\r\n\"RoutingStrategy\" = \"Configurar Estrategia de Enrutamiento de Dominios\"\r\n\"RoutingStrategyDesc\" = \"Establece la estrategia general de enrutamiento para la resolución de DNS.\"\r\n\"outboundTestUrl\" = \"URL de prueba de outbound\"\r\n\"outboundTestUrlDesc\" = \"URL usada al probar la conectividad del outbound\"\r\n\"Torrent\" = \"Prohibir Uso de BitTorrent\"\r\n\"Inbounds\" = \"Entrante\"\r\n\"InboundsDesc\" = \"Cambia la plantilla de configuración para aceptar clientes específicos.\"\r\n\"Outbounds\" = \"Salidas\"\r\n\"Balancers\" = \"Equilibradores\"\r\n\"OutboundsDesc\" = \"Cambia la plantilla de configuración para definir formas de salida para este servidor.\"\r\n\"Routings\" = \"Reglas de enrutamiento\"\r\n\"RoutingsDesc\" = \"¡La prioridad de cada regla es importante!\"\r\n\"completeTemplate\" = \"Todos\"\r\n\"logLevel\" = \"Nivel de registro\"\r\n\"logLevelDesc\" = \"El nivel de registro para registros de errores, que indica la información que debe registrarse.\"\r\n\"accessLog\" = \"Registro de acceso\"\r\n\"accessLogDesc\" = \"La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso\"\r\n\"errorLog\" = \"Registro de Errores\"\r\n\"errorLogDesc\" = \"La ruta del archivo para el registro de errores. El valor especial 'none' desactiva los registros de errores.\"\r\n\"dnsLog\" = \"Registro DNS\"\r\n\"dnsLogDesc\" = \"Si habilitar los registros de consulta DNS\"\r\n\"maskAddress\" = \"Enmascarar Dirección\"\r\n\"maskAddressDesc\" = \"Máscara de dirección IP, cuando se habilita, reemplazará automáticamente la dirección IP que aparece en el registro.\"\r\n\"statistics\" = \"Estadísticas\"\r\n\"statsInboundUplink\" = \"Estadísticas de Subida de Entrada\"\r\n\"statsInboundUplinkDesc\" = \"Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de entrada.\"\r\n\"statsInboundDownlink\" = \"Estadísticas de Bajada de Entrada\"\r\n\"statsInboundDownlinkDesc\" = \"Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de entrada.\"\r\n\"statsOutboundUplink\" = \"Estadísticas de Subida de Salida\"\r\n\"statsOutboundUplinkDesc\" = \"Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de salida.\"\r\n\"statsOutboundDownlink\" = \"Estadísticas de Bajada de Salida\"\r\n\"statsOutboundDownlinkDesc\" = \"Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de salida.\"\r\n\r\n[pages.xray.rules]\r\n\"first\" = \"Primero\"\r\n\"last\" = \"Último\"\r\n\"up\" = \"Arriba\"\r\n\"down\" = \"Abajo\"\r\n\"source\" = \"Fuente\"\r\n\"dest\" = \"Destino\"\r\n\"inbound\" = \"Entrante\"\r\n\"outbound\" = \"Saliente\"\r\n\"balancer\" = \"Equilibrador\"\r\n\"info\" = \"Información\"\r\n\"add\" = \"Agregar Regla\"\r\n\"edit\" = \"Editar Regla\"\r\n\"useComma\" = \"Elementos separados por comas\"\r\n\r\n[pages.xray.outbound]\r\n\"addOutbound\" = \"Agregar salida\"\r\n\"addReverse\" = \"Agregar reverso\"\r\n\"editOutbound\" = \"Editar salida\"\r\n\"editReverse\" = \"Editar reverso\"\r\n\"tag\" = \"Etiqueta\"\r\n\"tagDesc\" = \"etiqueta única\"\r\n\"address\" = \"Dirección\"\r\n\"reverse\" = \"Reverso\"\r\n\"domain\" = \"Dominio\"\r\n\"type\" = \"Tipo\"\r\n\"bridge\" = \"puente\"\r\n\"portal\" = \"portal\"\r\n\"link\" = \"Enlace\"\r\n\"intercon\" = \"Interconexión\"\r\n\"settings\" = \"Configuración\"\r\n\"accountInfo\" = \"Información de la Cuenta\"\r\n\"outboundStatus\" = \"Estado de Salida\"\r\n\"sendThrough\" = \"Enviar a través de\"\r\n\"test\" = \"Probar\"\r\n\"testResult\" = \"Resultado de la prueba\"\r\n\"testing\" = \"Probando conexión...\"\r\n\"testSuccess\" = \"Prueba exitosa\"\r\n\"testFailed\" = \"Prueba fallida\"\r\n\"testError\" = \"Error al probar la salida\"\r\n\r\n[pages.xray.balancer]\r\n\"addBalancer\" = \"Agregar equilibrador\"\r\n\"editBalancer\" = \"Editar balanceador\"\r\n\"balancerStrategy\" = \"Estrategia\"\r\n\"balancerSelectors\" = \"Selectores\"\r\n\"tag\" = \"Etiqueta\"\r\n\"tagDesc\" = \"etiqueta única\"\r\n\"balancerDesc\" = \"No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag.\"\r\n\r\n[pages.xray.wireguard]\r\n\"secretKey\" = \"Llave secreta\"\r\n\"publicKey\" = \"Llave pública\"\r\n\"allowedIPs\" = \"IP permitidas\"\r\n\"endpoint\" = \"Punto final\"\r\n\"psk\" = \"Clave precompartida\"\r\n\"domainStrategy\" = \"Estrategia de dominio\"\r\n\r\n[pages.xray.tun]\r\n\"nameDesc\" = \"El nombre de la interfaz TUN. El valor predeterminado es 'xray0'\"\r\n\"mtuDesc\" = \"Unidad Máxima de Transmisión. El tamaño máximo de los paquetes de datos. El valor predeterminado es 1500\"\r\n\"userLevel\" = \"Nivel de Usuario\"\r\n\"userLevelDesc\" = \"Todas las conexiones realizadas a través de este entrada utilizarán este nivel de usuario. El valor predeterminado es 0\"\r\n\r\n[pages.xray.dns]\r\n\"enable\" = \"Habilitar DNS\"\r\n\"enableDesc\" = \"Habilitar servidor DNS incorporado\"\r\n\"tag\" = \"Etiqueta de Entrada DNS\"\r\n\"tagDesc\" = \"Esta etiqueta estará disponible como una etiqueta de entrada en las reglas de enrutamiento.\"\r\n\"clientIp\" = \"IP del cliente\"\r\n\"clientIpDesc\" = \"Se utiliza para notificar al servidor la ubicación IP especificada durante las consultas DNS\"\r\n\"disableCache\" = \"Desactivar caché\"\r\n\"disableCacheDesc\" = \"Desactiva el almacenamiento en caché de DNS\"\r\n\"disableFallback\" = \"Desactivar respaldo\"\r\n\"disableFallbackDesc\" = \"Desactiva las consultas DNS de respaldo\"\r\n\"disableFallbackIfMatch\" = \"Desactivar respaldo si coincide\"\r\n\"disableFallbackIfMatchDesc\" = \"Desactiva las consultas DNS de respaldo cuando se acierta en la lista de dominios coincidentes del servidor DNS\"\r\n\"enableParallelQuery\" = \"Habilitar consulta paralela\"\r\n\"enableParallelQueryDesc\" = \"Habilitar consultas DNS paralelas a múltiples servidores para una resolución más rápida\"\r\n\"strategy\" = \"Estrategia de Consulta\"\r\n\"strategyDesc\" = \"Estrategia general para resolver nombres de dominio\"\r\n\"add\" = \"Agregar Servidor\"\r\n\"edit\" = \"Editar Servidor\"\r\n\"domains\" = \"Dominios\"\r\n\"expectIPs\" = \"IPs esperadas\"\r\n\"unexpectIPs\" = \"IPs inesperadas\"\r\n\"useSystemHosts\" = \"Usar Hosts del sistema\"\r\n\"useSystemHostsDesc\" = \"Usar el archivo hosts de un sistema instalado\"\r\n\"usePreset\" = \"Usar plantilla\"\r\n\"dnsPresetTitle\" = \"Plantillas DNS\"\r\n\"dnsPresetFamily\" = \"Familiar\"\r\n\r\n[pages.xray.fakedns]\r\n\"add\" = \"Agregar DNS Falso\"\r\n\"edit\" = \"Editar DNS Falso\"\r\n\"ipPool\" = \"Subred del grupo de IP\"\r\n\"poolSize\" = \"Tamaño del grupo\"\r\n\r\n[pages.settings.security]\r\n\"admin\" = \"Credenciales de administrador\"\r\n\"twoFactor\" = \"Autenticación de dos factores\"\r\n\"twoFactorEnable\" = \"Habilitar 2FA\"\r\n\"twoFactorEnableDesc\" = \"Añade una capa adicional de autenticación para mayor seguridad.\"\r\n\"twoFactorModalSetTitle\" = \"Activar autenticación de dos factores\"\r\n\"twoFactorModalDeleteTitle\" = \"Desactivar autenticación de dos factores\"\r\n\"twoFactorModalSteps\" = \"Para configurar la autenticación de dos factores, sigue estos pasos:\"\r\n\"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\"\r\n\"twoFactorModalSecondStep\" = \"2. Ingresa el código de la aplicación\"\r\n\"twoFactorModalRemoveStep\" = \"Ingresa el código de la aplicación para eliminar la autenticación de dos factores.\"\r\n\"twoFactorModalChangeCredentialsTitle\" = \"Cambiar credenciales\"\r\n\"twoFactorModalChangeCredentialsStep\" = \"Ingrese el código de la aplicación para cambiar las credenciales del administrador.\"\r\n\"twoFactorModalSetSuccess\" = \"La autenticación de dos factores se ha establecido con éxito\"\r\n\"twoFactorModalDeleteSuccess\" = \"La autenticación de dos factores se ha eliminado con éxito\"\r\n\"twoFactorModalError\" = \"Código incorrecto\"\r\n\r\n[pages.settings.toasts]\r\n\"modifySettings\" = \"Los parámetros han sido modificados.\"\r\n\"getSettings\" = \"Ocurrió un error al obtener los parámetros.\"\r\n\"modifyUserError\" = \"Ocurrió un error al cambiar las credenciales del administrador.\"\r\n\"modifyUser\" = \"Has cambiado exitosamente las credenciales del administrador.\"\r\n\"originalUserPassIncorrect\" = \"Nombre de usuario o contraseña original incorrectos\"\r\n\"userPassMustBeNotEmpty\" = \"El nuevo nombre de usuario y la nueva contraseña no pueden estar vacíos\"\r\n\"getOutboundTrafficError\" = \"Error al obtener el tráfico saliente\"\r\n\"resetOutboundTrafficError\" = \"Error al reiniciar el tráfico saliente\"\r\n\r\n[tgbot]\r\n\"keyboardClosed\" = \"❌ Teclado cerrado!\"\r\n\"noResult\" = \"❗ ¡Sin resultados!\"\r\n\"noQuery\" = \"❌ ¡Consulta no encontrada! ¡Por favor, use el comando nuevamente!\"\r\n\"wentWrong\" = \"❌ ¡Algo salió mal!\"\r\n\"noIpRecord\" = \"❗ ¡No hay registro de IP!\"\r\n\"noInbounds\" = \"❗ ¡No se encontraron entradas!\"\r\n\"unlimited\" = \"♾ Ilimitado (Restablecer)\"\r\n\"add\" = \"Añadir\"\r\n\"month\" = \"Mes\"\r\n\"months\" = \"Meses\"\r\n\"day\" = \"Día\"\r\n\"days\" = \"Días\"\r\n\"hours\" = \"Horas\"\r\n\"minutes\" = \"Minutos\"\r\n\"unknown\" = \"Desconocido\"\r\n\"inbounds\" = \"Entradas\"\r\n\"clients\" = \"Clientes\"\r\n\"offline\" = \"🔴 Desconectado\"\r\n\"online\" = \"🟢 En línea\"\r\n\r\n[tgbot.commands]\r\n\"unknown\" = \"❗ Comando desconocido\"\r\n\"pleaseChoose\" = \"👇 Por favor elige:\\r\\n\"\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\"\r\n\"start\" = \"👋 Hola <i>{{ .Firstname }}</i>.\\r\\n\"\r\n\"welcome\" = \"🤖 Bienvenido al bot de gestión de <b>{{ .Hostname }}</b>.\\r\\n\"\r\n\"status\" = \"✅ ¡El bot está bien!\"\r\n\"usage\" = \"❗ ¡Por favor proporciona un texto para buscar!\"\r\n\"getID\" = \"🆔 Tu ID: <code>{{ .ID }}</code>\"\r\n\"helpAdminCommands\" = \"Para reiniciar Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\nPara buscar un correo electrónico de cliente:\\r\\n<code>/usage [Correo electrónico]</code>\\r\\n\\r\\nPara buscar entradas (con estadísticas de cliente):\\r\\n<code>/inbound [Observación]</code>\\r\\n\\r\\nID de Chat de Telegram:\\r\\n<code>/id</code>\"\r\n\"helpClientCommands\" = \"Para buscar estadísticas, utiliza el siguiente comando:\\r\\n<code>/usage [Correo electrónico]</code>\\r\\n\\r\\nID de Chat de Telegram:\\r\\n<code>/id</code>\"\r\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\r\n\"restartSuccess\" = \"✅ ¡Operación exitosa!\"\r\n\"restartFailed\" = \"❗ Error en la operación.\\r\\n\\r\\n<code>Error: {{ .Error }}</code>.\"\r\n\"xrayNotRunning\" = \"❗ Xray Core no está en ejecución.\"\r\n\"startDesc\" = \"Mostrar el menú principal\"\r\n\"helpDesc\" = \"Ayuda del bot\"\r\n\"statusDesc\" = \"Comprobar el estado del bot\"\r\n\"idDesc\" = \"Mostrar tu ID de Telegram\"\r\n\r\n[tgbot.messages]\r\n\"cpuThreshold\" = \"🔴 El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%\"\r\n\"selectUserFailed\" = \"❌ ¡Error al seleccionar usuario!\"\r\n\"userSaved\" = \"✅ Usuario de Telegram guardado.\"\r\n\"loginSuccess\" = \"✅ Has iniciado sesión en el panel con éxito.\\r\\n\"\r\n\"loginFailed\" = \"❗️ Falló el inicio de sesión en el panel.\\r\\n\"\r\n\"2faFailed\" = \"Error de 2FA\"\r\n\"report\" = \"🕰 Informes programados: {{ .RunTime }}\\r\\n\"\r\n\"datetime\" = \"⏰ Fecha y Hora: {{ .DateTime }}\\r\\n\"\r\n\"hostname\" = \"💻 Nombre del Host: {{ .Hostname }}\\r\\n\"\r\n\"version\" = \"🚀 Versión de X-UI: {{ .Version }}\\r\\n\"\r\n\"xrayVersion\" = \"📡 Versión de Xray: {{ .XrayVersion }}\\r\\n\"\r\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\r\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\r\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\r\n\"ips\" = \"🔢 IPs:\\r\\n{{ .IPs }}\\r\\n\"\r\n\"serverUpTime\" = \"⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\\r\\n\"\r\n\"serverLoad\" = \"📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\r\n\"serverMemory\" = \"📋 Memoria del servidor: {{ .Current }}/{{ .Total }}\\r\\n\"\r\n\"tcpCount\" = \"🔹 Conteo de TCP: {{ .Count }}\\r\\n\"\r\n\"udpCount\" = \"🔸 Conteo de UDP: {{ .Count }}\\r\\n\"\r\n\"traffic\" = \"🚦 Tráfico: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\r\n\"xrayStatus\" = \"ℹ️ Estado de Xray: {{ .State }}\\r\\n\"\r\n\"username\" = \"👤 Nombre de usuario: {{ .Username }}\\r\\n\"\r\n\"password\" = \"👤 Contraseña: {{ .Password }}\\r\\n\"\r\n\"time\" = \"⏰ Hora: {{ .Time }}\\r\\n\"\r\n\"inbound\" = \"📍 Inbound: {{ .Remark }}\\r\\n\"\r\n\"port\" = \"🔌 Puerto: {{ .Port }}\\r\\n\"\r\n\"expire\" = \"📅 Fecha de Vencimiento: {{ .Time }}\\r\\n\"\r\n\"expireIn\" = \"📅 Vence en: {{ .Time }}\\r\\n\"\r\n\"active\" = \"💡 Activo: {{ .Enable }}\\r\\n\"\r\n\"enabled\" = \"🚨 Habilitado: {{ .Enable }}\\r\\n\"\r\n\"online\" = \"🌐 Estado de conexión: {{ .Status }}\\r\\n\"\r\n\"lastOnline\" = \"🔙 Última conexión: {{ .Time }}\\r\\n\"\r\n\"email\" = \"📧 Email: {{ .Email }}\\r\\n\"\r\n\"upload\" = \"🔼 Subida: ↑{{ .Upload }}\\r\\n\"\r\n\"download\" = \"🔽 Bajada: ↓{{ .Download }}\\r\\n\"\r\n\"total\" = \"📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\\r\\n\"\r\n\"TGUser\" = \"👤 Usuario de Telegram: {{ .TelegramID }}\\r\\n\"\r\n\"exhaustedMsg\" = \"🚨 Agotado {{ .Type }}:\\r\\n\"\r\n\"exhaustedCount\" = \"🚨 Cantidad de Agotados {{ .Type }}:\\r\\n\"\r\n\"onlinesCount\" = \"🌐 Clientes en línea: {{ .Count }}\\r\\n\"\r\n\"disabled\" = \"🛑 Desactivado: {{ .Disabled }}\\r\\n\"\r\n\"depleteSoon\" = \"🔜 Se agotará pronto: {{ .Deplete }}\\r\\n\\r\\n\"\r\n\"backupTime\" = \"🗄 Hora de la Copia de Seguridad: {{ .Time }}\\r\\n\"\r\n\"refreshedOn\" = \"\\r\\n📋🔄 Actualizado en: {{ .Time }}\\r\\n\\r\\n\"\r\n\"yes\" = \"✅ Sí\"\r\n\"no\" = \"❌ No\"\r\n\"received_id\" = \"🔑📥 ID actualizado.\"\r\n\"received_password\" = \"🔑📥 Contraseña actualizada.\"\r\n\"received_email\" = \"📧📥 Correo electrónico actualizado.\"\r\n\"received_comment\" = \"💬📥 Comentario actualizado.\"\r\n\"id_prompt\" = \"🔑 ID predeterminado: {{ .ClientId }}\\n\\nIntroduce tu ID.\"\r\n\"pass_prompt\" = \"🔑 Contraseña predeterminada: {{ .ClientPassword }}\\n\\nIntroduce tu contraseña.\"\r\n\"email_prompt\" = \"📧 Correo electrónico predeterminado: {{ .ClientEmail }}\\n\\nIntroduce tu correo electrónico.\"\r\n\"comment_prompt\" = \"💬 Comentario predeterminado: {{ .ClientComment }}\\n\\nIntroduce tu comentario.\"\r\n\"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!\"\r\n\"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!\"\r\n\"cancel\" = \"❌ ¡Proceso cancelado! \\n\\nPuedes /start de nuevo en cualquier momento. 🔄\"\r\n\"error_add_client\" = \"⚠️ Error:\\n\\n {{ .error }}\"\r\n\"using_default_value\" = \"Está bien, me quedaré con el valor predeterminado. 😊\"\r\n\"incorrect_input\" = \"Tu entrada no es válida.\\nLas frases deben ser continuas sin espacios.\\nEjemplo correcto: aaaaaa\\nEjemplo incorrecto: aaa aaa 🚫\"\r\n\"AreYouSure\" = \"¿Estás seguro? 🤔\"\r\n\"SuccessResetTraffic\" = \"📧 Correo: {{ .ClientEmail }}\\n🏁 Resultado: ✅ Éxito\"\r\n\"FailedResetTraffic\" = \"📧 Correo: {{ .ClientEmail }}\\n🏁 Resultado: ❌ Fallido \\n\\n🛠️ Error: [ {{ .ErrorMessage }} ]\"\r\n\"FinishProcess\" = \"🔚 Proceso de reinicio de tráfico finalizado para todos los clientes.\"\r\n\r\n[tgbot.buttons]\r\n\"closeKeyboard\" = \"❌ Cerrar Teclado\"\r\n\"cancel\" = \"❌ Cancelar\"\r\n\"cancelReset\" = \"❌ Cancelar Reinicio\"\r\n\"cancelIpLimit\" = \"❌ Cancelar Límite de IP\"\r\n\"confirmResetTraffic\" = \"✅ ¿Confirmar Reinicio de Tráfico?\"\r\n\"confirmClearIps\" = \"✅ ¿Confirmar Limpiar IPs?\"\r\n\"confirmRemoveTGUser\" = \"✅ ¿Confirmar Eliminar Usuario de Telegram?\"\r\n\"confirmToggle\" = \"✅ ¿Confirmar habilitar/deshabilitar usuario?\"\r\n\"dbBackup\" = \"Obtener Copia de Seguridad de BD\"\r\n\"serverUsage\" = \"Uso del Servidor\"\r\n\"getInbounds\" = \"Obtener Entradas\"\r\n\"depleteSoon\" = \"Pronto se Agotará\"\r\n\"clientUsage\" = \"Obtener Uso\"\r\n\"onlines\" = \"Clientes en línea\"\r\n\"commands\" = \"Comandos\"\r\n\"refresh\" = \"🔄 Actualizar\"\r\n\"clearIPs\" = \"❌ Limpiar IPs\"\r\n\"removeTGUser\" = \"❌ Eliminar Usuario de Telegram\"\r\n\"selectTGUser\" = \"👤 Seleccionar Usuario de Telegram\"\r\n\"selectOneTGUser\" = \"👤 Selecciona un usuario de telegram:\"\r\n\"resetTraffic\" = \"📈 Reiniciar Tráfico\"\r\n\"resetExpire\" = \"📅 Cambiar fecha de Vencimiento\"\r\n\"ipLog\" = \"🔢 Registro de IP\"\r\n\"ipLimit\" = \"🔢 Límite de IP\"\r\n\"setTGUser\" = \"👤 Establecer Usuario de Telegram\"\r\n\"toggle\" = \"🔘 Habilitar / Deshabilitar\"\r\n\"custom\" = \"🔢 Costumbre\"\r\n\"confirmNumber\" = \"✅ Confirmar: {{ .Num }}\"\r\n\"confirmNumberAdd\" = \"✅ Confirmar agregando: {{ .Num }}\"\r\n\"limitTraffic\" = \"🚧 Límite de tráfico\"\r\n\"getBanLogs\" = \"Registros de prohibición\"\r\n\"allClients\" = \"Todos los Clientes\"\r\n\"addClient\" = \"Añadir cliente\"\r\n\"submitDisable\" = \"Enviar como deshabilitado ☑️\"\r\n\"submitEnable\" = \"Enviar como habilitado ✅\"\r\n\"use_default\" = \"🏷️ Usar por defecto\"\r\n\"change_id\" = \"⚙️🔑 ID\"\r\n\"change_password\" = \"⚙️🔑 Contraseña\"\r\n\"change_email\" = \"⚙️📧 Correo electrónico\"\r\n\"change_comment\" = \"⚙️💬 Comentario\"\r\n\"ResetAllTraffics\" = \"Reiniciar todo el tráfico\"\r\n\"SortedTrafficUsageReport\" = \"Informe de uso de tráfico ordenado\"\r\n\r\n[tgbot.answers]\r\n\"successfulOperation\" = \"✅ ¡Exitosa!\"\r\n\"errorOperation\" = \"❗ Error en la Operación.\"\r\n\"getInboundsFailed\" = \"❌ Error al obtener las entradas\"\r\n\"getClientsFailed\" = \"❌ No se pudo obtener los clientes.\"\r\n\"canceled\" = \"❌ {{ .Email }} : Operación cancelada.\"\r\n\"clientRefreshSuccess\" = \"✅ {{ .Email }} : Cliente actualizado exitosamente.\"\r\n\"IpRefreshSuccess\" = \"✅ {{ .Email }} : IPs actualizadas exitosamente.\"\r\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }} : Usuario de Telegram del cliente actualizado exitosamente.\"\r\n\"resetTrafficSuccess\" = \"✅ {{ .Email }} : Tráfico reiniciado exitosamente.\"\r\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }} : Límite de Tráfico guardado exitosamente.\"\r\n\"expireResetSuccess\" = \"✅ {{ .Email }} : Días de vencimiento reiniciados exitosamente.\"\r\n\"resetIpSuccess\" = \"✅ {{ .Email }} : Límite de IP {{ .Count }} guardado exitosamente.\"\r\n\"clearIpSuccess\" = \"✅ {{ .Email }} : IPs limpiadas exitosamente.\"\r\n\"getIpLog\" = \"✅ {{ .Email }} : Obtener Registro de IP.\"\r\n\"getUserInfo\" = \"✅ {{ .Email }} : Obtener Información de Usuario de Telegram.\"\r\n\"removedTGUserSuccess\" = \"✅ {{ .Email }} : Usuario de Telegram eliminado exitosamente.\"\r\n\"enableSuccess\" = \"✅ {{ .Email }} : Habilitado exitosamente.\"\r\n\"disableSuccess\" = \"✅ {{ .Email }} : Deshabilitado exitosamente.\"\r\n\"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: <code>{{ .TgUserID }}</code>\"\r\n\"chooseClient\" = \"Elige un Cliente para Inbound {{ .Inbound }}\"\r\n\"chooseInbound\" = \"Elige un Inbound\"\r\n"
  },
  {
    "path": "web/translation/translate.fa_IR.toml",
    "content": "\"username\" = \"نام‌کاربری\"\n\"password\" = \"رمزعبور\"\n\"login\" = \"ورود\"\n\"confirm\" = \"تایید\"\n\"cancel\" = \"انصراف\"\n\"close\" = \"بستن\"\n\"create\" = \"ایجاد\"\n\"update\" = \"به‌روزرسانی\"\n\"copy\" = \"کپی\"\n\"copied\" = \"کپی شد\"\n\"download\" = \"دانلود\"\n\"remark\" = \"نام\"\n\"enable\" = \"فعال\"\n\"protocol\" = \"پروتکل\"\n\"search\" = \"جستجو\"\n\"filter\" = \"فیلتر\"\n\"loading\" = \"...در حال بارگذاری\"\n\"second\" = \"ثانیه\"\n\"minute\" = \"دقیقه\"\n\"hour\" = \"ساعت\"\n\"day\" = \"روز\"\n\"check\" = \"چک کردن\"\n\"indefinite\" = \"نامحدود\"\n\"unlimited\" = \"نامحدود\"\n\"none\" = \"هیچ\"\n\"qrCode\" = \"QRکد\"\n\"info\" = \"اطلاعات بیشتر\"\n\"edit\" = \"ویرایش\"\n\"delete\" = \"حذف\"\n\"reset\" = \"ریست\"\n\"noData\" = \"داده‌ای وجود ندارد.\"\n\"copySuccess\" = \"باموفقیت کپی‌شد\"\n\"sure\" = \"مطمئن\"\n\"encryption\" = \"رمزگذاری\"\n\"useIPv4ForHost\" = \"از IPv4 برای میزبان استفاده کنید\"\n\"transmission\" = \"راه‌اتصال\"\n\"host\" = \"آدرس\"\n\"path\" = \"مسیر\"\n\"camouflage\" = \"مبهم‌سازی\"\n\"status\" = \"وضعیت\"\n\"enabled\" = \"فعال\"\n\"disabled\" = \"غیرفعال\"\n\"depleted\" = \"منقضی\"\n\"depletingSoon\" = \"در‌حال‌انقضا\"\n\"offline\" = \"آفلاین\"\n\"online\" = \"آنلاین\"\n\"domainName\" = \"آدرس دامنه\"\n\"monitor\" = \"آی‌پی اتصال\"\n\"certificate\" = \"گواهی دیجیتال\"\n\"fail\" = \"ناموفق\"\n\"comment\" = \"توضیحات\"\n\"success\" = \"موفق\"\n\"lastOnline\" = \"آخرین فعالیت\"\n\"getVersion\" = \"دریافت نسخه\"\n\"install\" = \"نصب\"\n\"clients\" = \"کاربران\"\n\"usage\" = \"استفاده\"\n\"twoFactorCode\" = \"کد\"\n\"remained\" = \"باقی‌مانده\"\n\"security\" = \"امنیت\"\n\"secAlertTitle\" = \"هشدار‌امنیتی\"\n\"secAlertSsl\" = \"این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید\"\n\"secAlertConf\" = \"تنظیمات خاصی در برابر حملات آسیب پذیر هستند. توصیه می‌شود پروتکل‌های امنیتی را برای جلوگیری از نفوذ احتمالی تقویت کنید\"\n\"secAlertSSL\" = \"پنل فاقد ارتباط امن است. لطفاً یک گواهینامه تی‌ال‌اس برای محافظت از داده‌ها نصب کنید\"\n\"secAlertPanelPort\" = \"استفاده از پورت پیش‌فرض پنل ناامن است. لطفاً یک پورت تصادفی یا خاص تنظیم کنید\"\n\"secAlertPanelURI\" = \"مسیر پیش‌فرض لینک پنل ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید\"\n\"secAlertSubURI\" = \"مسیر پیش‌فرض لینک سابسکریپشن ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید\"\n\"secAlertSubJsonURI\" = \"مسیر پیش‌فرض لینک سابسکریپشن جیسون ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید\"\n\"emptyDnsDesc\" = \"هیچ سرور DNS اضافه نشده است.\"\n\"emptyFakeDnsDesc\" = \"هیچ سرور Fake DNS اضافه نشده است.\"\n\"emptyBalancersDesc\" = \"هیچ بالانسر اضافه نشده است.\"\n\"emptyReverseDesc\" = \"هیچ پروکسی معکوس اضافه نشده است.\"\n\"somethingWentWrong\" = \"مشکلی پیش آمد\"\n\n[subscription]\n\"title\" = \"اطلاعات سابسکریپشن\"\n\"subId\" = \"شناسه اشتراک\"\n\"status\" = \"وضعیت\"\n\"downloaded\" = \"دانلود\"\n\"uploaded\" = \"آپلود\"\n\"expiry\" = \"تاریخ پایان\"\n\"totalQuota\" = \"حجم کلی\"\n\"individualLinks\" = \"لینک‌های تکی\"\n\"active\" = \"فعال\"\n\"inactive\" = \"غیرفعال\"\n\"unlimited\" = \"نامحدود\"\n\"noExpiry\" = \"بدون انقضا\"\n\n[menu]\n\"theme\" = \"تم\"\n\"dark\" = \"تیره\"\n\"ultraDark\" = \"فوق تیره\"\n\"dashboard\" = \"نمای کلی\"\n\"inbounds\" = \"ورودی‌ها\"\n\"settings\" = \"تنظیمات پنل\"\n\"xray\" = \"پیکربندی ایکس‌ری\"\n\"logout\" = \"خروج\"\n\"link\" = \"مدیریت\"\n\n[pages.login]\n\"hello\" = \"سلام\"\n\"title\" = \"خوش‌آمدید\"\n\"loginAgain\" = \"مدت زمان استفاده به‌اتمام‌رسیده، لطفا دوباره وارد شوید\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"اطلاعات به‌درستی وارد نشده‌است\"\n\"emptyUsername\" = \"لطفا یک نام‌کاربری وارد کنید‌\"\n\"emptyPassword\" = \"لطفا یک رمزعبور وارد کنید\"\n\"wrongUsernameOrPassword\" = \"نام کاربری، رمز عبور یا کد دو مرحله‌ای نامعتبر است.\"\n\"successLogin\" = \"شما با موفقیت به حساب کاربری خود وارد شدید.\"\n\n[pages.index]\n\"title\" = \"نمای کلی\"\n\"cpu\" = \"پردازنده\"\n\"logicalProcessors\" = \"پردازنده‌های منطقی\"\n\"frequency\" = \"فرکانس\"\n\"swap\" = \"سواپ\"\n\"storage\" = \"ذخیره‌سازی\"\n\"memory\" = \"حافظه رم\"\n\"threads\" = \"رشته‌ها\"\n\"xrayStatus\" = \"ایکس‌ری\"\n\"stopXray\" = \"توقف\"\n\"restartXray\" = \"شروع‌مجدد\"\n\"xraySwitch\" = \"‌نسخه\"\n\"xraySwitchClick\" = \"نسخه مورد نظر را انتخاب کنید\"\n\"xraySwitchClickDesk\" = \"لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمی‌تر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد\"\n\"xrayStatusUnknown\" = \"ناشناخته\"\n\"xrayStatusRunning\" = \"در حال اجرا\"\n\"xrayStatusStop\" = \"متوقف\"\n\"xrayStatusError\" = \"خطا\"\n\"xrayErrorPopoverTitle\" = \"خطا در هنگام اجرای Xray رخ داد\"\n\"operationHours\" = \"مدت‌کارکرد\"\n\"systemLoad\" = \"بارسیستم\"\n\"systemLoadDesc\" = \"میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته\"\n\"connectionCount\" = \"تعداد کانکشن ها\"\n\"ipAddresses\" = \"آدرس‌های IP\"\n\"toggleIpVisibility\" = \"تغییر وضعیت نمایش IP\"\n\"overallSpeed\" = \"سرعت کلی\"\n\"upload\" = \"آپلود\"\n\"download\" = \"دانلود\"\n\"totalData\" = \"داده‌های کل\"\n\"sent\" = \"ارسال شده\"\n\"received\" = \"دریافت شده\"\n\"documentation\" = \"مستندات\"\n\"xraySwitchVersionDialog\" = \"آیا واقعاً می‌خواهید نسخه Xray را تغییر دهید؟\"\n\"xraySwitchVersionDialogDesc\" = \"این کار نسخه Xray را به #version# تغییر می‌دهد.\"\n\"xraySwitchVersionPopover\" = \"Xray با موفقیت به‌روز شد\"\n\"geofileUpdateDialog\" = \"آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟\"\n\"geofileUpdateDialogDesc\" = \"این عمل فایل #filename# را به‌روز می‌کند.\"\n\"geofilesUpdateDialogDesc\" = \"با این کار همه فایل‌ها به‌روزرسانی می‌شوند.\"\n\"geofilesUpdateAll\" = \"همه را به‌روزرسانی کنید\"\n\"geofileUpdatePopover\" = \"فایل جغرافیایی با موفقیت به‌روز شد\"\n\"dontRefresh\" = \"در حال نصب، لطفا صفحه را رفرش نکنید\"\n\"logs\" = \"گزارش‌ها\"\n\"config\" = \"پیکربندی\"\n\"backup\" = \"پشتیبان‌گیری\"\n\"backupTitle\" = \"پشتیبان‌گیری دیتابیس\"\n\"exportDatabase\" = \"پشتیبان‌گیری\"\n\"exportDatabaseDesc\" = \"برای دانلود یک فایل .db حاوی پشتیبان از پایگاه داده فعلی خود به دستگاهتان کلیک کنید.\"\n\"importDatabase\" = \"بازیابی\"\n\"importDatabaseDesc\" = \"برای انتخاب و آپلود یک فایل .db از دستگاهتان و بازیابی پایگاه داده از یک پشتیبان کلیک کنید.\"\n\"importDatabaseSuccess\" = \"پایگاه داده با موفقیت وارد شد\"\n\"importDatabaseError\" = \"خطا در وارد کردن پایگاه داده\"\n\"readDatabaseError\" = \"خطا در خواندن پایگاه داده\"\n\"getDatabaseError\" = \"خطا در دریافت پایگاه داده\"\n\"getConfigError\" = \"خطا در دریافت فایل پیکربندی\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"کل ترافیک\"\n\"allTimeTrafficUsage\" = \"کل استفاده در تمام مدت\"\n\"title\" = \"کاربران\"\n\"totalDownUp\" = \"دریافت/ارسال کل\"\n\"totalUsage\" = \"‌‌‌مصرف کل\"\n\"inboundCount\" = \"کل ورودی‌ها\"\n\"operate\" = \"عملیات\"\n\"enable\" = \"فعال\"\n\"remark\" = \"نام\"\n\"protocol\" = \"پروتکل\"\n\"port\" = \"پورت\"\n\"portMap\" = \"پورت‌های نظیر\"\n\"traffic\" = \"ترافیک\"\n\"details\" = \"توضیحات\"\n\"transportConfig\" = \"نحوه اتصال\"\n\"expireDate\" = \"مدت زمان\"\n\"createdAt\" = \"ایجاد\"\n\"updatedAt\" = \"به‌روزرسانی\"\n\"resetTraffic\" = \"ریست ترافیک\"\n\"addInbound\" = \"افزودن ورودی\"\n\"generalActions\" = \"عملیات کلی\"\n\"autoRefresh\" = \"تازه‌سازی خودکار\"\n\"autoRefreshInterval\" = \"فاصله\"\n\"modifyInbound\" = \"ویرایش ورودی\"\n\"deleteInbound\" = \"حذف ورودی\"\n\"deleteInboundContent\" = \"آیا مطمئن به حذف ورودی هستید؟\"\n\"deleteClient\" = \"حذف کاربر\"\n\"deleteClientContent\" = \"آیا مطمئن به حذف کاربر هستید؟\"\n\"resetTrafficContent\" = \"آیا مطمئن به ریست ترافیک هستید؟\"\n\"copyLink\" = \"کپی لینک\"\n\"address\" = \"آدرس\"\n\"network\" = \"شبکه\"\n\"destinationPort\" = \"پورت مقصد\"\n\"targetAddress\" = \"آدرس مقصد\"\n\"monitorDesc\" = \"به‌طور پیش‌فرض خالی‌بگذارید\"\n\"meansNoLimit\" = \"0 = واحد: گیگابایت) نامحدود)\"\n\"totalFlow\" = \"ترافیک کل\"\n\"leaveBlankToNeverExpire\" = \"برای منقضی‌نشدن خالی‌بگذارید\"\n\"noRecommendKeepDefault\" = \"توصیه‌می‌شود به‌طور پیش‌فرض حفظ‌شود\"\n\"certificatePath\" = \"مسیر فایل\"\n\"certificateContent\" = \"محتوای فایل\"\n\"publicKey\" = \"کلید عمومی\"\n\"privatekey\" = \"کلید خصوصی\"\n\"clickOnQRcode\" = \"برای کپی بر روی کدتصویری کلیک کنید\"\n\"client\" = \"کاربر\"\n\"export\" = \"استخراج لینک‌ها\"\n\"clone\" = \"شبیه‌سازی\"\n\"cloneInbound\" = \"شبیه‌سازی ورودی\"\n\"cloneInboundContent\" = \"همه موارد این ورودی بجز پورت، آی‌پی و کاربر‌ها شبیه‌سازی خواهند شد\"\n\"cloneInboundOk\" = \"ساختن شبیه ساز\"\n\"resetAllTraffic\" = \"ریست ترافیک کل ورودی‌ها\"\n\"resetAllTrafficTitle\" = \"ریست ترافیک کل ورودی‌ها\"\n\"resetAllTrafficContent\" = \"آیا مطمئن به ریست ترافیک تمام ورودی‌ها هستید؟\"\n\"resetInboundClientTraffics\" = \"ریست ترافیک کاربران\"\n\"resetInboundClientTrafficTitle\" = \"ریست ترافیک کاربران\"\n\"resetInboundClientTrafficContent\" = \"آیا مطمئن به ریست ترافیک تمام کاربران این‌ ورودی هستید؟\"\n\"resetAllClientTraffics\" = \"ریست ترافیک کل کاربران\"\n\"resetAllClientTrafficTitle\" = \"ریست ترافیک کل کاربران\"\n\"resetAllClientTrafficContent\" = \"آیا مطمئن به ریست ترافیک تمام کاربران هستید؟\"\n\"delDepletedClients\" = \"حذف کاربران منقضی\"\n\"delDepletedClientsTitle\" = \"حذف کاربران منقضی\"\n\"delDepletedClientsContent\" = \"آیا مطمئن به حذف تمام کاربران منقضی‌شده ‌هستید؟\"\n\"email\" = \"ایمیل\"\n\"emailDesc\" = \"باید یک ایمیل یکتا باشد\"\n\"IPLimit\" = \"محدودیت آی‌پی\"\n\"IPLimitDesc\" = \"(اگر تعداد از مقدار تنظیم شده بیشتر شود، ورودی را غیرفعال می کند. (0 = غیرفعال\"\n\"IPLimitlog\" = \"گزارش‌ها\"\n\"IPLimitlogDesc\" = \"گزارش تاریخچه آی‌پی. برای فعال کردن ورودی پس از غیرفعال شدن، گزارش را پاک کنید\"\n\"IPLimitlogclear\" = \"پاک کردن گزارش‌ها\"\n\"setDefaultCert\" = \"استفاده از گواهی پنل\"\n\"telegramDesc\" = \"لطفا شناسه گفتگوی تلگرام را وارد کنید. (از دستور '/id' در ربات استفاده کنید) یا (@userinfobot)\"\n\"subscriptionDesc\" = \"شما می‌توانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین می‌توانید از همین نام برای چندین کاربر استفاده‌کنید\"\n\"info\" = \"اطلاعات\"\n\"same\" = \"همسان\"\n\"inboundData\" = \"داده‌های ورودی\"\n\"exportInbound\" = \"استخراج ورودی\"\n\"import\" = \"افزودن\"\n\"importInbound\" = \"افزودن یک ورودی\"\n\"periodicTrafficResetTitle\" = \"بازنشانی ترافیک\"\n\"periodicTrafficResetDesc\" = \"بازنشانی خودکار شمارنده ترافیک در فواصل زمانی مشخص\"\n\"lastReset\" = \"آخرین بازنشانی\"\n\n[pages.client]\n\"add\" = \"کاربر جدید\"\n\"edit\" = \"ویرایش کاربر\"\n\"submitAdd\" = \"اضافه کردن\"\n\"submitEdit\" = \"ذخیره تغییرات\"\n\"clientCount\" = \"تعداد کاربران\"\n\"bulk\" = \"انبوه‌سازی\"\n\"method\" = \"روش\"\n\"first\" = \"از\"\n\"last\" = \"تا\"\n\"prefix\" = \"پیشوند\"\n\"postfix\" = \"پسوند\"\n\"delayedStart\" = \"شروع‌پس‌از‌اولین‌استفاده\"\n\"expireDays\" = \"مدت زمان\"\n\"days\" = \"(روز)\"\n\"renew\" = \"تمدید خودکار\"\n\"renewDesc\" = \"تمدید خودکار پس‌از ‌انقضا. (0 = غیرفعال)(واحد: روز)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"هرگز\"\n\"daily\" = \"روزانه\"\n\"weekly\" = \"هفتگی\"\n\"monthly\" = \"ماهانه\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"فراهم‌سازی\"\n\"updateSuccess\" = \"بروزرسانی با موفقیت انجام شد\"\n\"logCleanSuccess\" = \"لاگ پاکسازی شد\"\n\"inboundsUpdateSuccess\" = \"ورودی‌ها با موفقیت به‌روزرسانی شدند\"\n\"inboundUpdateSuccess\" = \"ورودی با موفقیت به‌روزرسانی شد\"\n\"inboundCreateSuccess\" = \"ورودی با موفقیت ایجاد شد\"\n\"inboundDeleteSuccess\" = \"ورودی با موفقیت حذف شد\"\n\"inboundClientAddSuccess\" = \"کلاینت(های) ورودی اضافه شدند\"\n\"inboundClientDeleteSuccess\" = \"کلاینت ورودی حذف شد\"\n\"inboundClientUpdateSuccess\" = \"کلاینت ورودی به‌روزرسانی شد\"\n\"delDepletedClientsSuccess\" = \"تمام کلاینت‌های مصرف شده حذف شدند\"\n\"resetAllClientTrafficSuccess\" = \"تمام ترافیک کلاینت بازنشانی شد\"\n\"resetAllTrafficSuccess\" = \"تمام ترافیک‌ها بازنشانی شدند\"\n\"resetInboundClientTrafficSuccess\" = \"ترافیک بازنشانی شد\"\n\"trafficGetError\" = \"خطا در دریافت ترافیک‌ها\"\n\"getNewX25519CertError\" = \"خطا در دریافت گواهی X25519.\"\n\"getNewmldsa65Error\" = \"خطا در دریافت گواهی mldsa65.\"\n\"getNewVlessEncError\" = \"خطا در دریافت گواهی VlessEnc.\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"درخواست\"\n\"response\" = \"پاسخ\"\n\"name\" = \"نام\"\n\"value\" = \"مقدار\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"نسخه\"\n\"method\" = \"متد\"\n\"path\" = \"مسیر\"\n\"status\" = \"وضعیت\"\n\"statusDescription\" = \"توضیحات وضعیت\"\n\"requestHeader\" = \"سربرگ درخواست\"\n\"responseHeader\" = \"سربرگ پاسخ\"\n\n[pages.settings]\n\"title\" = \"تنظیمات پنل\"\n\"save\" = \"ذخیره\"\n\"infoDesc\" = \"برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید\"\n\"restartPanel\" = \"ریستارت پنل\"\n\"restartPanelDesc\" = \"آیا مطمئن به ریستارت پنل هستید؟ اگر پس‌از ریستارت نمی‌توانید به پنل دسترسی پیدا کنید، لطفاً گزارش‌های موجود در اسکریپت پنل را بررسی کنید\"\n\"restartPanelSuccess\" = \"پنل با موفقیت راه‌اندازی مجدد شد\"\n\"actions\" = \"عملیات ها\"\n\"resetDefaultConfig\" = \"برگشت به پیش‌فرض\"\n\"panelSettings\" = \"پیکربندی\"\n\"securitySettings\" = \"احرازهویت\"\n\"TGBotSettings\" = \"ربات تلگرام\"\n\"panelListeningIP\" = \"آدرس آی‌پی\"\n\"panelListeningIPDesc\" = \"آدرس آی‌پی برای وب پنل. برای گوش‌دادن به‌تمام آی‌پی‌ها خالی‌بگذارید\"\n\"panelListeningDomain\" = \"نام دامنه\"\n\"panelListeningDomainDesc\" = \"آدرس دامنه برای وب پنل. برای گوش دادن به‌تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید\"\n\"panelPort\" = \"پورت\"\n\"panelPortDesc\" = \"شماره پورت برای وب پنل. باید پورت استفاده نشده‌باشد\"\n\"publicKeyPath\" = \"مسیر کلید عمومی\"\n\"publicKeyPathDesc\" = \"مسیر فایل کلیدعمومی برای وب پنل. با '/' شروع‌می‌شود\"\n\"privateKeyPath\" = \"مسیر کلید خصوصی\"\n\"privateKeyPathDesc\" = \"مسیر فایل کلیدخصوصی برای وب پنل. با '/' شروع‌می‌شود\"\n\"panelUrlPath\" = \"URI مسیر\"\n\"panelUrlPathDesc\" = \"برای وب پنل. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر\"\n\"pageSize\" = \"اندازه صفحه بندی جدول\"\n\"pageSizeDesc\" = \"(اندازه صفحه برای جدول ورودی‌ها.(0 = غیرفعال\"\n\"remarkModel\" = \"نام‌کانفیگ و جداکننده\"\n\"datepicker\" = \"نوع تقویم\"\n\"datepickerPlaceholder\" = \"انتخاب تاریخ\"\n\"datepickerDescription\" = \"وظایف برنامه ریزی شده بر اساس این تقویم اجرا می‌شود\"\n\"sampleRemark\" = \"نمونه‌نام\"\n\"oldUsername\" = \"نام‌کاربری فعلی\"\n\"currentPassword\" = \"رمز‌عبور فعلی\"\n\"newUsername\" = \"نام‌کاربری جدید\"\n\"newPassword\" = \"رمزعبور جدید\"\n\"telegramBotEnable\" = \"فعال‌سازی ربات تلگرام\"\n\"telegramBotEnableDesc\" = \"ربات تلگرام را فعال می‌کند\"\n\"telegramToken\" = \"توکن تلگرام\"\n\"telegramTokenDesc\" = \"دریافت کنید @botfather توکن را می‌توانید از\"\n\"telegramProxy\" = \"SOCKS پراکسی\"\n\"telegramProxyDesc\" = \"را برای اتصال به تلگرام فعال می کند SOCKS5 پراکسی\"\n\"telegramAPIServer\" = \"سرور API تلگرام\"\n\"telegramAPIServerDesc\" = \"API سرور تلگرام برای اتصال را تغییر میدهد. برای استفاده از سرور پیش فرض خالی بگذارید\"\n\"telegramChatId\" = \"آی‌دی چت مدیر\"\n\"telegramChatIdDesc\" = \"دریافت ‌کنید ('/id'یا (دستور (@userinfobot) آی‌دی(های) چت تلگرام مدیر، از\"\n\"telegramNotifyTime\" = \"زمان نوتیفیکیشن\"\n\"telegramNotifyTimeDesc\" = \"زمان‌اطلاع‌رسانی ربات تلگرام برای گزارش های دوره‌ای. از فرمت زمانبندی لینوکس استفاده‌کنید‌\"\n\"tgNotifyBackup\" = \"پشتیبان‌گیری از دیتابیس\"\n\"tgNotifyBackupDesc\" = \"فایل پشتیبان‌دیتابیس را به‌همراه گزارش ارسال می‌کند\"\n\"tgNotifyLogin\" = \"اعلان ورود\"\n\"tgNotifyLoginDesc\" = \"نام‌کاربری، آدرس آی‌پی، و زمان ورود، فردی که سعی می‌کند وارد پنل شود را نمایش می‌دهد\"\n\"sessionMaxAge\" = \"بیشینه زمان جلسه وب\"\n\"sessionMaxAgeDesc\" = \"(بیشینه زمانی که می‌توانید لاگین بمانید. (واحد: دقیقه\"\n\"expireTimeDiff\" = \"آستانه زمان باقی مانده\"\n\"expireTimeDiffDesc\" = \"(فاصله زمانی هشدار تا رسیدن به زمان انقضا. (واحد: روز\"\n\"trafficDiff\" = \"آستانه ترافیک باقی مانده\"\n\"trafficDiffDesc\" = \"(فاصله زمانی هشدار تا رسیدن به اتمام ترافیک. (واحد: گیگابایت\"\n\"tgNotifyCpu\" = \"آستانه هشدار بار پردازنده\"\n\"tgNotifyCpuDesc\" = \"(اگر بار روی پردازنده ازاین آستانه فراتر رفت، برای شما پیام ارسال می‌شود. (واحد: درصد\"\n\"timeZone\" = \"منطقه زمانی\"\n\"timeZoneDesc\" = \"وظایف برنامه ریزی شده بر اساس این منطقه‌زمانی اجرا می‌شود\"\n\"subSettings\" = \"سابسکریپشن\"\n\"subEnable\" = \"فعال‌سازی سرویس سابسکریپشن\"\n\"subEnableDesc\" = \"سرویس سابسکریپشن‌ را فعال‌می‌کند\"\n\"subJsonEnable\" = \"فعال/غیرفعال‌سازی مستقل نقطه دسترسی سابسکریپشن JSON.\"\n\"subTitle\" = \"عنوان اشتراک\"\n\"subTitleDesc\" = \"عنوان نمایش داده شده در کلاینت VPN\"\n\"subSupportUrl\" = \"آدرس پشتیبانی\"\n\"subSupportUrlDesc\" = \"لینک پشتیبانی فنی که در کلاینت VPN نمایش داده می‌شود\"\n\"subProfileUrl\" = \"آدرس پروفایل\"\n\"subProfileUrlDesc\" = \"لینک وب‌سایت شما که در کلاینت VPN نمایش داده می‌شود\"\n\"subAnnounce\" = \"اعلان\"\n\"subAnnounceDesc\" = \"متن اعلانی که در کلاینت VPN نمایش داده می‌شود\"\n\"subEnableRouting\" = \"فعال‌سازی مسیریابی\"\n\"subEnableRoutingDesc\" = \"تنظیمات سراسری برای فعال‌سازی مسیریابی در کلاینت VPN. (فقط برای Happ)\"\n\"subRoutingRules\" = \"قوانین مسیریابی\"\n\"subRoutingRulesDesc\" = \"قوانین مسیریابی سراسری برای کلاینت VPN. (فقط برای Happ)\"\n\"subListen\" = \"آدرس آی‌پی\"\n\"subListenDesc\" = \"آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید\"\n\"subPort\" = \"پورت\"\n\"subPortDesc\" = \"شماره پورت برای سرویس سابسکریپشن. باید پورت استفاده نشده‌باشد\"\n\"subCertPath\" = \"مسیر کلید عمومی\"\n\"subCertPathDesc\" = \"مسیر فایل کلیدعمومی برای سرویس سابیکریپشن. با '/' شروع‌می‌شود\"\n\"subKeyPath\" = \"مسیر کلید خصوصی\"\n\"subKeyPathDesc\" = \"مسیر فایل کلیدخصوصی برای سرویس سابسکریپشن. با '/' شروع‌می‌شود\"\n\"subPath\" = \"URI مسیر\"\n\"subPathDesc\" = \"برای سرویس سابسکریپشن. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر\"\n\"subDomain\" = \"نام دامنه\"\n\"subDomainDesc\" = \"آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌\"\n\"subUpdates\" = \"فاصله بروزرسانی‌ سابسکریپشن\"\n\"subUpdatesDesc\" = \"(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت\"\n\"subEncrypt\" = \"کدگذاری\"\n\"subEncryptDesc\" = \"کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه\"\n\"subShowInfo\" = \"نمایش اطلاعات مصرف\"\n\"subShowInfoDesc\" = \"ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد\"\n\"subURI\" = \"پروکسی معکوس URI مسیر\"\n\"subURIDesc\" = \"سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر\"\n\"externalTrafficInformEnable\" = \"اطلاع رسانی خارجی مصرف ترافیک\"\n\"externalTrafficInformEnableDesc\" = \"مصرف ترافیک به سرویس خارجی ارسال می شود\"\n\"externalTrafficInformURI\" = \"لینک اطلاع رسانی خارجی مصرف ترافیک\"\n\"externalTrafficInformURIDesc\" = \"ترافیک های مصرفی به این لینک هم ارسال می شود\"\n\"fragment\" = \"فرگمنت\"\n\"fragmentDesc\" = \"فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس\"\n\"fragmentSett\" = \"تنظیمات فرگمنت\"\n\"noisesDesc\" = \"فعال کردن Noises.\"\n\"noisesSett\" = \"تنظیمات Noises\"\n\"mux\" = \"ماکس\"\n\"muxDesc\" = \"چندین جریان داده مستقل را در یک جریان داده ثابت منتقل می کند\"\n\"muxSett\" = \"تنظیمات ماکس\"\n\"direct\" = \"اتصال مستقیم\"\n\"directDesc\" = \"به طور مستقیم با دامنه ها یا محدوده آی‌پی یک کشور خاص ارتباط برقرار می کند\"\n\"notifications\" = \"اعلان‌ها\"\n\"certs\" = \"گواهی‌ها\"\n\"externalTraffic\" = \"ترافیک خارجی\"\n\"dateAndTime\" = \"تاریخ و زمان\"\n\"proxyAndServer\" = \"پراکسی و سرور\"\n\"intervals\" = \"فواصل\"\n\"information\" = \"اطلاعات\"\n\"language\" = \"زبان\"\n\"telegramBotLanguage\" = \"زبان ربات تلگرام\"\n\n[pages.xray]\n\"title\" = \"پیکربندی ایکس‌ری\"\n\"save\" = \"ذخیره\"\n\"restart\" = \"ریستارت ایکس‌ری\"\n\"restartSuccess\" = \"Xray با موفقیت راه‌اندازی مجدد شد\"\n\"stopSuccess\" = \"Xray با موفقیت متوقف شد\"\n\"restartError\" = \"خطا در راه‌اندازی مجدد Xray.\"\n\"stopError\" = \"خطا در توقف Xray.\"\n\"basicTemplate\" = \"پایه\"\n\"advancedTemplate\" = \"پیشرفته\"\n\"generalConfigs\" = \"استراتژی‌ کلی\"\n\"generalConfigsDesc\" = \"این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند\"\n\"logConfigs\" = \"گزارش\"\n\"logConfigsDesc\" = \"گزارش‌ها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید\"\n\"blockConfigsDesc\" = \"این گزینه‌ها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود می‌کند\"\n\"basicRouting\" = \"مسیریابی پایه\"\n\"blockConnectionsConfigsDesc\" = \"این گزینه‌ها ترافیک را بر اساس کشور درخواست‌شده خاص مسدود می‌کنند.\"\n\"directConnectionsConfigsDesc\" = \"یک اتصال مستقیم تضمین می‌کند که ترافیک خاص از طریق سرور دیگری مسیریابی نشود.\"\n\"blockips\" = \"مسدود کردن آی‌پی‌ها\"\n\"blockdomains\" = \"مسدود کردن دامنه‌ها\"\n\"directips\" = \"آی‌پی‌های مستقیم\"\n\"directdomains\" = \"دامنه‌های مستقیم\"\n\"ipv4Routing\" = \"IPv4 مسیریابی\"\n\"ipv4RoutingDesc\" = \"این گزینه‌ها ترافیک را از طریق آی‌پی نسخه4 سرور، به مقصد هدایت می‌کند\"\n\"warpRouting\" = \"WARP مسیریابی\"\n\"warpRoutingDesc\" = \"این گزینه‌ها ترافیک‌ را از طریق وارپ کلادفلر به مقصد هدایت می‌کند\"\n\"Template\" = \"‌پیکربندی پیشرفته الگو ایکس‌ری\"\n\"TemplateDesc\" = \"فایل پیکربندی نهایی ایکس‌ری بر اساس این الگو ایجاد می‌شود\"\n\"FreedomStrategy\" = \"Freedom استراتژی پروتکل\"\n\"FreedomStrategyDesc\" = \"تعیین می‌کند Freedom استراتژی خروجی شبکه را برای پروتکل\"\n\"RoutingStrategy\" = \"استراتژی کلی مسیریابی\"\n\"RoutingStrategyDesc\" = \"استراتژی کلی مسیریابی برای حل تمام درخواست‌ها را تعیین می‌کند\"\n\"outboundTestUrl\" = \"آدرس تست خروجی\"\n\"outboundTestUrlDesc\" = \"آدرسی که برای تست اتصال خروجی استفاده می‌شود.\"\n\"Torrent\" = \"مسدودسازی پروتکل بیت‌تورنت\"\n\"Inbounds\" = \"ورودی‌ها\"\n\"InboundsDesc\" = \"پذیرش کلاینت خاص\"\n\"Outbounds\" = \"خروجی‌ها\"\n\"Balancers\" = \"بالانسرها\"\n\"OutboundsDesc\" = \"مسیر ترافیک خروجی را تنظیم کنید\"\n\"Routings\" = \"قوانین مسیریابی\"\n\"RoutingsDesc\" = \"اولویت هر قانون مهم است\"\n\"completeTemplate\" = \"کامل\"\n\"logLevel\" = \"سطح گزارش\"\n\"logLevelDesc\" = \"سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند.\"\n\"accessLog\" = \"مسیر گزارش\"\n\"accessLogDesc\" = \"مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارش‌های دسترسی را غیرفعال میکند.\"\n\"errorLog\" = \"گزارش خطا\"\n\"errorLogDesc\" = \"مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند\"\n\"dnsLog\" = \"گزارش DNS\"\n\"dnsLogDesc\" = \"آیا ثبت‌های درخواست DNS را فعال کنید\"\n\"maskAddress\" = \"پنهان کردن آدرس\"\n\"maskAddressDesc\" = \"پوشش آدرس IP، هنگامی که فعال می‌شود، به طور خودکار آدرس IP که در لاگ ظاهر می‌شود را جایگزین می‌کند.\"\n\"statistics\" = \"آمار\"\n\"statsInboundUplink\" = \"آمار آپلود ورودی\"\n\"statsInboundUplinkDesc\" = \"جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های ورودی را فعال می‌کند.\"\n\"statsInboundDownlink\" = \"آمار دانلود ورودی\"\n\"statsInboundDownlinkDesc\" = \"جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های ورودی را فعال می‌کند.\"\n\"statsOutboundUplink\" = \"آمار آپلود خروجی\"\n\"statsOutboundUplinkDesc\" = \"جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های خروجی را فعال می‌کند.\"\n\"statsOutboundDownlink\" = \"آمار دانلود خروجی\"\n\"statsOutboundDownlinkDesc\" = \"جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های خروجی را فعال می‌کند.\"\n\n[pages.xray.rules]\n\"first\" = \"اولین\"\n\"last\" = \"آخرین\"\n\"up\" = \"بالا\"\n\"down\" = \"پایین\"\n\"source\" = \"مبدا\"\n\"dest\" = \"مقصد\"\n\"inbound\" = \"ورودی\"\n\"outbound\" = \"خروجی\"\n\"balancer\" = \"بالانسر\"\n\"info\" = \"اطلاعات\"\n\"add\" = \"افزودن قانون\"\n\"edit\" = \"ویرایش قانون\"\n\"useComma\" = \"موارد جدا شده با کاما\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"افزودن خروجی\"\n\"addReverse\" = \"افزودن معکوس\"\n\"editOutbound\" = \"ویرایش خروجی\"\n\"editReverse\" = \"ویرایش معکوس\"\n\"tag\" = \"برچسب\"\n\"tagDesc\" = \"برچسب یگانه\"\n\"address\" = \"آدرس\"\n\"reverse\" = \"معکوس\"\n\"domain\" = \"دامنه\"\n\"type\" = \"نوع\"\n\"bridge\" = \"پل\"\n\"portal\" = \"پورتال\"\n\"link\" = \"لینک\"\n\"intercon\" = \"اتصال میانی\"\n\"settings\" = \"تنظیمات\"\n\"accountInfo\" = \"اطلاعات حساب\"\n\"outboundStatus\" = \"وضعیت خروجی\"\n\"sendThrough\" = \"ارسال با\"\n\"test\" = \"تست\"\n\"testResult\" = \"نتیجه تست\"\n\"testing\" = \"در حال تست اتصال...\"\n\"testSuccess\" = \"تست موفقیت‌آمیز\"\n\"testFailed\" = \"تست ناموفق\"\n\"testError\" = \"خطا در تست خروجی\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"افزودن بالانسر\"\n\"editBalancer\" = \"ویرایش بالانسر\"\n\"balancerStrategy\" = \"استراتژی\"\n\"balancerSelectors\" = \"انتخاب‌گرها\"\n\"tag\" = \"برچسب\"\n\"tagDesc\" = \"برچسب یگانه\"\n\"balancerDesc\" = \"امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد.\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"کلید شخصی\"\n\"publicKey\" = \"کلید عمومی\"\n\"allowedIPs\" = \"آی‌پی‌های مجاز\"\n\"endpoint\" = \"نقطه پایانی\"\n\"psk\" = \"کلید مشترک\"\n\"domainStrategy\" = \"استراتژی حل دامنه\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"نام رابط TUN. مقدار پیش‌فرض 'xray0' است\"\n\"mtuDesc\" = \"واحد انتقال حداکثر. بیشترین اندازه بسته‌های داده. مقدار پیش‌فرض 1500 است\"\n\"userLevel\" = \"سطح کاربر\"\n\"userLevelDesc\" = \"تمام اتصالات انجام‌شده از طریق این ورودی از این سطح کاربری استفاده خواهند کرد. مقدار پیش‌فرض 0 است\"\n\n[pages.xray.dns]\n\"enable\" = \"فعال کردن حل دامنه\"\n\"enableDesc\" = \"سرور حل دامنه داخلی را فعال کنید\"\n\"tag\" = \"برچسب\"\n\"tagDesc\" = \"این برچسب در قوانین مسیریابی به عنوان یک برچسب ورودی قابل استفاده خواهد بود\"\n\"clientIp\" = \"آی‌پی کلاینت\"\n\"clientIpDesc\" = \"برای اطلاع‌رسانی به سرور درباره مکان IP مشخص‌شده در طول درخواست‌های DNS استفاده می‌شود\"\n\"disableCache\" = \"غیرفعال‌سازی کش\"\n\"disableCacheDesc\" = \"کش DNS را غیرفعال می‌کند\"\n\"disableFallback\" = \"غیرفعال‌سازی Fallback\"\n\"disableFallbackDesc\" = \"درخواست‌های DNS Fallback را غیرفعال می‌کند\"\n\"disableFallbackIfMatch\" = \"غیرفعال‌سازی Fallback در صورت تطابق\"\n\"disableFallbackIfMatchDesc\" = \"درخواست‌های DNS Fallback را زمانی که لیست دامنه‌های مطابقت‌یافته سرور DNS فعال است، غیرفعال می‌کند\"\n\"enableParallelQuery\" = \"فعال‌سازی پرس‌وجوی موازی\"\n\"enableParallelQueryDesc\" = \"فعال‌سازی پرس‌وجوهای DNS موازی به چندین سرور برای وضوح سریع‌تر\"\n\"strategy\" = \"استراتژی پرس‌وجو\"\n\"strategyDesc\" = \"استراتژی کلی برای حل نام دامنه\"\n\"add\" = \"افزودن سرور\"\n\"edit\" = \"ویرایش سرور\"\n\"domains\" = \"دامنه‌ها\"\n\"expectIPs\" = \"آی‌پی‌های مورد انتظار\"\n\"unexpectIPs\" = \"آی‌پی‌های غیرمنتظره\"\n\"useSystemHosts\" = \"استفاده از Hosts سیستم\"\n\"useSystemHostsDesc\" = \"استفاده از فایل hosts یک سیستم نصب‌شده\"\n\"usePreset\" = \"استفاده از پیش‌تنظیم\"\n\"dnsPresetTitle\" = \"پیش‌تنظیم‌های DNS\"\n\"dnsPresetFamily\" = \"خانوادگی\"\n\n[pages.xray.fakedns]\n\"add\" = \"افزودن دی‌ان‌اس جعلی\"\n\"edit\" = \"ویرایش دی‌ان‌اس جعلی\"\n\"ipPool\" = \"زیرشبکه استخر آی‌پی\"\n\"poolSize\" = \"اندازه استخر\"\n\n[pages.settings.security]\n\"admin\" = \"اعتبارنامه‌های ادمین\"\n\"twoFactor\" = \"احراز هویت دو مرحله‌ای\"\n\"twoFactorEnable\" = \"فعال‌سازی 2FA\"\n\"twoFactorEnableDesc\" = \"یک لایه اضافی امنیتی برای احراز هویت فراهم می‌کند.\"\n\"twoFactorModalSetTitle\" = \"فعال‌سازی احراز هویت دو مرحله‌ای\"\n\"twoFactorModalDeleteTitle\" = \"غیرفعال‌سازی احراز هویت دو مرحله‌ای\"\n\"twoFactorModalSteps\" = \"برای راه‌اندازی احراز هویت دو مرحله‌ای، مراحل زیر را انجام دهید:\"\n\"twoFactorModalFirstStep\" = \"1. این کد QR را در برنامه احراز هویت اسکن کنید یا توکن کنار کد QR را کپی کرده و در برنامه بچسبانید\"\n\"twoFactorModalSecondStep\" = \"2. کد را از برنامه وارد کنید\"\n\"twoFactorModalRemoveStep\" = \"برای حذف احراز هویت دو مرحله‌ای، کد را از برنامه وارد کنید.\"\n\"twoFactorModalChangeCredentialsTitle\" = \"تغییر اعتبارنامه‌ها\"\n\"twoFactorModalChangeCredentialsStep\" = \"برای تغییر اعتبارنامه‌های مدیر، کد را از برنامه وارد کنید.\"\n\"twoFactorModalSetSuccess\" = \"احراز هویت دو مرحله‌ای با موفقیت برقرار شد\"\n\"twoFactorModalDeleteSuccess\" = \"احراز هویت دو مرحله‌ای با موفقیت حذف شد\"\n\"twoFactorModalError\" = \"کد نادرست\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"پارامترها تغییر کرده‌اند.\"\n\"getSettings\" = \"خطا در دریافت پارامترها\"\n\"modifyUserError\" = \"خطا در تغییر اعتبارنامه‌های مدیر سیستم.\"\n\"modifyUser\" = \"شما با موفقیت اعتبارنامه‌های مدیر سیستم را تغییر دادید.\"\n\"originalUserPassIncorrect\" = \"نام‌کاربری یا رمزعبور فعلی اشتباه‌است\"\n\"userPassMustBeNotEmpty\" = \"نام‌کاربری یا رمزعبور جدید خالی‌است\"\n\"getOutboundTrafficError\" = \"خطا در دریافت ترافیک خروجی\"\n\"resetOutboundTrafficError\" = \"خطا در بازنشانی ترافیک خروجی\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ صفحه کلید بسته شد!\"\n\"noResult\" = \"❗ نتیجه ای یافت نشد!\"\n\"noQuery\" = \"❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!\"\n\"wentWrong\" = \"❌ مشکلی پیش آمد!\"\n\"noIpRecord\" = \"❗ رکورد آی پی وجود ندارد!\"\n\"noInbounds\" = \"❗ هیچ ورودی یافت نشد!\"\n\"unlimited\" = \"♾ نامحدود(ریست)\"\n\"add\" = \"افزودن\"\n\"month\" = \"ماه\"\n\"months\" = \"ماه\"\n\"day\" = \"روز\"\n\"days\" = \"روز\"\n\"hours\" = \"ساعت\"\n\"minutes\" = \"دقیقه\"\n\"unknown\" = \"نامشخص\"\n\"inbounds\" = \"ورودی ها\"\n\"clients\" = \"کاربران\"\n\"offline\" = \"🔴 آفلاین\"\n\"online\" = \"🟢 آنلاین\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ دستور ناشناخته\"\n\"pleaseChoose\" = \"👇 لطفاً انتخاب کنید:\\r\\n\"\n\"help\" = \"🤖 به این ربات خوش آمدید! این ربات برای ارائه داده‌های خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را می‌دهد.\\r\\n\\r\\n\"\n\"start\" = \"👋 سلام <i>{{ .Firstname }}</i>.\\r\\n\"\n\"welcome\" = \"🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\\r\\n\"\n\"status\" = \"✅ ربات در حالت عادی است!\"\n\"usage\" = \"❗ لطفاً یک متن برای جستجو وارد کنید!\"\n\"getID\" = \"🆔 شناسه شما: <code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"برای راه‌اندازی مجدد Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\nبرای جستجوی ایمیل مشتری:\\r\\n<code>/usage [ایمیل]</code>\\r\\n\\r\\nبرای جستجوی ورودی‌ها (با آمار مشتری):\\r\\n<code>/inbound [توضیحات]</code>\\r\\n\\r\\nشناسه گفتگوی تلگرام:\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"برای جستجوی آمار، از دستور زیر استفاده کنید:\\r\\n<code>/usage [ایمیل]</code>\\r\\n\\r\\nشناسه گفتگوی تلگرام:\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ عملیات با موفقیت انجام شد!\"\n\"restartFailed\" = \"❗ خطا در عملیات.\\r\\n\\r\\n<code>خطا: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core در حال اجرا نیست.\"\n\"startDesc\" = \"نمایش منوی اصلی\"\n\"helpDesc\" = \"راهنمای ربات\"\n\"statusDesc\" = \"بررسی وضعیت ربات\"\n\"idDesc\" = \"نمایش شناسه تلگرام شما\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 بار ‌پردازنده {{ .Percent }}% بیشتر از آستانه است {{ .Threshold }}%\"\n\"selectUserFailed\" = \"❌ خطا در انتخاب کاربر!\"\n\"userSaved\" = \"✅ کاربر تلگرام ذخیره شد.\"\n\"loginSuccess\" = \"✅ با موفقیت به پنل وارد شدید.\\r\\n\"\n\"loginFailed\" = \"❗️ ورود به پنل ناموفق‌بود \\r\\n\"\n\"2faFailed\" = \"خطای 2FA\"\n\"report\" = \"🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ تاریخ‌وزمان: {{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 نام‌میزبان: {{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 نسخه‌پنل: {{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 نسخه‌هسته: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 آدرس‌آی‌پی: {{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 آدرس‌های آی‌پی:\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ مدت‌کارکردسیستم: {{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 بارسیستم: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 RAM: {{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP: {{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP: {{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ وضعیت‌ایکس‌ری: {{ .State }}\\r\\n\"\n\"username\" = \"👤 نام‌کاربری: {{ .Username }}\\r\\n\"\n\"password\" = \"👤 رمز عبور: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ زمان: {{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 نام‌ورودی: {{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 پورت: {{ .Port }}\\r\\n\"\n\"expire\" = \"📅 تاریخ‌انقضا: {{ .Time }}\\r\\n\\r\\n\"\n\"expireIn\" = \"📅 باقی‌ مانده‌ تا انقضا: {{ .Time }}\\r\\n\\r\\n\"\n\"active\" = \"💡 فعال: {{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 وضعیت: {{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 وضعیت اتصال: {{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 آخرین فعالیت: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 ایمیل: {{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 آپلود↑: {{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 دانلود↓: {{ .Download }}\\r\\n\"\n\"total\" = \"🔄 کل: {{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 کاربر تلگرام: {{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 {{ .Type }} به‌اتمام‌رسیده‌است:\\r\\n\"\n\"exhaustedCount\" = \"🚨 تعداد {{ .Type }} به‌اتمام‌رسیده‌است:\\r\\n\"\n\"onlinesCount\" = \"🌐 کاربران‌آنلاین: {{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 غیرفعال: {{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 به‌زودی‌به‌پایان‌خواهدرسید: {{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 زمان‌پشتیبان‌گیری: {{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 تازه‌سازی شده در: {{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ بله\"\n\"no\" = \"❌ خیر\"\n\"received_id\" = \"🔑📥 شناسه به‌روزرسانی شد.\"\n\"received_password\" = \"🔑📥 رمز عبور به‌روزرسانی شد.\"\n\"received_email\" = \"📧📥 ایمیل به‌روزرسانی شد.\"\n\"received_comment\" = \"💬📥 نظر به‌روزرسانی شد.\"\n\"id_prompt\" = \"🔑 شناسه پیش‌فرض: {{ .ClientId }}\\n\\nشناسه خود را وارد کنید.\"\n\"pass_prompt\" = \"🔑 رمز عبور پیش‌فرض: {{ .ClientPassword }}\\n\\nرمز عبور خود را وارد کنید.\"\n\"email_prompt\" = \"📧 ایمیل پیش‌فرض: {{ .ClientEmail }}\\n\\nایمیل خود را وارد کنید.\"\n\"comment_prompt\" = \"💬 نظر پیش‌فرض: {{ .ClientComment }}\\n\\nنظر خود را وارد کنید.\"\n\"inbound_client_data_id\" = \"🔄 ورودی: {{ .InboundRemark }}\\n\\n🔑 شناسه: {{ .ClientId }}\\n📧 ایمیل: {{ .ClientEmail }}\\n📊 ترافیک: {{ .ClientTraffic }}\\n📅 تاریخ انقضا: {{ .ClientExp }}\\n🌐 محدودیت IP: {{ .IpLimit }}\\n💬 توضیح: {{ .ClientComment }}\\n\\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!\"\n\"inbound_client_data_pass\" = \"🔄 ورودی: {{ .InboundRemark }}\\n\\n🔑 رمز عبور: {{ .ClientPass }}\\n📧 ایمیل: {{ .ClientEmail }}\\n📊 ترافیک: {{ .ClientTraffic }}\\n📅 تاریخ انقضا: {{ .ClientExp }}\\n🌐 محدودیت IP: {{ .IpLimit }}\\n💬 توضیح: {{ .ClientComment }}\\n\\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!\"\n\"cancel\" = \"❌ فرآیند لغو شد! \\n\\nمی‌توانید هر زمان که خواستید /start را دوباره اجرا کنید. 🔄\"\n\"error_add_client\" = \"⚠️ خطا:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"باشه، از مقدار پیش‌فرض استفاده می‌کنم. 😊\"\n\"incorrect_input\" = \"ورودی شما معتبر نیست.\\nعبارت‌ها باید بدون فاصله باشند.\\nمثال صحیح: aaaaaa\\nمثال نادرست: aaa aaa 🚫\"\n\"AreYouSure\" = \"مطمئنی؟ 🤔\"\n\"SuccessResetTraffic\" = \"📧 ایمیل: {{ .ClientEmail }}\\n🏁 نتیجه: ✅ موفقیت‌آمیز\"\n\"FailedResetTraffic\" = \"📧 ایمیل: {{ .ClientEmail }}\\n🏁 نتیجه: ❌ ناموفق \\n\\n🛠️ خطا: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید.\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ بستن کیبورد\"\n\"cancel\" = \"❌ لغو\"\n\"cancelReset\" = \"❌ لغو تنظیم مجدد\"\n\"cancelIpLimit\" = \"❌ لغو محدودیت آی‌پی\"\n\"confirmResetTraffic\" = \"✅ تأیید تنظیم مجدد ترافیک؟\"\n\"confirmClearIps\" = \"✅ تأیید پاک‌سازی آدرس‌های آی‌پی؟\"\n\"confirmRemoveTGUser\" = \"✅ تأیید حذف کاربر تلگرام؟\"\n\"confirmToggle\" = \"✅ تایید فعال/غیرفعال کردن کاربر؟\"\n\"dbBackup\" = \"دریافت پشتیبان\"\n\"serverUsage\" = \"استفاده از سیستم\"\n\"getInbounds\" = \"دریافت ورودی‌ها\"\n\"depleteSoon\" = \"به‌زودی به پایان خواهد رسید\"\n\"clientUsage\" = \"دریافت آمار کاربر\"\n\"onlines\" = \"کاربران آنلاین\"\n\"commands\" = \"دستورات\"\n\"refresh\" = \"🔄 تازه‌سازی\"\n\"clearIPs\" = \"❌ پاک‌سازی آدرس‌ها\"\n\"removeTGUser\" = \"❌ حذف کاربر تلگرام\"\n\"selectTGUser\" = \"👤 انتخاب کاربر تلگرام\"\n\"selectOneTGUser\" = \"👤 یک کاربر تلگرام را انتخاب کنید:\"\n\"resetTraffic\" = \"📈 تنظیم مجدد ترافیک\"\n\"resetExpire\" = \"📅 تنظیم مجدد تاریخ انقضا\"\n\"ipLog\" = \"🔢 لاگ آدرس‌های IP\"\n\"ipLimit\" = \"🔢 محدودیت IP\"\n\"setTGUser\" = \"👤 تنظیم کاربر تلگرام\"\n\"toggle\" = \"🔘 فعال / غیرفعال\"\n\"custom\" = \"🔢 سفارشی\"\n\"confirmNumber\" = \"✅ تایید: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ تایید اضافه کردن: {{ .Num }}\"\n\"limitTraffic\" = \"🚧 محدودیت ترافیک\"\n\"getBanLogs\" = \"گزارش های بلوک را دریافت کنید\"\n\"allClients\" = \"همه مشتریان\"\n\"addClient\" = \"افزودن مشتری\"\n\"submitDisable\" = \"ارسال به عنوان غیرفعال ☑️\"\n\"submitEnable\" = \"ارسال به عنوان فعال ✅\"\n\"use_default\" = \"🏷️ استفاده از پیش‌فرض\"\n\"change_id\" = \"⚙️🔑 شناسه\"\n\"change_password\" = \"⚙️🔑 گذرواژه\"\n\"change_email\" = \"⚙️📧 ایمیل\"\n\"change_comment\" = \"⚙️💬 نظر\"\n\"ResetAllTraffics\" = \"بازنشانی همه ترافیک‌ها\"\n\"SortedTrafficUsageReport\" = \"گزارش استفاده از ترافیک مرتب‌شده\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ انجام شد!\"\n\"errorOperation\" = \"❗ خطا در عملیات.\"\n\"getInboundsFailed\" = \"❌ دریافت ورودی‌ها با خطا مواجه شد.\"\n\"getClientsFailed\" = \"❌ دریافت مشتریان با شکست مواجه شد.\"\n\"canceled\" = \"❌ {{ .Email }} : عملیات لغو شد.\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }} : کلاینت با موفقیت تازه‌سازی شد.\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }} : آدرس‌ها با موفقیت تازه‌سازی شدند.\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازه‌سازی شد.\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد.\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }} : محدودیت ترافیک با موفقیت ذخیره شد.\"\n\"expireResetSuccess\" = \"✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد.\"\n\"resetIpSuccess\" = \"✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد.\"\n\"clearIpSuccess\" = \"✅ {{ .Email }} : آدرس‌ها با موفقیت پاک‌سازی شدند.\"\n\"getIpLog\" = \"✅ {{ .Email }} : دریافت لاگ آدرس‌های IP.\"\n\"getUserInfo\" = \"✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام.\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد.\"\n\"enableSuccess\" = \"✅ {{ .Email }} : با موفقیت فعال شد.\"\n\"disableSuccess\" = \"✅ {{ .Email }} : با موفقیت غیرفعال شد.\"\n\"askToAddUserId\" = \"پیکربندی شما یافت نشد!\\r\\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\\r\\n\\r\\nشناسه کاربری شما: <code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"یک مشتری برای ورودی {{ .Inbound }} انتخاب کنید\"\n\"chooseInbound\" = \"یک ورودی انتخاب کنید\"\n"
  },
  {
    "path": "web/translation/translate.id_ID.toml",
    "content": "\"username\" = \"Nama Pengguna\"\n\"password\" = \"Kata Sandi\"\n\"login\" = \"Masuk\"\n\"confirm\" = \"Konfirmasi\"\n\"cancel\" = \"Batal\"\n\"close\" = \"Tutup\"\n\"create\" = \"Buat\"\n\"update\" = \"Perbarui\"\n\"copy\" = \"Salin\"\n\"copied\" = \"Tersalin\"\n\"download\" = \"Unduh\"\n\"remark\" = \"Catatan\"\n\"enable\" = \"Aktifkan\"\n\"protocol\" = \"Protokol\"\n\"search\" = \"Cari\"\n\"filter\" = \"Filter\"\n\"loading\" = \"Memuat...\"\n\"second\" = \"Detik\"\n\"minute\" = \"Menit\"\n\"hour\" = \"Jam\"\n\"day\" = \"Hari\"\n\"check\" = \"Centang\"\n\"indefinite\" = \"Tak Terbatas\"\n\"unlimited\" = \"Tanpa Batas\"\n\"none\" = \"None\"\n\"qrCode\" = \"Kode QR\"\n\"info\" = \"Informasi Lebih Lanjut\"\n\"edit\" = \"Edit\"\n\"delete\" = \"Hapus\"\n\"reset\" = \"Reset\"\n\"noData\" = \"Tidak ada data.\"\n\"copySuccess\" = \"Berhasil Disalin\"\n\"sure\" = \"Yakin\"\n\"encryption\" = \"Enkripsi\"\n\"useIPv4ForHost\" = \"Gunakan IPv4 untuk host\"\n\"transmission\" = \"Transmisi\"\n\"host\" = \"Host\"\n\"path\" = \"Jalur\"\n\"camouflage\" = \"Obfuscation\"\n\"status\" = \"Status\"\n\"enabled\" = \"Aktif\"\n\"disabled\" = \"Nonaktif\"\n\"depleted\" = \"Habis\"\n\"depletingSoon\" = \"Akan Habis\"\n\"offline\" = \"Offline\"\n\"online\" = \"Online\"\n\"domainName\" = \"Nama Domain\"\n\"monitor\" = \"IP Pemantauan\"\n\"certificate\" = \"Sertifikat Digital\"\n\"fail\" = \"Gagal\"\n\"comment\" = \"Komentar\"\n\"success\" = \"Berhasil\"\n\"lastOnline\" = \"Terakhir online\"\n\"getVersion\" = \"Dapatkan Versi\"\n\"install\" = \"Instal\"\n\"clients\" = \"Klien\"\n\"usage\" = \"Penggunaan\"\n\"twoFactorCode\" = \"Kode\"\n\"remained\" = \"Tersisa\"\n\"security\" = \"Keamanan\"\n\"secAlertTitle\" = \"Peringatan keamanan\"\n\"secAlertSsl\" = \"Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data.\"\n\"secAlertConf\" = \"Beberapa pengaturan rentan terhadap serangan. Disarankan untuk memperkuat protokol keamanan guna mencegah pelanggaran potensial.\"\n\"secAlertSSL\" = \"Panel kekurangan koneksi yang aman. Harap instal sertifikat TLS untuk perlindungan data.\"\n\"secAlertPanelPort\" = \"Port default panel rentan. Harap konfigurasi port acak atau tertentu.\"\n\"secAlertPanelURI\" = \"Jalur URI default panel tidak aman. Harap konfigurasi jalur URI kompleks.\"\n\"secAlertSubURI\" = \"Jalur URI default langganan tidak aman. Harap konfigurasi jalur URI kompleks.\"\n\"secAlertSubJsonURI\" = \"Jalur URI default JSON langganan tidak aman. Harap konfigurasikan jalur URI kompleks.\"\n\"emptyDnsDesc\" = \"Tidak ada server DNS yang ditambahkan.\"\n\"emptyFakeDnsDesc\" = \"Tidak ada server Fake DNS yang ditambahkan.\"\n\"emptyBalancersDesc\" = \"Tidak ada penyeimbang yang ditambahkan.\"\n\"emptyReverseDesc\" = \"Tidak ada proxy terbalik yang ditambahkan.\"\n\"somethingWentWrong\" = \"Terjadi kesalahan\"\n\n[subscription]\n\"title\" = \"Info langganan\"\n\"subId\" = \"ID langganan\"\n\"status\" = \"Status\"\n\"downloaded\" = \"Diunduh\"\n\"uploaded\" = \"Diunggah\"\n\"expiry\" = \"Kedaluwarsa\"\n\"totalQuota\" = \"Kuota total\"\n\"individualLinks\" = \"Tautan individual\"\n\"active\" = \"Aktif\"\n\"inactive\" = \"Nonaktif\"\n\"unlimited\" = \"Tanpa batas\"\n\"noExpiry\" = \"Tanpa kedaluwarsa\"\n\n[menu]\n\"theme\" = \"Tema\"\n\"dark\" = \"Gelap\"\n\"ultraDark\" = \"Sangat Gelap\"\n\"dashboard\" = \"Ikhtisar\"\n\"inbounds\" = \"Masuk\"\n\"settings\" = \"Pengaturan Panel\"\n\"xray\" = \"Konfigurasi Xray\"\n\"logout\" = \"Keluar\"\n\"link\" = \"Kelola\"\n\n[pages.login]\n\"hello\" = \"Halo\"\n\"title\" = \"Selamat Datang\"\n\"loginAgain\" = \"Sesi Anda telah berakhir, harap masuk kembali\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"Format data input tidak valid.\"\n\"emptyUsername\" = \"Nama Pengguna diperlukan\"\n\"emptyPassword\" = \"Kata Sandi diperlukan\"\n\"wrongUsernameOrPassword\" = \"Username, kata sandi, atau kode dua faktor tidak valid.\"\n\"successLogin\" = \"Anda telah berhasil masuk ke akun Anda.\"\n\n[pages.index]\n\"title\" = \"Ikhtisar\"\n\"cpu\" = \"CPU\"\n\"logicalProcessors\" = \"Prosesor logis\"\n\"frequency\" = \"Frekuensi\"\n\"swap\" = \"Swap\"\n\"storage\" = \"Penyimpanan\"\n\"memory\" = \"RAM\"\n\"threads\" = \"Thread\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"Stop\"\n\"restartXray\" = \"Restart\"\n\"xraySwitch\" = \"Versi\"\n\"xraySwitchClick\" = \"Pilih versi yang ingin Anda pindah.\"\n\"xraySwitchClickDesk\" = \"Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini.\"\n\"xrayStatusUnknown\" = \"Tidak diketahui\"\n\"xrayStatusRunning\" = \"Berjalan\"\n\"xrayStatusStop\" = \"Berhenti\"\n\"xrayStatusError\" = \"Kesalahan\"\n\"xrayErrorPopoverTitle\" = \"Terjadi kesalahan saat menjalankan Xray\"\n\"operationHours\" = \"Waktu Aktif\"\n\"systemLoad\" = \"Beban Sistem\"\n\"systemLoadDesc\" = \"Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir\"\n\"connectionCount\" = \"Statistik Koneksi\"\n\"ipAddresses\" = \"Alamat IP\"\n\"toggleIpVisibility\" = \"Alihkan visibilitas IP\"\n\"overallSpeed\" = \"Kecepatan keseluruhan\"\n\"upload\" = \"Unggah\"\n\"download\" = \"Unduh\"\n\"totalData\" = \"Total data\"\n\"sent\" = \"Dikirim\"\n\"received\" = \"Diterima\"\n\"documentation\" = \"Dokumentasi\"\n\"xraySwitchVersionDialog\" = \"Apakah Anda yakin ingin mengubah versi Xray?\"\n\"xraySwitchVersionDialogDesc\" = \"Ini akan mengubah versi Xray ke #version#.\"\n\"xraySwitchVersionPopover\" = \"Xray berhasil diperbarui\"\n\"geofileUpdateDialog\" = \"Apakah Anda yakin ingin memperbarui geofile?\"\n\"geofileUpdateDialogDesc\" = \"Ini akan memperbarui file #filename#.\"\n\"geofilesUpdateDialogDesc\" = \"Ini akan memperbarui semua berkas.\"\n\"geofilesUpdateAll\" = \"Perbarui semua\"\n\"geofileUpdatePopover\" = \"Geofile berhasil diperbarui\"\n\"dontRefresh\" = \"Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini\"\n\"logs\" = \"Log\"\n\"config\" = \"Konfigurasi\"\n\"backup\" = \"Cadangan\"\n\"backupTitle\" = \"Cadangan & Pulihkan Database\"\n\"exportDatabase\" = \"Cadangkan\"\n\"exportDatabaseDesc\" = \"Klik untuk mengunduh file .db yang berisi cadangan dari database Anda saat ini ke perangkat Anda.\"\n\"importDatabase\" = \"Pulihkan\"\n\"importDatabaseDesc\" = \"Klik untuk memilih dan mengunggah file .db dari perangkat Anda untuk memulihkan database dari cadangan.\"\n\"importDatabaseSuccess\" = \"Database berhasil diimpor\"\n\"importDatabaseError\" = \"Terjadi kesalahan saat mengimpor database\"\n\"readDatabaseError\" = \"Terjadi kesalahan saat membaca database\"\n\"getDatabaseError\" = \"Terjadi kesalahan saat mengambil database\"\n\"getConfigError\" = \"Terjadi kesalahan saat mengambil file konfigurasi\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"Total Lalu Lintas\"\n\"allTimeTrafficUsage\" = \"Total Penggunaan Sepanjang Waktu\"\n\"title\" = \"Masuk\"\n\"totalDownUp\" = \"Total Terkirim/Diterima\"\n\"totalUsage\" = \"Penggunaan Total\"\n\"inboundCount\" = \"Total Masuk\"\n\"operate\" = \"Menu\"\n\"enable\" = \"Aktifkan\"\n\"remark\" = \"Catatan\"\n\"protocol\" = \"Protokol\"\n\"port\" = \"Port\"\n\"portMap\" = \"Port Mapping\"\n\"traffic\" = \"Traffic\"\n\"details\" = \"Rincian\"\n\"transportConfig\" = \"Transport\"\n\"expireDate\" = \"Durasi\"\n\"createdAt\" = \"Dibuat\"\n\"updatedAt\" = \"Diperbarui\"\n\"resetTraffic\" = \"Reset Traffic\"\n\"addInbound\" = \"Tambahkan Masuk\"\n\"generalActions\" = \"Tindakan Umum\"\n\"autoRefresh\" = \"Pembaruan otomatis\"\n\"autoRefreshInterval\" = \"Interval\"\n\"modifyInbound\" = \"Ubah Masuk\"\n\"deleteInbound\" = \"Hapus Masuk\"\n\"deleteInboundContent\" = \"Apakah Anda yakin ingin menghapus masuk?\"\n\"deleteClient\" = \"Hapus Klien\"\n\"deleteClientContent\" = \"Apakah Anda yakin ingin menghapus klien?\"\n\"resetTrafficContent\" = \"Apakah Anda yakin ingin mereset traffic?\"\n\"copyLink\" = \"Salin URL\"\n\"address\" = \"Alamat\"\n\"network\" = \"Jaringan\"\n\"destinationPort\" = \"Port Tujuan\"\n\"targetAddress\" = \"Alamat Target\"\n\"monitorDesc\" = \"Biarkan kosong untuk mendengarkan semua IP\"\n\"meansNoLimit\" = \"= Unlimited. (unit: GB)\"\n\"totalFlow\" = \"Total Aliran\"\n\"leaveBlankToNeverExpire\" = \"Biarkan kosong untuk tidak pernah kedaluwarsa\"\n\"noRecommendKeepDefault\" = \"Disarankan untuk tetap menggunakan pengaturan default\"\n\"certificatePath\" = \"Path Berkas\"\n\"certificateContent\" = \"Konten Berkas\"\n\"publicKey\" = \"Kunci Publik\"\n\"privatekey\" = \"Kunci Pribadi\"\n\"clickOnQRcode\" = \"Klik pada Kode QR untuk Menyalin\"\n\"client\" = \"Klien\"\n\"export\" = \"Ekspor Semua URL\"\n\"clone\" = \"Duplikat\"\n\"cloneInbound\" = \"Duplikat\"\n\"cloneInboundContent\" = \"Semua pengaturan masuk ini, kecuali Port, Listening IP, dan Klien, akan diterapkan pada duplikat.\"\n\"cloneInboundOk\" = \"Duplikat\"\n\"resetAllTraffic\" = \"Reset Semua Traffic Masuk\"\n\"resetAllTrafficTitle\" = \"Reset Semua Traffic Masuk\"\n\"resetAllTrafficContent\" = \"Apakah Anda yakin ingin mereset traffic semua masuk?\"\n\"resetInboundClientTraffics\" = \"Reset Traffic Klien Masuk\"\n\"resetInboundClientTrafficTitle\" = \"Reset Traffic Klien Masuk\"\n\"resetInboundClientTrafficContent\" = \"Apakah Anda yakin ingin mereset traffic klien masuk ini?\"\n\"resetAllClientTraffics\" = \"Reset Traffic Semua Klien\"\n\"resetAllClientTrafficTitle\" = \"Reset Traffic Semua Klien\"\n\"resetAllClientTrafficContent\" = \"Apakah Anda yakin ingin mereset traffic semua klien?\"\n\"delDepletedClients\" = \"Hapus Klien Habis\"\n\"delDepletedClientsTitle\" = \"Hapus Klien Habis\"\n\"delDepletedClientsContent\" = \"Apakah Anda yakin ingin menghapus semua klien yang habis?\"\n\"email\" = \"Email\"\n\"emailDesc\" = \"Harap berikan alamat email yang unik.\"\n\"IPLimit\" = \"Batas IP\"\n\"IPLimitDesc\" = \"Menonaktifkan masuk jika jumlah melebihi nilai yang ditetapkan. (0 = nonaktif)\"\n\"IPLimitlog\" = \"Log IP\"\n\"IPLimitlogDesc\" = \"Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)\"\n\"IPLimitlogclear\" = \"Hapus Log\"\n\"setDefaultCert\" = \"Atur Sertifikat dari Panel\"\n\"telegramDesc\" = \"Harap berikan ID Obrolan Telegram. (gunakan perintah '/id' di bot) atau (@userinfobot)\"\n\"subscriptionDesc\" = \"Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien.\"\n\"info\" = \"Info\"\n\"same\" = \"Sama\"\n\"inboundData\" = \"Data Masuk\"\n\"exportInbound\" = \"Ekspor Masuk\"\n\"import\" = \"Impor\"\n\"importInbound\" = \"Impor Masuk\"\n\"periodicTrafficResetTitle\" = \"Reset Trafik Berkala\"\n\"periodicTrafficResetDesc\" = \"Reset otomatis penghitung trafik pada interval tertentu\"\n\"lastReset\" = \"Reset Terakhir\"\n\n[pages.client]\n\"add\" = \"Tambah Klien\"\n\"edit\" = \"Edit Klien\"\n\"submitAdd\" = \"Tambah Klien\"\n\"submitEdit\" = \"Simpan Perubahan\"\n\"clientCount\" = \"Jumlah Klien\"\n\"bulk\" = \"Tambahkan Massal\"\n\"method\" = \"Metode\"\n\"first\" = \"Pertama\"\n\"last\" = \"Terakhir\"\n\"prefix\" = \"Awalan\"\n\"postfix\" = \"Akhiran\"\n\"delayedStart\" = \"Mulai Awal\"\n\"expireDays\" = \"Durasi\"\n\"days\" = \"Hari\"\n\"renew\" = \"Perpanjang Otomatis\"\n\"renewDesc\" = \"Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif)(unit: hari)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"Tidak Pernah\"\n\"daily\" = \"Harian\"\n\"weekly\" = \"Mingguan\"\n\"monthly\" = \"Bulanan\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"Dapatkan\"\n\"updateSuccess\" = \"Pembaruan berhasil\"\n\"logCleanSuccess\" = \"Log telah dibersihkan\"\n\"inboundsUpdateSuccess\" = \"Inbound berhasil diperbarui\"\n\"inboundUpdateSuccess\" = \"Inbound berhasil diperbarui\"\n\"inboundCreateSuccess\" = \"Inbound berhasil dibuat\"\n\"inboundDeleteSuccess\" = \"Inbound berhasil dihapus\"\n\"inboundClientAddSuccess\" = \"Klien inbound telah ditambahkan\"\n\"inboundClientDeleteSuccess\" = \"Klien inbound telah dihapus\"\n\"inboundClientUpdateSuccess\" = \"Klien inbound telah diperbarui\"\n\"delDepletedClientsSuccess\" = \"Semua klien yang habis telah dihapus\"\n\"resetAllClientTrafficSuccess\" = \"Semua lalu lintas klien telah direset\"\n\"resetAllTrafficSuccess\" = \"Semua lalu lintas telah direset\"\n\"resetInboundClientTrafficSuccess\" = \"Lalu lintas telah direset\"\n\"trafficGetError\" = \"Gagal mendapatkan data lalu lintas\"\n\"getNewX25519CertError\" = \"Terjadi kesalahan saat mendapatkan sertifikat X25519.\"\n\"getNewmldsa65Error\" = \"Terjadi kesalahan saat mendapatkan sertifikat mldsa65.\"\n\"getNewVlessEncError\" = \"Terjadi kesalahan saat mendapatkan sertifikat VlessEnc.\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"Permintaan\"\n\"response\" = \"Respons\"\n\"name\" = \"Nama\"\n\"value\" = \"Nilai\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"Versi\"\n\"method\" = \"Metode\"\n\"path\" = \"Path\"\n\"status\" = \"Status\"\n\"statusDescription\" = \"Deskripsi Status\"\n\"requestHeader\" = \"Header Permintaan\"\n\"responseHeader\" = \"Header Respons\"\n\n[pages.settings]\n\"title\" = \"Pengaturan Panel\"\n\"save\" = \"Simpan\"\n\"infoDesc\" = \"Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan.\"\n\"restartPanel\" = \"Restart Panel\"\n\"restartPanelDesc\" = \"Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server.\"\n\"restartPanelSuccess\" = \"Panel berhasil dimulai ulang\"\n\"actions\" = \"Tindakan\"\n\"resetDefaultConfig\" = \"Reset ke Default\"\n\"panelSettings\" = \"Umum\"\n\"securitySettings\" = \"Otentikasi\"\n\"TGBotSettings\" = \"Bot Telegram\"\n\"panelListeningIP\" = \"IP Pendengar\"\n\"panelListeningIPDesc\" = \"Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)\"\n\"panelListeningDomain\" = \"Domain Pendengar\"\n\"panelListeningDomainDesc\" = \"Nama domain untuk panel web. (biarkan kosong untuk mendengarkan semua domain dan IP)\"\n\"panelPort\" = \"Port Pendengar\"\n\"panelPortDesc\" = \"Nomor port untuk panel web. (harus menjadi port yang tidak digunakan)\"\n\"publicKeyPath\" = \"Path Kunci Publik\"\n\"publicKeyPathDesc\" = \"Path berkas kunci publik untuk panel web. (dimulai dengan ‘/‘)\"\n\"privateKeyPath\" = \"Path Kunci Privat\"\n\"privateKeyPathDesc\" = \"Path berkas kunci privat untuk panel web. (dimulai dengan ‘/‘)\"\n\"panelUrlPath\" = \"URI Path\"\n\"panelUrlPathDesc\" = \"URI path untuk panel web. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)\"\n\"pageSize\" = \"Ukuran Halaman\"\n\"pageSizeDesc\" = \"Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)\"\n\"remarkModel\" = \"Model Catatan & Karakter Pemisah\"\n\"datepicker\" = \"Jenis Kalender\"\n\"datepickerPlaceholder\" = \"Pilih tanggal\"\n\"datepickerDescription\" = \"Tugas terjadwal akan berjalan berdasarkan kalender ini.\"\n\"sampleRemark\" = \"Contoh Catatan\"\n\"oldUsername\" = \"Username Saat Ini\"\n\"currentPassword\" = \"Kata Sandi Saat Ini\"\n\"newUsername\" = \"Username Baru\"\n\"newPassword\" = \"Kata Sandi Baru\"\n\"telegramBotEnable\" = \"Aktifkan Bot Telegram\"\n\"telegramBotEnableDesc\" = \"Mengaktifkan bot Telegram.\"\n\"telegramToken\" = \"Token Telegram\"\n\"telegramTokenDesc\" = \"Token bot Telegram yang diperoleh dari '@BotFather'.\"\n\"telegramProxy\" = \"Proxy SOCKS\"\n\"telegramProxyDesc\" = \"Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)\"\n\"telegramAPIServer\" = \"Telegram API Server\"\n\"telegramAPIServerDesc\" = \"Server API Telegram yang akan digunakan. Biarkan kosong untuk menggunakan server default.\"\n\"telegramChatId\" = \"ID Obrolan Admin\"\n\"telegramChatIdDesc\" = \"ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)\"\n\"telegramNotifyTime\" = \"Waktu Notifikasi\"\n\"telegramNotifyTimeDesc\" = \"Waktu notifikasi bot Telegram yang diatur untuk laporan berkala. (gunakan format waktu crontab)\"\n\"tgNotifyBackup\" = \"Cadangan Database\"\n\"tgNotifyBackupDesc\" = \"Kirim berkas cadangan database dengan laporan.\"\n\"tgNotifyLogin\" = \"Notifikasi Login\"\n\"tgNotifyLoginDesc\" = \"Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda.\"\n\"sessionMaxAge\" = \"Durasi Sesi\"\n\"sessionMaxAgeDesc\" = \"Durasi di mana Anda dapat tetap masuk. (unit: menit)\"\n\"expireTimeDiff\" = \"Notifikasi Tanggal Kedaluwarsa\"\n\"expireTimeDiffDesc\" = \"Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)\"\n\"trafficDiff\" = \"Notifikasi Batas Traffic\"\n\"trafficDiffDesc\" = \"Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)\"\n\"tgNotifyCpu\" = \"Notifikasi Beban CPU\"\n\"tgNotifyCpuDesc\" = \"Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)\"\n\"timeZone\" = \"Zone Waktu\"\n\"timeZoneDesc\" = \"Tugas terjadwal akan berjalan berdasarkan zona waktu ini.\"\n\"subSettings\" = \"Langganan\"\n\"subEnable\" = \"Aktifkan Layanan Langganan\"\n\"subEnableDesc\" = \"Mengaktifkan layanan langganan.\"\n\"subJsonEnable\" = \"Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri.\"\n\"subTitle\" = \"Judul Langganan\"\n\"subTitleDesc\" = \"Judul yang ditampilkan di klien VPN\"\n\"subSupportUrl\" = \"URL Dukungan\"\n\"subSupportUrlDesc\" = \"Tautan dukungan teknis yang ditampilkan di klien VPN\"\n\"subProfileUrl\" = \"URL Profil\"\n\"subProfileUrlDesc\" = \"Tautan ke situs web Anda yang ditampilkan di klien VPN\"\n\"subAnnounce\" = \"Pengumuman\"\n\"subAnnounceDesc\" = \"Teks pengumuman yang ditampilkan di klien VPN\"\n\"subEnableRouting\" = \"Aktifkan perutean\"\n\"subEnableRoutingDesc\" = \"Pengaturan global untuk mengaktifkan perutean (routing) di klien VPN. (Hanya untuk Happ)\"\n\"subRoutingRules\" = \"Aturan routing\"\n\"subRoutingRulesDesc\" = \"Aturan routing global untuk klien VPN. (Hanya untuk Happ)\"\n\"subListen\" = \"IP Pendengar\"\n\"subListenDesc\" = \"Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)\"\n\"subPort\" = \"Port Pendengar\"\n\"subPortDesc\" = \"Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)\"\n\"subCertPath\" = \"Path Kunci Publik\"\n\"subCertPathDesc\" = \"Path berkas kunci publik untuk layanan langganan. (dimulai dengan ‘/‘)\"\n\"subKeyPath\" = \"Path Kunci Privat\"\n\"subKeyPathDesc\" = \"Path berkas kunci privat untuk layanan langganan. (dimulai dengan ‘/‘)\"\n\"subPath\" = \"URI Path\"\n\"subPathDesc\" = \"URI path untuk layanan langganan. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)\"\n\"subDomain\" = \"Domain Pendengar\"\n\"subDomainDesc\" = \"Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)\"\n\"subUpdates\" = \"Interval Pembaruan\"\n\"subUpdatesDesc\" = \"Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)\"\n\"subEncrypt\" = \"Encode\"\n\"subEncryptDesc\" = \"Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64.\"\n\"subShowInfo\" = \"Tampilkan Info Penggunaan\"\n\"subShowInfoDesc\" = \"Sisa traffic dan tanggal akan ditampilkan di aplikasi klien.\"\n\"subURI\" = \"URI Proxy Terbalik\"\n\"subURIDesc\" = \"Path URI dari URL langganan untuk digunakan di belakang proxy.\"\n\"externalTrafficInformEnable\" = \"Informasikan API eksternal pada setiap pembaruan lalu lintas.\"\n\"externalTrafficInformEnableDesc\" = \"Inform external API on every traffic update.\"\n\"externalTrafficInformURI\" = \"Lalu Lintas Eksternal Menginformasikan URI\"\n\"externalTrafficInformURIDesc\" = \"Pembaruan lalu lintas dikirim ke URI ini.\"\n\"fragment\" = \"Fragmentasi\"\n\"fragmentDesc\" = \"Aktifkan fragmentasi untuk paket hello TLS\"\n\"fragmentSett\" = \"Pengaturan Fragmentasi\"\n\"noisesDesc\" = \"Aktifkan Noises.\"\n\"noisesSett\" = \"Pengaturan Noises\"\n\"mux\" = \"Mux\"\n\"muxDesc\" = \"Mengirimkan beberapa aliran data independen dalam aliran data yang sudah ada.\"\n\"muxSett\" = \"Pengaturan Mux\"\n\"direct\" = \"Koneksi langsung\"\n\"directDesc\" = \"Secara langsung membuat koneksi dengan domain atau rentang IP negara tertentu.\"\n\"notifications\" = \"Notifikasi\"\n\"certs\" = \"Sertifikat\"\n\"externalTraffic\" = \"Lalu Lintas Eksternal\"\n\"dateAndTime\" = \"Tanggal dan Waktu\"\n\"proxyAndServer\" = \"Proxy dan Server\"\n\"intervals\" = \"Interval\"\n\"information\" = \"Informasi\"\n\"language\" = \"Bahasa\"\n\"telegramBotLanguage\" = \"Bahasa Bot Telegram\"\n\n[pages.xray]\n\"title\" = \"Konfigurasi Xray\"\n\"save\" = \"Simpan\"\n\"restart\" = \"Restart Xray\"\n\"restartSuccess\" = \"Xray berhasil diluncurkan ulang\"\n\"stopSuccess\" = \"Xray telah berhasil dihentikan\"\n\"restartError\" = \"Terjadi kesalahan saat memulai ulang Xray.\"\n\"stopError\" = \"Terjadi kesalahan saat menghentikan Xray.\"\n\"basicTemplate\" = \"Dasar\"\n\"advancedTemplate\" = \"Lanjutan\"\n\"generalConfigs\" = \"Strategi Umum\"\n\"generalConfigsDesc\" = \"Opsi ini akan menentukan penyesuaian strategi umum.\"\n\"logConfigs\" = \"Catatan\"\n\"logConfigsDesc\" = \"Log dapat mempengaruhi efisiensi server Anda. Disarankan untuk mengaktifkannya dengan bijak hanya jika diperlukan\"\n\"blockConfigsDesc\" = \"Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta.\"\n\"basicRouting\" = \"Perutean Dasar\"\n\"blockConnectionsConfigsDesc\" = \"Opsi ini akan memblokir lalu lintas berdasarkan negara yang diminta.\"\n\"directConnectionsConfigsDesc\" = \"Koneksi langsung memastikan bahwa lalu lintas tertentu tidak dialihkan melalui server lain.\"\n\"blockips\" = \"Blokir IP\"\n\"blockdomains\" = \"Blokir Domain\"\n\"directips\" = \"IP Langsung\"\n\"directdomains\" = \"Domain Langsung\"\n\"ipv4Routing\" = \"Perutean IPv4\"\n\"ipv4RoutingDesc\" = \"Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4.\"\n\"warpRouting\" = \"Perutean WARP\"\n\"warpRoutingDesc\" = \"Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui WARP.\"\n\"Template\" = \"Template Konfigurasi Xray Lanjutan\"\n\"TemplateDesc\" = \"File konfigurasi Xray akhir akan dibuat berdasarkan template ini.\"\n\"FreedomStrategy\" = \"Strategi Protokol Freedom\"\n\"FreedomStrategyDesc\" = \"Atur strategi output untuk jaringan dalam Protokol Freedom.\"\n\"RoutingStrategy\" = \"Strategi Pengalihan Keseluruhan\"\n\"RoutingStrategyDesc\" = \"Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan.\"\n\"outboundTestUrl\" = \"URL tes outbound\"\n\"outboundTestUrlDesc\" = \"URL yang digunakan saat menguji konektivitas outbound\"\n\"Torrent\" = \"Blokir Protokol BitTorrent\"\n\"Inbounds\" = \"Masuk\"\n\"InboundsDesc\" = \"Menerima klien tertentu.\"\n\"Outbounds\" = \"Keluar\"\n\"Balancers\" = \"Penyeimbang\"\n\"OutboundsDesc\" = \"Atur jalur lalu lintas keluar.\"\n\"Routings\" = \"Aturan Pengalihan\"\n\"RoutingsDesc\" = \"Prioritas setiap aturan penting!\"\n\"completeTemplate\" = \"Semua\"\n\"logLevel\" = \"Tingkat Log\"\n\"logLevelDesc\" = \"Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat.\"\n\"accessLog\" = \"Log Akses\"\n\"accessLogDesc\" = \"Jalur file untuk log akses. Nilai khusus 'tidak ada' menonaktifkan log akses\"\n\"errorLog\" = \"Catatan eror\"\n\"errorLogDesc\" = \"Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan\"\n\"dnsLog\" = \"Log DNS\"\n\"dnsLogDesc\" = \"Apakah akan mengaktifkan log kueri DNS\"\n\"maskAddress\" = \"Alamat Masker\"\n\"maskAddressDesc\" = \"Masker alamat IP, ketika diaktifkan, akan secara otomatis mengganti alamat IP yang muncul di log.\"\n\"statistics\" = \"Statistik\"\n\"statsInboundUplink\" = \"Statistik Unggah Masuk\"\n\"statsInboundUplinkDesc\" = \"Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy masuk.\"\n\"statsInboundDownlink\" = \"Statistik Unduh Masuk\"\n\"statsInboundDownlinkDesc\" = \"Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy masuk.\"\n\"statsOutboundUplink\" = \"Statistik Unggah Keluar\"\n\"statsOutboundUplinkDesc\" = \"Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar.\"\n\"statsOutboundDownlink\" = \"Statistik Unduh Keluar\"\n\"statsOutboundDownlinkDesc\" = \"Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy keluar.\"\n\n[pages.xray.rules]\n\"first\" = \"Pertama\"\n\"last\" = \"Terakhir\"\n\"up\" = \"Naik\"\n\"down\" = \"Turun\"\n\"source\" = \"Sumber\"\n\"dest\" = \"Tujuan\"\n\"inbound\" = \"Masuk\"\n\"outbound\" = \"Keluar\"\n\"balancer\" = \"Pengimbang\"\n\"info\" = \"Info\"\n\"add\" = \"Tambahkan Aturan\"\n\"edit\" = \"Edit Aturan\"\n\"useComma\" = \"Item yang dipisahkan koma\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"Tambahkan Keluar\"\n\"addReverse\" = \"Tambahkan Revers\"\n\"editOutbound\" = \"Edit Keluar\"\n\"editReverse\" = \"Edit Revers\"\n\"tag\" = \"Tag\"\n\"tagDesc\" = \"Tag Unik\"\n\"address\" = \"Alamat\"\n\"reverse\" = \"Revers\"\n\"domain\" = \"Domain\"\n\"type\" = \"Tipe\"\n\"bridge\" = \"Jembatan\"\n\"portal\" = \"Portal\"\n\"link\" = \"Tautan\"\n\"intercon\" = \"Interkoneksi\"\n\"settings\" = \"Pengaturan\"\n\"accountInfo\" = \"Informasi Akun\"\n\"outboundStatus\" = \"Status Keluar\"\n\"sendThrough\" = \"Kirim Melalui\"\n\"test\" = \"Tes\"\n\"testResult\" = \"Hasil Tes\"\n\"testing\" = \"Menguji koneksi...\"\n\"testSuccess\" = \"Tes berhasil\"\n\"testFailed\" = \"Tes gagal\"\n\"testError\" = \"Gagal menguji outbound\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"Tambahkan Penyeimbang\"\n\"editBalancer\" = \"Sunting Penyeimbang\"\n\"balancerStrategy\" = \"Strategi\"\n\"balancerSelectors\" = \"Penyeleksi\"\n\"tag\" = \"Menandai\"\n\"tagDesc\" = \"Label Unik\"\n\"balancerDesc\" = \"BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi.\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"Kunci Rahasia\"\n\"publicKey\" = \"Kunci Publik\"\n\"allowedIPs\" = \"IP yang Diizinkan\"\n\"endpoint\" = \"Titik Akhir\"\n\"psk\" = \"Kunci Pra-Bagi\"\n\"domainStrategy\" = \"Strategi Domain\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"Nama antarmuka TUN. Standar adalah 'xray0'\"\n\"mtuDesc\" = \"Unit Transmisi Maksimum. Ukuran maksimum paket data. Standar adalah 1500\"\n\"userLevel\" = \"Level Pengguna\"\n\"userLevelDesc\" = \"Semua koneksi yang dibuat melalui inbound ini akan menggunakan level pengguna ini. Standar adalah 0\"\n\n[pages.xray.dns]\n\"enable\" = \"Aktifkan DNS\"\n\"enableDesc\" = \"Aktifkan server DNS bawaan\"\n\"tag\" = \"Tanda DNS Masuk\"\n\"tagDesc\" = \"Tanda ini akan tersedia sebagai tanda masuk dalam aturan penataan.\"\n\"clientIp\" = \"IP Klien\"\n\"clientIpDesc\" = \"Digunakan untuk memberi tahu server tentang lokasi IP yang ditentukan selama kueri DNS\"\n\"disableCache\" = \"Nonaktifkan cache\"\n\"disableCacheDesc\" = \"Menonaktifkan caching DNS\"\n\"disableFallback\" = \"Nonaktifkan Fallback\"\n\"disableFallbackDesc\" = \"Menonaktifkan kueri DNS fallback\"\n\"disableFallbackIfMatch\" = \"Nonaktifkan Fallback Jika Cocok\"\n\"disableFallbackIfMatchDesc\" = \"Menonaktifkan kueri DNS fallback ketika daftar domain yang cocok dari server DNS terpenuhi\"\n\"enableParallelQuery\" = \"Aktifkan Kueri Paralel\"\n\"enableParallelQueryDesc\" = \"Aktifkan kueri DNS paralel ke beberapa server untuk resolusi yang lebih cepat\"\n\"strategy\" = \"Strategi Kueri\"\n\"strategyDesc\" = \"Strategi keseluruhan untuk menyelesaikan nama domain\"\n\"add\" = \"Tambahkan Server\"\n\"edit\" = \"Sunting Server\"\n\"domains\" = \"Domains\"\n\"expectIPs\" = \"IP yang Diharapkan\"\n\"unexpectIPs\" = \"IP tak terduga\"\n\"useSystemHosts\" = \"Gunakan Hosts Sistem\"\n\"useSystemHostsDesc\" = \"Gunakan file hosts dari sistem yang terinstal\"\n\"usePreset\" = \"Gunakan templat\"\n\"dnsPresetTitle\" = \"Templat DNS\"\n\"dnsPresetFamily\" = \"Keluarga\"\n\n[pages.xray.fakedns]\n\"add\" = \"Tambahkan DNS Palsu\"\n\"edit\" = \"Edit DNS Palsu\"\n\"ipPool\" = \"Subnet Kumpulan IP\"\n\"poolSize\" = \"Ukuran Kolam\"\n\n[pages.settings.security]\n\"admin\" = \"Kredensial admin\"\n\"twoFactor\" = \"Autentikasi dua faktor\"\n\"twoFactorEnable\" = \"Aktifkan 2FA\"\n\"twoFactorEnableDesc\" = \"Menambahkan lapisan autentikasi tambahan untuk keamanan lebih.\"\n\"twoFactorModalSetTitle\" = \"Aktifkan autentikasi dua faktor\"\n\"twoFactorModalDeleteTitle\" = \"Nonaktifkan autentikasi dua faktor\"\n\"twoFactorModalSteps\" = \"Untuk menyiapkan autentikasi dua faktor, lakukan beberapa langkah:\"\n\"twoFactorModalFirstStep\" = \"1. Pindai kode QR ini di aplikasi autentikasi atau salin token di dekat kode QR dan tempelkan ke aplikasi\"\n\"twoFactorModalSecondStep\" = \"2. Masukkan kode dari aplikasi\"\n\"twoFactorModalRemoveStep\" = \"Masukkan kode dari aplikasi untuk menghapus autentikasi dua faktor.\"\n\"twoFactorModalChangeCredentialsTitle\" = \"Ubah kredensial\"\n\"twoFactorModalChangeCredentialsStep\" = \"Masukkan kode dari aplikasi untuk mengubah kredensial administrator.\"\n\"twoFactorModalSetSuccess\" = \"Autentikasi dua faktor telah berhasil dibuat\"\n\"twoFactorModalDeleteSuccess\" = \"Autentikasi dua faktor telah berhasil dihapus\"\n\"twoFactorModalError\" = \"Kode salah\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"Parameter telah diubah.\"\n\"getSettings\" = \"Terjadi kesalahan saat mengambil parameter.\"\n\"modifyUserError\" = \"Terjadi kesalahan saat mengubah kredensial administrator.\"\n\"modifyUser\" = \"Anda telah berhasil mengubah kredensial administrator.\"\n\"originalUserPassIncorrect\" = \"Username atau password saat ini tidak valid\"\n\"userPassMustBeNotEmpty\" = \"Username dan password baru tidak boleh kosong\"\n\"getOutboundTrafficError\" = \"Gagal mendapatkan lalu lintas keluar\"\n\"resetOutboundTrafficError\" = \"Gagal mereset lalu lintas keluar\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ Keyboard ditutup!\"\n\"noResult\" = \"❗ Tidak ada hasil!\"\n\"noQuery\" = \"❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!\"\n\"wentWrong\" = \"❌ Terjadi kesalahan!\"\n\"noIpRecord\" = \"❗ Tidak ada Catatan IP!\"\n\"noInbounds\" = \"❗ Tidak ada inbound yang ditemukan!\"\n\"unlimited\" = \"♾ Tidak terbatas (Reset)\"\n\"add\" = \"Tambah\"\n\"month\" = \"Bulan\"\n\"months\" = \"Bulan\"\n\"day\" = \"Hari\"\n\"days\" = \"Hari\"\n\"hours\" = \"Jam\"\n\"minutes\" = \"Menit\"\n\"unknown\" = \"Tidak diketahui\"\n\"inbounds\" = \"Inbound\"\n\"clients\" = \"Klien\"\n\"offline\" = \"🔴 Offline\"\n\"online\" = \"🟢 Online\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ Perintah tidak dikenal.\"\n\"pleaseChoose\" = \"👇 Harap pilih:\\r\\n\"\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\"\n\"start\" = \"👋 Halo <i>{{ .Firstname }}</i>.\\r\\n\"\n\"welcome\" = \"🤖 Selamat datang di <b>{{.Hostname }}</b> bot managemen.\\r\\n\"\n\"status\" = \"✅ Bot dalam keadaan baik!\"\n\"usage\" = \"❗ Harap berikan teks untuk mencari!\"\n\"getID\" = \"🆔 ID Anda: <code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"Untuk memulai ulang Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\nUntuk mencari email klien:\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nUntuk mencari inbound (dengan statistik klien):\\r\\n<code>/inbound [Catatan]</code>\\r\\n\\r\\nID Obrolan Telegram:\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"Untuk mencari statistik, gunakan perintah berikut:\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nID Obrolan Telegram:\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ Operasi berhasil!\"\n\"restartFailed\" = \"❗ Kesalahan dalam operasi.\\r\\n\\r\\n<code>Error: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core tidak berjalan.\"\n\"startDesc\" = \"Tampilkan menu utama\"\n\"helpDesc\" = \"Bantuan bot\"\n\"statusDesc\" = \"Periksa status bot\"\n\"idDesc\" = \"Tampilkan ID Telegram Anda\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%\"\n\"selectUserFailed\" = \"❌ Kesalahan dalam pemilihan pengguna!\"\n\"userSaved\" = \"✅ Pengguna Telegram tersimpan.\"\n\"loginSuccess\" = \"✅ Berhasil masuk ke panel.\\r\\n\"\n\"loginFailed\" = \"❗️ Gagal masuk ke panel.\\r\\n\"\n\"2faFailed\" = \"2FA Gagal\"\n\"report\" = \"🕰 Laporan Terjadwal: {{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ Tanggal & Waktu: {{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 Host: {{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 Versi 3X-UI: {{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Versi Xray: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IP:\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 RAM: {{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP: {{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP: {{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Status: {{ .State }}\\r\\n\"\n\"username\" = \"👤 Nama Pengguna: {{ .Username }}\\r\\n\"\n\"password\" = \"👤 Kata Sandi: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ Waktu: {{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 Inbound: {{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 Port: {{ .Port }}\\r\\n\"\n\"expire\" = \"📅 Tanggal Kadaluarsa: {{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 Kadaluarsa Dalam: {{ .Time }}\\r\\n\"\n\"active\" = \"💡 Aktif: {{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 Diaktifkan: {{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 Status Koneksi: {{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 Terakhir online: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 Email: {{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 Unggah: ↑{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 Unduh: ↓{{ .Download }}\\r\\n\"\n\"total\" = \"📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 Pengguna Telegram: {{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 Habis {{ .Type }}:\\r\\n\"\n\"exhaustedCount\" = \"🚨 Jumlah Habis {{ .Type }}:\\r\\n\"\n\"onlinesCount\" = \"🌐 Klien Online: {{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 Dinonaktifkan: {{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 Habis Sebentar: {{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 Waktu Backup: {{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 Diperbarui Pada: {{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ Ya\"\n\"no\" = \"❌ Tidak\"\n\"received_id\" = \"🔑📥 ID diperbarui.\"\n\"received_password\" = \"🔑📥 Kata sandi diperbarui.\"\n\"received_email\" = \"📧📥 Email diperbarui.\"\n\"received_comment\" = \"💬📥 Komentar diperbarui.\"\n\"id_prompt\" = \"🔑 ID Default: {{ .ClientId }}\\n\\nMasukkan ID Anda.\"\n\"pass_prompt\" = \"🔑 Kata Sandi Default: {{ .ClientPassword }}\\n\\nMasukkan kata sandi Anda.\"\n\"email_prompt\" = \"📧 Email Default: {{ .ClientEmail }}\\n\\nMasukkan email Anda.\"\n\"comment_prompt\" = \"💬 Komentar Default: {{ .ClientComment }}\\n\\nMasukkan komentar Anda.\"\n\"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!\"\n\"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!\"\n\"cancel\" = \"❌ Proses Dibatalkan! \\n\\nAnda dapat /start lagi kapan saja. 🔄\"\n\"error_add_client\" = \"⚠️ Kesalahan:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"Oke, saya akan tetap menggunakan nilai default. 😊\"\n\"incorrect_input\" = \"Masukan Anda tidak valid.\\nFrasa harus berlanjut tanpa spasi.\\nContoh benar: aaaaaa\\nContoh salah: aaa aaa 🚫\"\n\"AreYouSure\" = \"Apakah kamu yakin? 🤔\"\n\"SuccessResetTraffic\" = \"📧 Email: {{ .ClientEmail }}\\n🏁 Hasil: ✅ Berhasil\"\n\"FailedResetTraffic\" = \"📧 Email: {{ .ClientEmail }}\\n🏁 Hasil: ❌ Gagal \\n\\n🛠️ Kesalahan: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 Proses reset traffic selesai untuk semua klien.\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ Tutup Papan Ketik\"\n\"cancel\" = \"❌ Batal\"\n\"cancelReset\" = \"❌ Batal Reset\"\n\"cancelIpLimit\" = \"❌ Batal Batas IP\"\n\"confirmResetTraffic\" = \"✅ Konfirmasi Reset Lalu Lintas?\"\n\"confirmClearIps\" = \"✅ Konfirmasi Hapus IPs?\"\n\"confirmRemoveTGUser\" = \"✅ Konfirmasi Hapus Pengguna Telegram?\"\n\"confirmToggle\" = \"✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?\"\n\"dbBackup\" = \"Dapatkan Cadangan DB\"\n\"serverUsage\" = \"Penggunaan Server\"\n\"getInbounds\" = \"Dapatkan Inbounds\"\n\"depleteSoon\" = \"Habis Sebentar\"\n\"clientUsage\" = \"Dapatkan Penggunaan\"\n\"onlines\" = \"Klien Online\"\n\"commands\" = \"Perintah\"\n\"refresh\" = \"🔄 Perbarui\"\n\"clearIPs\" = \"❌ Hapus IPs\"\n\"removeTGUser\" = \"❌ Hapus Pengguna Telegram\"\n\"selectTGUser\" = \"👤 Pilih Pengguna Telegram\"\n\"selectOneTGUser\" = \"👤 Pilih Pengguna Telegram:\"\n\"resetTraffic\" = \"📈 Reset Lalu Lintas\"\n\"resetExpire\" = \"📅 Ubah Tanggal Kadaluarsa\"\n\"ipLog\" = \"🔢 Log IP\"\n\"ipLimit\" = \"🔢 Batas IP\"\n\"setTGUser\" = \"👤 Set Pengguna Telegram\"\n\"toggle\" = \"🔘 Aktifkan / Nonaktifkan\"\n\"custom\" = \"🔢 Kustom\"\n\"confirmNumber\" = \"✅ Konfirmasi: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ Konfirmasi menambahkan: {{ .Num }}\"\n\"limitTraffic\" = \"🚧 Batas Lalu Lintas\"\n\"getBanLogs\" = \"Dapatkan Log Pemblokiran\"\n\"allClients\" = \"Semua Klien\"\n\"addClient\" = \"Tambah Klien\"\n\"submitDisable\" = \"Kirim Sebagai Nonaktif ☑️\"\n\"submitEnable\" = \"Kirim Sebagai Aktif ✅\"\n\"use_default\" = \"🏷️ Gunakan Default\"\n\"change_id\" = \"⚙️🔑 ID\"\n\"change_password\" = \"⚙️🔑 Kata Sandi\"\n\"change_email\" = \"⚙️📧 Email\"\n\"change_comment\" = \"⚙️💬 Komentar\"\n\"ResetAllTraffics\" = \"Reset Semua Lalu Lintas\"\n\"SortedTrafficUsageReport\" = \"Laporan Penggunaan Lalu Lintas yang Terurut\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ Operasi berhasil!\"\n\"errorOperation\" = \"❗ Kesalahan dalam operasi.\"\n\"getInboundsFailed\" = \"❌ Gagal mendapatkan inbounds.\"\n\"getClientsFailed\" = \"❌ Gagal mendapatkan klien.\"\n\"canceled\" = \"❌ {{ .Email }}: Operasi dibatalkan.\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}: Klien diperbarui dengan berhasil.\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}: IP diperbarui dengan berhasil.\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil.\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}: Lalu lintas direset dengan berhasil.\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil.\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil.\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil.\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}: IP dihapus dengan berhasil.\"\n\"getIpLog\" = \"✅ {{ .Email }}: Dapatkan Log IP.\"\n\"getUserInfo\" = \"✅ {{ .Email }}: Dapatkan Info Pengguna Telegram.\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil.\"\n\"enableSuccess\" = \"✅ {{ .Email }}: Diaktifkan dengan berhasil.\"\n\"disableSuccess\" = \"✅ {{ .Email }}: Dinonaktifkan dengan berhasil.\"\n\"askToAddUserId\" = \"Konfigurasi Anda tidak ditemukan!\\r\\nSilakan minta admin Anda untuk menggunakan ChatID Telegram Anda dalam konfigurasi Anda.\\r\\n\\r\\nChatID Pengguna Anda: <code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"Pilih Klien untuk Inbound {{ .Inbound }}\"\n\"chooseInbound\" = \"Pilih Inbound\"\n"
  },
  {
    "path": "web/translation/translate.ja_JP.toml",
    "content": "\"username\" = \"ユーザー名\"\n\"password\" = \"パスワード\"\n\"login\" = \"ログイン\"\n\"confirm\" = \"確認\"\n\"cancel\" = \"キャンセル\"\n\"close\" = \"閉じる\"\n\"create\" = \"作成\"\n\"update\" = \"更新\"\n\"copy\" = \"コピー\"\n\"copied\" = \"コピー済み\"\n\"download\" = \"ダウンロード\"\n\"remark\" = \"備考\"\n\"enable\" = \"有効化\"\n\"protocol\" = \"プロトコル\"\n\"search\" = \"検索\"\n\"filter\" = \"フィルター\"\n\"loading\" = \"読み込み中...\"\n\"second\" = \"秒\"\n\"minute\" = \"分\"\n\"hour\" = \"時間\"\n\"day\" = \"日\"\n\"check\" = \"確認\"\n\"indefinite\" = \"無期限\"\n\"unlimited\" = \"無制限\"\n\"none\" = \"なし\"\n\"qrCode\" = \"QRコード\"\n\"info\" = \"詳細情報\"\n\"edit\" = \"編集\"\n\"delete\" = \"削除\"\n\"reset\" = \"リセット\"\n\"noData\" = \"データなし。\"\n\"copySuccess\" = \"コピー成功\"\n\"sure\" = \"確定\"\n\"encryption\" = \"暗号化\"\n\"useIPv4ForHost\" = \"ホストにIPv4を使用\"\n\"transmission\" = \"伝送\"\n\"host\" = \"ホスト\"\n\"path\" = \"パス\"\n\"camouflage\" = \"偽装\"\n\"status\" = \"ステータス\"\n\"enabled\" = \"有効\"\n\"disabled\" = \"無効\"\n\"depleted\" = \"消耗済み\"\n\"depletingSoon\" = \"間もなく消耗\"\n\"offline\" = \"オフライン\"\n\"online\" = \"オンライン\"\n\"domainName\" = \"ドメイン名\"\n\"monitor\" = \"監視\"\n\"certificate\" = \"証明書\"\n\"fail\" = \"失敗\"\n\"comment\" = \"コメント\"\n\"success\" = \"成功\"\n\"lastOnline\" = \"最終オンライン\"\n\"getVersion\" = \"バージョン取得\"\n\"install\" = \"インストール\"\n\"clients\" = \"クライアント\"\n\"usage\" = \"利用状況\"\n\"twoFactorCode\" = \"コード\"\n\"remained\" = \"残り\"\n\"security\" = \"セキュリティ\"\n\"secAlertTitle\" = \"セキュリティアラート\"\n\"secAlertSsl\" = \"この接続は安全ではありません。TLSを有効にしてデータ保護を行うまで、機密情報を入力しないでください。\"\n\"secAlertConf\" = \"一部の設定は脆弱です。潜在的な脆弱性を防ぐために、セキュリティプロトコルを強化することをお勧めします。\"\n\"secAlertSSL\" = \"セキュアな接続がありません。データ保護のためにTLS証明書をインストールしてください。\"\n\"secAlertPanelPort\" = \"デフォルトのポートにはセキュリティリスクがあります。ランダムなポートまたは特定のポートを設定してください。\"\n\"secAlertPanelURI\" = \"デフォルトのURIパスは安全ではありません。複雑なURIパスを設定してください。\"\n\"secAlertSubURI\" = \"サブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。\"\n\"secAlertSubJsonURI\" = \"JSONサブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。\"\n\"emptyDnsDesc\" = \"追加されたDNSサーバーはありません。\"\n\"emptyFakeDnsDesc\" = \"追加されたFake DNSサーバーはありません。\"\n\"emptyBalancersDesc\" = \"追加されたバランサーはありません。\"\n\"emptyReverseDesc\" = \"追加されたリバースプロキシはありません。\"\n\"somethingWentWrong\" = \"エラーが発生しました\"\n\n[subscription]\n\"title\" = \"サブスクリプション情報\"\n\"subId\" = \"サブスクリプションID\"\n\"status\" = \"ステータス\"\n\"downloaded\" = \"ダウンロード\"\n\"uploaded\" = \"アップロード\"\n\"expiry\" = \"有効期限\"\n\"totalQuota\" = \"合計クォータ\"\n\"individualLinks\" = \"個別リンク\"\n\"active\" = \"有効\"\n\"inactive\" = \"無効\"\n\"unlimited\" = \"無制限\"\n\"noExpiry\" = \"期限なし\"\n\n[menu]\n\"theme\" = \"テーマ\"\n\"dark\" = \"ダーク\"\n\"ultraDark\" = \"ウルトラダーク\"\n\"dashboard\" = \"ダッシュボード\"\n\"inbounds\" = \"インバウンド一覧\"\n\"settings\" = \"パネル設定\"\n\"xray\" = \"Xray設定\"\n\"logout\" = \"ログアウト\"\n\"link\" = \"リンク管理\"\n\n[pages.login]\n\"hello\" = \"こんにちは\"\n\"title\" = \"ようこそ\"\n\"loginAgain\" = \"ログインセッションが切れました。再度ログインしてください。\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"データ形式エラー\"\n\"emptyUsername\" = \"ユーザー名を入力してください\"\n\"emptyPassword\" = \"パスワードを入力してください\"\n\"wrongUsernameOrPassword\" = \"ユーザー名、パスワード、または二段階認証コードが無効です。\"\n\"successLogin\" = \"アカウントに正常にログインしました。\"\n\n[pages.index]\n\"title\" = \"システムステータス\"\n\"cpu\" = \"CPU\"\n\"logicalProcessors\" = \"論理プロセッサ\"\n\"frequency\" = \"周波数\"\n\"swap\" = \"スワップ\"\n\"storage\" = \"ストレージ\"\n\"memory\" = \"RAM\"\n\"threads\" = \"スレッド\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"停止\"\n\"restartXray\" = \"再起動\"\n\"xraySwitch\" = \"バージョン\"\n\"xraySwitchClick\" = \"切り替えるバージョンを選択してください\"\n\"xraySwitchClickDesk\" = \"慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。\"\n\"xrayStatusUnknown\" = \"不明\"\n\"xrayStatusRunning\" = \"実行中\"\n\"xrayStatusStop\" = \"停止\"\n\"xrayStatusError\" = \"エラー\"\n\"xrayErrorPopoverTitle\" = \"Xrayの実行中にエラーが発生しました\"\n\"operationHours\" = \"システム稼働時間\"\n\"systemLoad\" = \"システム負荷\"\n\"systemLoadDesc\" = \"過去1、5、15分間のシステム平均負荷\"\n\"connectionCount\" = \"接続数\"\n\"ipAddresses\" = \"IPアドレス\"\n\"toggleIpVisibility\" = \"IPの表示を切り替える\"\n\"overallSpeed\" = \"全体の速度\"\n\"upload\" = \"アップロード\"\n\"download\" = \"ダウンロード\"\n\"totalData\" = \"総データ量\"\n\"sent\" = \"送信\"\n\"received\" = \"受信\"\n\"documentation\" = \"ドキュメント\"\n\"xraySwitchVersionDialog\" = \"Xrayのバージョンを本当に変更しますか？\"\n\"xraySwitchVersionDialogDesc\" = \"Xrayのバージョンが#version#に変更されます。\"\n\"xraySwitchVersionPopover\" = \"Xrayの更新が成功しました\"\n\"geofileUpdateDialog\" = \"ジオファイルを本当に更新しますか？\"\n\"geofileUpdateDialogDesc\" = \"これにより#filename#ファイルが更新されます。\"\n\"geofilesUpdateDialogDesc\" = \"これにより、すべてのファイルが更新されます。\"\n\"geofilesUpdateAll\" = \"すべて更新\"\n\"geofileUpdatePopover\" = \"ジオファイルの更新が成功しました\"\n\"dontRefresh\" = \"インストール中、このページをリロードしないでください\"\n\"logs\" = \"ログ\"\n\"config\" = \"設定\"\n\"backup\" = \"バックアップ\"\n\"backupTitle\" = \"データベースのバックアップと復元\"\n\"exportDatabase\" = \"バックアップ\"\n\"exportDatabaseDesc\" = \"クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。\"\n\"importDatabase\" = \"復元\"\n\"importDatabaseDesc\" = \"クリックして、デバイスから .db ファイルを選択し、アップロードしてバックアップからデータベースを復元します。\"\n\"importDatabaseSuccess\" = \"データベースのインポートに成功しました\"\n\"importDatabaseError\" = \"データベースのインポート中にエラーが発生しました\"\n\"readDatabaseError\" = \"データベースの読み取り中にエラーが発生しました\"\n\"getDatabaseError\" = \"データベースの取得中にエラーが発生しました\"\n\"getConfigError\" = \"設定ファイルの取得中にエラーが発生しました\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"総トラフィック\"\n\"allTimeTrafficUsage\" = \"これまでの総使用量\"\n\"title\" = \"インバウンド一覧\"\n\"totalDownUp\" = \"総アップロード / ダウンロード\"\n\"totalUsage\" = \"総使用量\"\n\"inboundCount\" = \"インバウンド数\"\n\"operate\" = \"メニュー\"\n\"enable\" = \"有効化\"\n\"remark\" = \"備考\"\n\"protocol\" = \"プロトコル\"\n\"port\" = \"ポート\"\n\"portMap\" = \"ポートマッピング\"\n\"traffic\" = \"トラフィック\"\n\"details\" = \"詳細情報\"\n\"transportConfig\" = \"トランスポート設定\"\n\"expireDate\" = \"有効期限\"\n\"createdAt\" = \"作成\"\n\"updatedAt\" = \"更新\"\n\"resetTraffic\" = \"トラフィックリセット\"\n\"addInbound\" = \"インバウンド追加\"\n\"generalActions\" = \"一般操作\"\n\"autoRefresh\" = \"自動更新\"\n\"autoRefreshInterval\" = \"間隔\"\n\"modifyInbound\" = \"インバウンド修正\"\n\"deleteInbound\" = \"インバウンド削除\"\n\"deleteInboundContent\" = \"インバウンドを削除してもよろしいですか？\"\n\"deleteClient\" = \"クライアント削除\"\n\"deleteClientContent\" = \"クライアントを削除してもよろしいですか？\"\n\"resetTrafficContent\" = \"トラフィックをリセットしてもよろしいですか？\"\n\"copyLink\" = \"リンクをコピー\"\n\"address\" = \"アドレス\"\n\"network\" = \"ネットワーク\"\n\"destinationPort\" = \"宛先ポート\"\n\"targetAddress\" = \"宛先アドレス\"\n\"monitorDesc\" = \"空白にするとすべてのIPを監視\"\n\"meansNoLimit\" = \"= 無制限（単位：GB）\"\n\"totalFlow\" = \"総トラフィック\"\n\"leaveBlankToNeverExpire\" = \"空白にすると期限なし\"\n\"noRecommendKeepDefault\" = \"デフォルト値を保持することをお勧めします\"\n\"certificatePath\" = \"ファイルパス\"\n\"certificateContent\" = \"ファイル内容\"\n\"publicKey\" = \"公開鍵\"\n\"privatekey\" = \"秘密鍵\"\n\"clickOnQRcode\" = \"QRコードをクリックしてコピー\"\n\"client\" = \"クライアント\"\n\"export\" = \"リンクエクスポート\"\n\"clone\" = \"複製\"\n\"cloneInbound\" = \"複製\"\n\"cloneInboundContent\" = \"このインバウンドルールは、ポート（Port）、リスニングIP（Listening IP）、クライアント（Clients）を除くすべての設定がクローンされます\"\n\"cloneInboundOk\" = \"クローン作成\"\n\"resetAllTraffic\" = \"すべてのインバウンドトラフィックをリセット\"\n\"resetAllTrafficTitle\" = \"すべてのインバウンドトラフィックをリセット\"\n\"resetAllTrafficContent\" = \"すべてのインバウンドトラフィックをリセットしてもよろしいですか？\"\n\"resetInboundClientTraffics\" = \"クライアントトラフィックをリセット\"\n\"resetInboundClientTrafficTitle\" = \"すべてのクライアントトラフィックをリセット\"\n\"resetInboundClientTrafficContent\" = \"このインバウンドクライアントのすべてのトラフィックをリセットしてもよろしいですか？\"\n\"resetAllClientTraffics\" = \"すべてのクライアントトラフィックをリセット\"\n\"resetAllClientTrafficTitle\" = \"すべてのクライアントトラフィックをリセット\"\n\"resetAllClientTrafficContent\" = \"すべてのクライアントのトラフィックをリセットしてもよろしいですか？\"\n\"delDepletedClients\" = \"トラフィックが尽きたクライアントを削除\"\n\"delDepletedClientsTitle\" = \"トラフィックが尽きたクライアントを削除\"\n\"delDepletedClientsContent\" = \"トラフィックが尽きたすべてのクライアントを削除してもよろしいですか？\"\n\"email\" = \"メールアドレス\"\n\"emailDesc\" = \"メールアドレスは一意でなければなりません\"\n\"IPLimit\" = \"IP制限\"\n\"IPLimitDesc\" = \"設定値を超えるとインバウンドトラフィックが無効になります。（0 = 無効）\"\n\"IPLimitlog\" = \"IPログ\"\n\"IPLimitlogDesc\" = \"IP履歴ログ（無効なインバウンドトラフィックを有効にするには、ログをクリアしてください）\"\n\"IPLimitlogclear\" = \"ログをクリア\"\n\"setDefaultCert\" = \"パネル設定から証明書を設定\"\n\"telegramDesc\" = \"TelegramチャットIDを提供してください。（ボットで'/id'コマンドを使用）または（@userinfobot）\"\n\"subscriptionDesc\" = \"サブスクリプションURLを見つけるには、“詳細情報”に移動してください。また、複数のクライアントに同じ名前を使用することができます。\"\n\"info\" = \"情報\"\n\"same\" = \"同じ\"\n\"inboundData\" = \"インバウンドデータ\"\n\"exportInbound\" = \"インバウンドルールをエクスポート\"\n\"import\" = \"インポート\"\n\"importInbound\" = \"インバウンドルールをインポート\"\n\"periodicTrafficResetTitle\" = \"トラフィックリセット\"\n\"periodicTrafficResetDesc\" = \"指定された間隔でトラフィックカウンタを自動的にリセット\"\n\"lastReset\" = \"最後のリセット\"\n\n[pages.client]\n\"add\" = \"クライアント追加\"\n\"edit\" = \"クライアント編集\"\n\"submitAdd\" = \"クライアント追加\"\n\"submitEdit\" = \"変更を保存\"\n\"clientCount\" = \"クライアント数\"\n\"bulk\" = \"一括作成\"\n\"method\" = \"方法\"\n\"first\" = \"最初\"\n\"last\" = \"最後\"\n\"prefix\" = \"プレフィックス\"\n\"postfix\" = \"サフィックス\"\n\"delayedStart\" = \"初回使用後に開始\"\n\"expireDays\" = \"期間\"\n\"days\" = \"日\"\n\"renew\" = \"自動更新\"\n\"renewDesc\" = \"期限が切れた後に自動更新。（0 = 無効）（単位：日）\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"なし\"\n\"daily\" = \"毎日\"\n\"weekly\" = \"毎週\"\n\"monthly\" = \"毎月\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"取得\"\n\"updateSuccess\" = \"更新が成功しました\"\n\"logCleanSuccess\" = \"ログがクリアされました\"\n\"inboundsUpdateSuccess\" = \"インバウンドが正常に更新されました\"\n\"inboundUpdateSuccess\" = \"インバウンドが正常に更新されました\"\n\"inboundCreateSuccess\" = \"インバウンドが正常に作成されました\"\n\"inboundDeleteSuccess\" = \"インバウンドが正常に削除されました\"\n\"inboundClientAddSuccess\" = \"インバウンドクライアントが追加されました\"\n\"inboundClientDeleteSuccess\" = \"インバウンドクライアントが削除されました\"\n\"inboundClientUpdateSuccess\" = \"インバウンドクライアントが更新されました\"\n\"delDepletedClientsSuccess\" = \"すべての枯渇したクライアントが削除されました\"\n\"resetAllClientTrafficSuccess\" = \"クライアントのすべてのトラフィックがリセットされました\"\n\"resetAllTrafficSuccess\" = \"すべてのトラフィックがリセットされました\"\n\"resetInboundClientTrafficSuccess\" = \"トラフィックがリセットされました\"\n\"trafficGetError\" = \"トラフィックの取得中にエラーが発生しました\"\n\"getNewX25519CertError\" = \"X25519証明書の取得中にエラーが発生しました。\"\n\"getNewmldsa65Error\" = \"mldsa65証明書の取得中にエラーが発生しました。\"\n\"getNewVlessEncError\" = \"VlessEnc証明書の取得中にエラーが発生しました。\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"リクエスト\"\n\"response\" = \"レスポンス\"\n\"name\" = \"名前\"\n\"value\" = \"値\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"バージョン\"\n\"method\" = \"方法\"\n\"path\" = \"パス\"\n\"status\" = \"ステータス\"\n\"statusDescription\" = \"ステータス説明\"\n\"requestHeader\" = \"リクエストヘッダー\"\n\"responseHeader\" = \"レスポンスヘッダー\"\n\n[pages.settings]\n\"title\" = \"パネル設定\"\n\"save\" = \"保存\"\n\"infoDesc\" = \"ここでのすべての変更は、保存してパネルを再起動する必要があります\"\n\"restartPanel\" = \"パネル再起動\"\n\"restartPanelDesc\" = \"パネルを再起動してもよろしいですか？再起動後にパネルにアクセスできない場合は、サーバーでパネルログを確認してください\"\n\"restartPanelSuccess\" = \"パネルの再起動に成功しました\"\n\"actions\" = \"操作\"\n\"resetDefaultConfig\" = \"デフォルト設定にリセット\"\n\"panelSettings\" = \"一般\"\n\"securitySettings\" = \"セキュリティ設定\"\n\"TGBotSettings\" = \"Telegramボット設定\"\n\"panelListeningIP\" = \"パネル監視IP\"\n\"panelListeningIPDesc\" = \"デフォルトではすべてのIPを監視する\"\n\"panelListeningDomain\" = \"パネル監視ドメイン\"\n\"panelListeningDomainDesc\" = \"デフォルトで空白の場合、すべてのドメインとIPアドレスを監視する\"\n\"panelPort\" = \"パネル監視ポート\"\n\"panelPortDesc\" = \"再起動で有効\"\n\"publicKeyPath\" = \"パネル証明書公開鍵ファイルパス\"\n\"publicKeyPathDesc\" = \"'/'で始まる絶対パスを入力\"\n\"privateKeyPath\" = \"パネル証明書秘密鍵ファイルパス\"\n\"privateKeyPathDesc\" = \"'/'で始まる絶対パスを入力\"\n\"panelUrlPath\" = \"パネルURLルートパス\"\n\"panelUrlPathDesc\" = \"'/'で始まり、'/'で終わる必要があります\"\n\"pageSize\" = \"ページサイズ\"\n\"pageSizeDesc\" = \"インバウンドテーブルのページサイズを定義します。0を設定すると無効化されます\"\n\"remarkModel\" = \"備考モデルと区切り記号\"\n\"datepicker\" = \"日付ピッカー\"\n\"datepickerPlaceholder\" = \"日付を選択\"\n\"datepickerDescription\" = \"日付選択カレンダーで有効期限を指定する\"\n\"sampleRemark\" = \"備考の例\"\n\"oldUsername\" = \"旧ユーザー名\"\n\"currentPassword\" = \"旧パスワード\"\n\"newUsername\" = \"新しいユーザー名\"\n\"newPassword\" = \"新しいパスワード\"\n\"telegramBotEnable\" = \"Telegramボットを有効にする\"\n\"telegramBotEnableDesc\" = \"Telegramボット機能を有効にする\"\n\"telegramToken\" = \"Telegramボットトークン\"\n\"telegramTokenDesc\" = \"'@BotFather'から取得したTelegramボットトークン\"\n\"telegramProxy\" = \"SOCKS5プロキシ\"\n\"telegramProxyDesc\" = \"SOCKS5プロキシを有効にしてTelegramに接続する（ガイドに従って設定を調整）\"\n\"telegramAPIServer\" = \"Telegram APIサーバー\"\n\"telegramAPIServerDesc\" = \"使用するTelegram APIサーバー。空白の場合はデフォルトサーバーを使用する\"\n\"telegramChatId\" = \"管理者チャットID\"\n\"telegramChatIdDesc\" = \"Telegram管理者チャットID（複数の場合はカンマで区切る）@userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する\"\n\"telegramNotifyTime\" = \"通知時間\"\n\"telegramNotifyTimeDesc\" = \"定期的なTelegramボット通知時間を設定する（crontab時間形式を使用）\"\n\"tgNotifyBackup\" = \"データベースバックアップ\"\n\"tgNotifyBackupDesc\" = \"レポート付きのデータベースバックアップファイルを送信\"\n\"tgNotifyLogin\" = \"ログイン通知\"\n\"tgNotifyLoginDesc\" = \"誰かがパネルにログインしようとしたときに、ユーザー名、IPアドレス、時間を表示する\"\n\"sessionMaxAge\" = \"セッション期間\"\n\"sessionMaxAgeDesc\" = \"ログイン状態を保持する期間（単位：分）\"\n\"expireTimeDiff\" = \"有効期限通知のしきい値\"\n\"expireTimeDiffDesc\" = \"このしきい値に達した場合、有効期限に関する通知を受け取る（単位：日）\"\n\"trafficDiff\" = \"トラフィック消耗しきい値\"\n\"trafficDiffDesc\" = \"このしきい値に達した場合、トラフィック消耗に関する通知を受け取る（単位：GB）\"\n\"tgNotifyCpu\" = \"CPU負荷通知しきい値\"\n\"tgNotifyCpuDesc\" = \"CPU負荷がこのしきい値を超えた場合、通知を受け取る（単位：%）\"\n\"timeZone\" = \"タイムゾーン\"\n\"timeZoneDesc\" = \"定時タスクはこのタイムゾーンの時間に従って実行される\"\n\"subSettings\" = \"サブスクリプション設定\"\n\"subEnable\" = \"サブスクリプションサービスを有効にする\"\n\"subEnableDesc\" = \"サブスクリプションサービス機能を有効にする\"\n\"subJsonEnable\" = \"JSON サブスクリプションのエンドポイントを個別に有効/無効にする。\"\n\"subTitle\" = \"サブスクリプションタイトル\"\n\"subTitleDesc\" = \"VPNクライアントに表示されるタイトル\"\n\"subSupportUrl\" = \"サポートURL\"\n\"subSupportUrlDesc\" = \"VPNクライアントに表示されるテクニカルサポートへのリンク\"\n\"subProfileUrl\" = \"プロフィールURL\"\n\"subProfileUrlDesc\" = \"VPNクライアントに表示されるWebサイトへのリンク\"\n\"subAnnounce\" = \"お知らせ\"\n\"subAnnounceDesc\" = \"VPNクライアントに表示されるお知らせのテキスト\"\n\"subEnableRouting\" = \"ルーティングを有効化\"\n\"subEnableRoutingDesc\" = \"VPNクライアントでルーティングを有効にするためのグローバル設定。(Happのみ)\"\n\"subRoutingRules\" = \"ルーティングルール\"\n\"subRoutingRulesDesc\" = \"VPNクライアントのグローバルルーティングルール。(Happのみ)\"\n\"subListen\" = \"監視IP\"\n\"subListenDesc\" = \"サブスクリプションサービスが監視するIPアドレス（空白にするとすべてのIPを監視）\"\n\"subPort\" = \"監視ポート\"\n\"subPortDesc\" = \"サブスクリプションサービスが監視するポート番号（使用されていないポートである必要があります）\"\n\"subCertPath\" = \"公開鍵パス\"\n\"subCertPathDesc\" = \"サブスクリプションサービスで使用する公開鍵ファイルのパス（'/'で始まる）\"\n\"subKeyPath\" = \"秘密鍵パス\"\n\"subKeyPathDesc\" = \"サブスクリプションサービスで使用する秘密鍵ファイルのパス（'/'で始まる）\"\n\"subPath\" = \"URIパス\"\n\"subPathDesc\" = \"サブスクリプションサービスで使用するURIパス（'/'で始まり、'/'で終わる）\"\n\"subDomain\" = \"監視ドメイン\"\n\"subDomainDesc\" = \"サブスクリプションサービスが監視するドメイン（空白にするとすべてのドメインとIPを監視）\"\n\"subUpdates\" = \"更新間隔\"\n\"subUpdatesDesc\" = \"クライアントアプリケーションでサブスクリプションURLの更新間隔（単位：時間）\"\n\"subEncrypt\" = \"エンコード\"\n\"subEncryptDesc\" = \"サブスクリプションサービスが返す内容をBase64エンコードする\"\n\"subShowInfo\" = \"利用情報を表示\"\n\"subShowInfoDesc\" = \"クライアントアプリで残りのトラフィックと日付情報を表示する\"\n\"subURI\" = \"リバースプロキシURI\"\n\"subURIDesc\" = \"プロキシ後ろのサブスクリプションURLのURIパスに使用する\"\n\"externalTrafficInformEnable\" = \"外部トラフィック情報\"\n\"externalTrafficInformEnableDesc\" = \"トラフィックの更新ごとに外部 API に通知します。\"\n\"externalTrafficInformURI\" = \"外部トラフィック通知 URI\"\n\"externalTrafficInformURIDesc\" = \"トラフィックの更新ごとに外部 API に通知します。\"\n\"fragment\" = \"フラグメント\"\n\"fragmentDesc\" = \"TLS helloパケットのフラグメントを有効にする\"\n\"fragmentSett\" = \"設定\"\n\"noisesDesc\" = \"Noisesを有効にする\"\n\"noisesSett\" = \"Noises設定\"\n\"mux\" = \"マルチプレクサ\"\n\"muxDesc\" = \"確立されたストリーム内で複数の独立したストリームを伝送する\"\n\"muxSett\" = \"マルチプレクサ設定\"\n\"direct\" = \"直接接続\"\n\"directDesc\" = \"特定の国のドメインまたはIP範囲に直接接続する\"\n\"notifications\" = \"通知\"\n\"certs\" = \"証明書\"\n\"externalTraffic\" = \"外部トラフィック\"\n\"dateAndTime\" = \"日付と時刻\"\n\"proxyAndServer\" = \"プロキシとサーバー\"\n\"intervals\" = \"間隔\"\n\"information\" = \"情報\"\n\"language\" = \"言語\"\n\"telegramBotLanguage\" = \"Telegram Botの言語\"\n\n[pages.xray]\n\"title\" = \"Xray 設定\"\n\"save\" = \"保存\"\n\"restart\" = \"Xray 再起動\"\n\"restartSuccess\" = \"Xrayの再起動に成功しました\"\n\"stopSuccess\" = \"Xrayが正常に停止しました\"\n\"restartError\" = \"Xrayの再起動中にエラーが発生しました。\"\n\"stopError\" = \"Xrayの停止中にエラーが発生しました。\"\n\"basicTemplate\" = \"基本設定\"\n\"advancedTemplate\" = \"高度な設定\"\n\"generalConfigs\" = \"一般設定\"\n\"generalConfigsDesc\" = \"これらのオプションは一般設定を決定します\"\n\"logConfigs\" = \"ログ\"\n\"logConfigsDesc\" = \"ログはサーバーのパフォーマンスに影響を与える可能性があるため、必要な場合にのみ有効にすることをお勧めします\"\n\"blockConfigsDesc\" = \"これらのオプションは、特定のプロトコルやウェブサイトへのユーザー接続をブロックします\"\n\"basicRouting\" = \"基本ルーティング\"\n\"blockConnectionsConfigsDesc\" = \"これらのオプションにより、特定のリクエスト元の国に基づいてトラフィックをブロックします。\"\n\"directConnectionsConfigsDesc\" = \"直接接続により、特定のトラフィックが他のサーバーを経由しないようにします。\"\n\"blockips\" = \"IPをブロック\"\n\"blockdomains\" = \"ドメインをブロック\"\n\"directips\" = \"直接IP\"\n\"directdomains\" = \"直接ドメイン\"\n\"ipv4Routing\" = \"IPv4 ルーティング\"\n\"ipv4RoutingDesc\" = \"このオプションはIPv4のみを介してターゲットドメインへルーティングします\"\n\"warpRouting\" = \"WARP ルーティング\"\n\"warpRoutingDesc\" = \"注意：これらのオプションを使用する前に、パネルのGitHubの手順に従って、サーバーにsocks5プロキシモードでWARPをインストールしてください。WARPはCloudflareサーバー経由でトラフィックをウェブサイトにルーティングします。\"\n\"Template\" = \"高度なXray設定テンプレート\"\n\"TemplateDesc\" = \"最終的なXray設定ファイルはこのテンプレートに基づいて生成されます\"\n\"FreedomStrategy\" = \"Freedom プロトコル戦略\"\n\"FreedomStrategyDesc\" = \"Freedomプロトコル内のネットワークの出力戦略を設定する\"\n\"RoutingStrategy\" = \"ルーティングドメイン戦略設定\"\n\"RoutingStrategyDesc\" = \"DNS解決の全体的なルーティング戦略を設定する\"\n\"outboundTestUrl\" = \"アウトバウンドテスト URL\"\n\"outboundTestUrlDesc\" = \"アウトバウンド接続テストに使用する URL。既定値\"\n\"Torrent\" = \"BitTorrent プロトコルをブロック\"\n\"Inbounds\" = \"インバウンドルール\"\n\"InboundsDesc\" = \"特定のクライアントからのトラフィックを受け入れる\"\n\"Outbounds\" = \"アウトバウンドルール\"\n\"Balancers\" = \"負荷分散\"\n\"OutboundsDesc\" = \"アウトバウンドトラフィックの送信方法を設定する\"\n\"Routings\" = \"ルーティングルール\"\n\"RoutingsDesc\" = \"各ルールの優先順位が重要です\"\n\"completeTemplate\" = \"すべて\"\n\"logLevel\" = \"ログレベル\"\n\"logLevelDesc\" = \"エラーログのレベルを指定し、記録する情報を示します\"\n\"accessLog\" = \"アクセスログ\"\n\"accessLogDesc\" = \"アクセスログのファイルパス。特殊値 'none' はアクセスログを無効にします\"\n\"errorLog\" = \"エラーログ\"\n\"errorLogDesc\" = \"エラーログのファイルパス。特殊値 'none' はエラーログを無効にします\"\n\"dnsLog\" = \"DNS ログ\"\n\"dnsLogDesc\" = \"DNSクエリのログを有効にするかどうか\"\n\"maskAddress\" = \"アドレスをマスク\"\n\"maskAddressDesc\" = \"IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます\"\n\"statistics\" = \"統計\"\n\"statsInboundUplink\" = \"インバウンドアップロード統計\"\n\"statsInboundUplinkDesc\" = \"すべてのインバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。\"\n\"statsInboundDownlink\" = \"インバウンドダウンロード統計\"\n\"statsInboundDownlinkDesc\" = \"すべてのインバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。\"\n\"statsOutboundUplink\" = \"アウトバウンドアップロード統計\"\n\"statsOutboundUplinkDesc\" = \"すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。\"\n\"statsOutboundDownlink\" = \"アウトバウンドダウンロード統計\"\n\"statsOutboundDownlinkDesc\" = \"すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。\"\n\n[pages.xray.rules]\n\"first\" = \"最初\"\n\"last\" = \"最後\"\n\"up\" = \"上へ\"\n\"down\" = \"下へ\"\n\"source\" = \"ソース\"\n\"dest\" = \"宛先アドレス\"\n\"inbound\" = \"インバウンド\"\n\"outbound\" = \"アウトバウンド\"\n\"balancer\" = \"負荷分散\"\n\"info\" = \"情報\"\n\"add\" = \"ルール追加\"\n\"edit\" = \"ルール編集\"\n\"useComma\" = \"カンマ区切りの項目\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"アウトバウンド追加\"\n\"addReverse\" = \"リバース追加\"\n\"editOutbound\" = \"アウトバウンド編集\"\n\"editReverse\" = \"リバース編集\"\n\"tag\" = \"タグ\"\n\"tagDesc\" = \"一意のタグ\"\n\"address\" = \"アドレス\"\n\"reverse\" = \"リバース\"\n\"domain\" = \"ドメイン\"\n\"type\" = \"タイプ\"\n\"bridge\" = \"ブリッジ\"\n\"portal\" = \"ポータル\"\n\"link\" = \"リンク\"\n\"intercon\" = \"インターコネクション\"\n\"settings\" = \"設定\"\n\"accountInfo\" = \"アカウント情報\"\n\"outboundStatus\" = \"アウトバウンドステータス\"\n\"sendThrough\" = \"送信経路\"\n\"test\" = \"テスト\"\n\"testResult\" = \"テスト結果\"\n\"testing\" = \"接続をテスト中...\"\n\"testSuccess\" = \"テスト成功\"\n\"testFailed\" = \"テスト失敗\"\n\"testError\" = \"アウトバウンドのテストに失敗しました\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"負荷分散追加\"\n\"editBalancer\" = \"負荷分散編集\"\n\"balancerStrategy\" = \"戦略\"\n\"balancerSelectors\" = \"セレクター\"\n\"tag\" = \"タグ\"\n\"tagDesc\" = \"一意のタグ\"\n\"balancerDesc\" = \"balancerTagとoutboundTagは同時に使用できません。同時に使用された場合、outboundTagのみが有効になります。\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"シークレットキー\"\n\"publicKey\" = \"公開鍵\"\n\"allowedIPs\" = \"許可されたIP\"\n\"endpoint\" = \"エンドポイント\"\n\"psk\" = \"共有キー\"\n\"domainStrategy\" = \"ドメイン戦略\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"TUN インターフェースの名前。デフォルトは 'xray0' です\"\n\"mtuDesc\" = \"最大伝送単位。データパケットの最大サイズ。デフォルトは 1500 です\"\n\"userLevel\" = \"ユーザーレベル\"\n\"userLevelDesc\" = \"このインバウンドを通じて確立されたすべての接続は、このユーザーレベルを使用します。デフォルトは 0 です\"\n\n[pages.xray.dns]\n\"enable\" = \"DNSを有効にする\"\n\"enableDesc\" = \"組み込みDNSサーバーを有効にする\"\n\"tag\" = \"DNSインバウンドタグ\"\n\"tagDesc\" = \"このタグはルーティングルールでインバウンドタグとして使用できます\"\n\"clientIp\" = \"クライアントIP\"\n\"clientIpDesc\" = \"DNSクエリ中に指定されたIPの位置をサーバーに通知するために使用されます\"\n\"disableCache\" = \"キャッシュを無効にする\"\n\"disableCacheDesc\" = \"DNSキャッシュを無効にします\"\n\"disableFallback\" = \"フォールバックを無効にする\"\n\"disableFallbackDesc\" = \"フォールバックDNSクエリを無効にします\"\n\"disableFallbackIfMatch\" = \"一致した場合にフォールバックを無効にする\"\n\"disableFallbackIfMatchDesc\" = \"DNSサーバーの一致するドメインリストにヒットした場合、フォールバックDNSクエリを無効にします\"\n\"enableParallelQuery\" = \"並列クエリを有効にする\"\n\"enableParallelQueryDesc\" = \"複数のサーバーへの並列DNSクエリを有効にして、より高速な解決を実現\"\n\"strategy\" = \"クエリ戦略\"\n\"strategyDesc\" = \"ドメイン名解決の全体的な戦略\"\n\"add\" = \"サーバー追加\"\n\"edit\" = \"サーバー編集\"\n\"domains\" = \"ドメイン\"\n\"expectIPs\" = \"期待されるIP\"\n\"unexpectIPs\" = \"予期しないIP\"\n\"useSystemHosts\" = \"システムのHostsを使用\"\n\"useSystemHostsDesc\" = \"インストール済みシステムのhostsファイルを使用する\"\n\"usePreset\" = \"テンプレートを使用\"\n\"dnsPresetTitle\" = \"DNSテンプレート\"\n\"dnsPresetFamily\" = \"ファミリー\"\n\n[pages.xray.fakedns]\n\"add\" = \"フェイクDNS追加\"\n\"edit\" = \"フェイクDNS編集\"\n\"ipPool\" = \"IPプールサブネット\"\n\"poolSize\" = \"プールサイズ\"\n\n[pages.settings.security]\n\"admin\" = \"管理者の資格情報\"\n\"twoFactor\" = \"二段階認証\"\n\"twoFactorEnable\" = \"2FAを有効化\"\n\"twoFactorEnableDesc\" = \"セキュリティを強化するために追加の認証層を追加します。\"\n\"twoFactorModalSetTitle\" = \"二段階認証を有効にする\"\n\"twoFactorModalDeleteTitle\" = \"二段階認証を無効にする\"\n\"twoFactorModalSteps\" = \"二段階認証を設定するには、次の手順を実行してください:\"\n\"twoFactorModalFirstStep\" = \"1. 認証アプリでこのQRコードをスキャンするか、QRコード近くのトークンをコピーしてアプリに貼り付けます\"\n\"twoFactorModalSecondStep\" = \"2. アプリからコードを入力してください\"\n\"twoFactorModalRemoveStep\" = \"二段階認証を削除するには、アプリからコードを入力してください。\"\n\"twoFactorModalChangeCredentialsTitle\" = \"認証情報の変更\"\n\"twoFactorModalChangeCredentialsStep\" = \"管理者の認証情報を変更するには、アプリケーションからコードを入力してください。\"\n\"twoFactorModalSetSuccess\" = \"二要素認証が正常に設定されました\"\n\"twoFactorModalDeleteSuccess\" = \"二要素認証が正常に削除されました\"\n\"twoFactorModalError\" = \"コードが間違っています\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"パラメーターが変更されました。\"\n\"getSettings\" = \"パラメーターの取得中にエラーが発生しました\"\n\"modifyUserError\" = \"管理者認証情報の変更中にエラーが発生しました。\"\n\"modifyUser\" = \"管理者の認証情報を正常に変更しました。\"\n\"originalUserPassIncorrect\" = \"旧ユーザー名または旧パスワードが間違っています\"\n\"userPassMustBeNotEmpty\" = \"新しいユーザー名と新しいパスワードは空にできません\"\n\"getOutboundTrafficError\" = \"送信トラフィックの取得エラー\"\n\"resetOutboundTrafficError\" = \"送信トラフィックのリセットエラー\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ キーボードを閉じました！\"\n\"noResult\" = \"❗ 結果がありません！\"\n\"noQuery\" = \"❌ クエリが見つかりません！コマンドを再利用してください！\"\n\"wentWrong\" = \"❌ 何かがうまくいかなかった！\"\n\"noIpRecord\" = \"❗ IPレコードがありません！\"\n\"noInbounds\" = \"❗ インバウンドが見つかりません！\"\n\"unlimited\" = \"♾ 無制限（リセット）\"\n\"add\" = \"追加\"\n\"month\" = \"月\"\n\"months\" = \"ヶ月\"\n\"day\" = \"日\"\n\"days\" = \"日間\"\n\"hours\" = \"時間\"\n\"minutes\" = \"分\"\n\"unknown\" = \"不明\"\n\"inbounds\" = \"インバウンド\"\n\"clients\" = \"クライアント\"\n\"offline\" = \"🔴 オフライン\"\n\"online\" = \"🟢 オンライン\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ 不明なコマンド\"\n\"pleaseChoose\" = \"👇 選択してください：\\r\\n\"\n\"help\" = \"🤖 このボットをご利用いただきありがとうございます！サーバーから特定のデータを提供し、必要な変更を行うことができます。\\r\\n\\r\\n\"\n\"start\" = \"👋 こんにちは、<i>{{ .Firstname }}</i>。\\r\\n\"\n\"welcome\" = \"🤖 <b>{{ .Hostname }}</b> 管理ボットへようこそ。\\r\\n\"\n\"status\" = \"✅ ボットは正常に動作しています！\"\n\"usage\" = \"❗ 検索するテキストを入力してください！\"\n\"getID\" = \"🆔 あなたのIDは：<code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"Xray Coreを再起動するには：\\r\\n<code>/restart</code>\\r\\n\\r\\nクライアントの電子メールを検索するには：\\r\\n<code>/usage [電子メール]</code>\\r\\n\\r\\nインバウンド（クライアントの統計情報を含む）を検索するには：\\r\\n<code>/inbound [備考]</code>\\r\\n\\r\\nTelegramチャットID：\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"統計情報を検索するには、次のコマンドを使用してください：\\r\\n<code>/usage [電子メール]</code>\\r\\n\\r\\nTelegramチャットID：\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ 操作成功！\"\n\"restartFailed\" = \"❗ 操作エラー。\\r\\n\\r\\n<code>エラー: {{ .Error }}</code>\"\n\"xrayNotRunning\" = \"❗ Xray Core は動作していません。\"\n\"startDesc\" = \"メインメニューを表示\"\n\"helpDesc\" = \"ボットのヘルプ\"\n\"statusDesc\" = \"ボットの状態を確認\"\n\"idDesc\" = \"Telegram IDを表示\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 CPU使用率は{{ .Percent }}%、しきい値{{ .Threshold }}%を超えました\"\n\"selectUserFailed\" = \"❌ ユーザーの選択に失敗しました！\"\n\"userSaved\" = \"✅ Telegramユーザーが保存されました。\"\n\"loginSuccess\" = \"✅ パネルに正常にログインしました。\\r\\n\"\n\"loginFailed\" = \"❗️ パネルのログインに失敗しました。\\r\\n\"\n\"2faFailed\" = \"2FAエラー\"\n\"report\" = \"🕰 定期報告：{{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ 日時：{{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 ホスト名：{{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 X-UI バージョン：{{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Xray バージョン: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6：{{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4：{{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP：{{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IPアドレス：\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ サーバー稼働時間：{{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 サーバー負荷：{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 サーバーメモリ：{{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP接続数：{{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP接続数：{{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 トラフィック：{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Xrayステータス：{{ .State }}\\r\\n\"\n\"username\" = \"👤 ユーザー名：{{ .Username }}\\r\\n\"\n\"password\" = \"👤 パスワード: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ 時間：{{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 インバウンド：{{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 ポート：{{ .Port }}\\r\\n\"\n\"expire\" = \"📅 有効期限：{{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 残り時間：{{ .Time }}\\r\\n\"\n\"active\" = \"💡 有効：{{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 有効化済み：{{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 接続ステータス：{{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 最終オンライン: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 メール：{{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 アップロード↑：{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 ダウンロード↓：{{ .Download }}\\r\\n\"\n\"total\" = \"📊 合計：{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 Telegramユーザー：{{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 消耗済みの {{ .Type }}：\\r\\n\"\n\"exhaustedCount\" = \"🚨 消耗済みの {{ .Type }} 数量：\\r\\n\"\n\"onlinesCount\" = \"🌐 オンラインクライアント：{{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 無効化：{{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 間もなく消耗：{{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 バックアップ時間：{{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 更新時間：{{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ はい\"\n\"no\" = \"❌ いいえ\"\n\"received_id\" = \"🔑📥 IDが更新されました。\"\n\"received_password\" = \"🔑📥 パスワードが更新されました。\"\n\"received_email\" = \"📧📥 メールが更新されました。\"\n\"received_comment\" = \"💬📥 コメントが更新されました。\"\n\"id_prompt\" = \"🔑 デフォルトID: {{ .ClientId }}\\n\\nIDを入力してください。\"\n\"pass_prompt\" = \"🔑 デフォルトパスワード: {{ .ClientPassword }}\\n\\nパスワードを入力してください。\"\n\"email_prompt\" = \"📧 デフォルトメール: {{ .ClientEmail }}\\n\\nメールを入力してください。\"\n\"comment_prompt\" = \"💬 デフォルトコメント: {{ .ClientComment }}\\n\\nコメントを入力してください。\"\n\"inbound_client_data_id\" = \"🔄 インバウンド: {{ .InboundRemark }}\\n\\n🔑 ID: {{ .ClientId }}\\n📧 メール: {{ .ClientEmail }}\\n📊 トラフィック: {{ .ClientTraffic }}\\n📅 有効期限: {{ .ClientExp }}\\n🌐 IP制限: {{ .IpLimit }}\\n💬 コメント: {{ .ClientComment }}\\n\\n今すぐこのクライアントをインバウンドに追加できます！\"\n\"inbound_client_data_pass\" = \"🔄 インバウンド: {{ .InboundRemark }}\\n\\n🔑 パスワード: {{ .ClientPass }}\\n📧 メール: {{ .ClientEmail }}\\n📊 トラフィック: {{ .ClientTraffic }}\\n📅 有効期限: {{ .ClientExp }}\\n🌐 IP制限: {{ .IpLimit }}\\n💬 コメント: {{ .ClientComment }}\\n\\n今すぐこのクライアントをインバウンドに追加できます！\"\n\"cancel\" = \"❌ プロセスがキャンセルされました！\\n\\nいつでも /start で再開できます。 🔄\"\n\"error_add_client\" = \"⚠️ エラー:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"わかりました、デフォルト値を使用します。 😊\"\n\"incorrect_input\" = \"入力が無効です。\\nフレーズはスペースなしで続けて入力してください。\\n正しい例: aaaaaa\\n間違った例: aaa aaa 🚫\"\n\"AreYouSure\" = \"本当にいいですか？🤔\"\n\"SuccessResetTraffic\" = \"📧 メール: {{ .ClientEmail }}\\n🏁 結果: ✅ 成功\"\n\"FailedResetTraffic\" = \"📧 メール: {{ .ClientEmail }}\\n🏁 結果: ❌ 失敗 \\n\\n🛠️ エラー: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 すべてのクライアントのトラフィックリセットが完了しました。\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ キーボードを閉じる\"\n\"cancel\" = \"❌ キャンセル\"\n\"cancelReset\" = \"❌ リセットをキャンセル\"\n\"cancelIpLimit\" = \"❌ IP制限をキャンセル\"\n\"confirmResetTraffic\" = \"✅ トラフィックをリセットしますか？\"\n\"confirmClearIps\" = \"✅ IPをクリアしますか？\"\n\"confirmRemoveTGUser\" = \"✅ Telegramユーザーを削除しますか？\"\n\"confirmToggle\" = \"✅ ユーザーを有効/無効にしますか？\"\n\"dbBackup\" = \"データベースバックアップを取得\"\n\"serverUsage\" = \"サーバーの使用状況\"\n\"getInbounds\" = \"インバウンド情報を取得\"\n\"depleteSoon\" = \"間もなく消耗\"\n\"clientUsage\" = \"使用状況を取得\"\n\"onlines\" = \"オンラインクライアント\"\n\"commands\" = \"コマンド\"\n\"refresh\" = \"🔄 更新\"\n\"clearIPs\" = \"❌ IPをクリア\"\n\"removeTGUser\" = \"❌ Telegramユーザーを削除\"\n\"selectTGUser\" = \"👤 Telegramユーザーを選択\"\n\"selectOneTGUser\" = \"👤 1人のTelegramユーザーを選択：\"\n\"resetTraffic\" = \"📈 トラフィックをリセット\"\n\"resetExpire\" = \"📅 有効期限を変更\"\n\"ipLog\" = \"🔢 IPログ\"\n\"ipLimit\" = \"🔢 IP制限\"\n\"setTGUser\" = \"👤 Telegramユーザーを設定\"\n\"toggle\" = \"🔘 有効/無効\"\n\"custom\" = \"🔢 カスタム\"\n\"confirmNumber\" = \"✅ 確認: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ 追加を確認：{{ .Num }}\"\n\"limitTraffic\" = \"🚧 トラフィック制限\"\n\"getBanLogs\" = \"禁止ログ\"\n\"allClients\" = \"すべてのクライアント\"\n\"addClient\" = \"クライアントを追加\"\n\"submitDisable\" = \"無効として送信 ☑️\"\n\"submitEnable\" = \"有効として送信 ✅\"\n\"use_default\" = \"🏷️ デフォルトを使用\"\n\"change_id\" = \"⚙️🔑 ID\"\n\"change_password\" = \"⚙️🔑 パスワード\"\n\"change_email\" = \"⚙️📧 メールアドレス\"\n\"change_comment\" = \"⚙️💬 コメント\"\n\"ResetAllTraffics\" = \"すべてのトラフィックをリセット\"\n\"SortedTrafficUsageReport\" = \"ソートされたトラフィック使用レポート\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ 成功！\"\n\"errorOperation\" = \"❗ 操作エラー。\"\n\"getInboundsFailed\" = \"❌ インバウンド情報の取得に失敗しました。\"\n\"getClientsFailed\" = \"❌ クライアントの取得に失敗しました。\"\n\"canceled\" = \"❌ {{ .Email }}：操作がキャンセルされました。\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}：クライアントが正常に更新されました。\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}：IPが正常に更新されました。\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}：クライアントのTelegramユーザーが正常に更新されました。\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}：トラフィックが正常にリセットされました。\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}：トラフィック制限が正常に保存されました。\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}：有効期限の日数が正常にリセットされました。\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}：IP制限数が正常に保存されました：{{ .Count }}。\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}：IPが正常にクリアされました。\"\n\"getIpLog\" = \"✅ {{ .Email }}：IPログの取得。\"\n\"getUserInfo\" = \"✅ {{ .Email }}：Telegramユーザー情報の取得。\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}：Telegramユーザーが正常に削除されました。\"\n\"enableSuccess\" = \"✅ {{ .Email }}：正常に有効化されました。\"\n\"disableSuccess\" = \"✅ {{ .Email }}：正常に無効化されました。\"\n\"askToAddUserId\" = \"設定が見つかりませんでした！\\r\\n管理者に問い合わせて、設定にTelegramユーザーのChatIDを使用してください。\\r\\n\\r\\nあなたのユーザーChatID：<code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"インバウンド {{ .Inbound }} のクライアントを選択\"\n\"chooseInbound\" = \"インバウンドを選択\"\n"
  },
  {
    "path": "web/translation/translate.pt_BR.toml",
    "content": "\"username\" = \"Nome de Usuário\"\n\"password\" = \"Senha\"\n\"login\" = \"Entrar\"\n\"confirm\" = \"Confirmar\"\n\"cancel\" = \"Cancelar\"\n\"close\" = \"Fechar\"\n\"create\" = \"Criar\"\n\"update\" = \"Atualizar\"\n\"copy\" = \"Copiar\"\n\"copied\" = \"Copiado\"\n\"download\" = \"Baixar\"\n\"remark\" = \"Observação\"\n\"enable\" = \"Ativado\"\n\"protocol\" = \"Protocolo\"\n\"search\" = \"Pesquisar\"\n\"filter\" = \"Filtrar\"\n\"loading\" = \"Carregando...\"\n\"second\" = \"Segundo\"\n\"minute\" = \"Minuto\"\n\"hour\" = \"Hora\"\n\"day\" = \"Dia\"\n\"check\" = \"Verificar\"\n\"indefinite\" = \"Indeterminado\"\n\"unlimited\" = \"Ilimitado\"\n\"none\" = \"Nada\"\n\"qrCode\" = \"Código QR\"\n\"info\" = \"Mais Informações\"\n\"edit\" = \"Editar\"\n\"delete\" = \"Excluir\"\n\"reset\" = \"Redefinir\"\n\"noData\" = \"Sem dados.\"\n\"copySuccess\" = \"Copiado com Sucesso\"\n\"sure\" = \"Certo\"\n\"encryption\" = \"Criptografia\"\n\"useIPv4ForHost\" = \"Usar IPv4 para o host\"\n\"transmission\" = \"Transmissão\"\n\"host\" = \"Servidor\"\n\"path\" = \"Caminho\"\n\"camouflage\" = \"Ofuscação\"\n\"status\" = \"Status\"\n\"enabled\" = \"Ativado\"\n\"disabled\" = \"Desativado\"\n\"depleted\" = \"Encerrado\"\n\"depletingSoon\" = \"Esgotando\"\n\"offline\" = \"Offline\"\n\"online\" = \"Online\"\n\"domainName\" = \"Nome de Domínio\"\n\"monitor\" = \"IP de Escuta\"\n\"certificate\" = \"Certificado Digital\"\n\"fail\" = \"Falhou\"\n\"comment\" = \"Comentário\"\n\"success\" = \"Com Sucesso\"\n\"lastOnline\" = \"Última vez online\"\n\"getVersion\" = \"Obter Versão\"\n\"install\" = \"Instalar\"\n\"clients\" = \"Clientes\"\n\"usage\" = \"Uso\"\n\"twoFactorCode\" = \"Código\"\n\"remained\" = \"Restante\"\n\"security\" = \"Segurança\"\n\"secAlertTitle\" = \"Alerta de Segurança\"\n\"secAlertSsl\" = \"Esta conexão não é segura. Evite inserir informações confidenciais até que o TLS seja ativado para proteção de dados.\"\n\"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.\"\n\"secAlertSSL\" = \"O painel não possui uma conexão segura. Instale o certificado TLS para proteção de dados.\"\n\"secAlertPanelPort\" = \"A porta padrão do painel é vulnerável. Configure uma porta aleatória ou específica.\"\n\"secAlertPanelURI\" = \"O caminho URI padrão do painel não é seguro. Configure um caminho URI complexo.\"\n\"secAlertSubURI\" = \"O caminho URI padrão de inscrição não é seguro. Configure um caminho URI complexo.\"\n\"secAlertSubJsonURI\" = \"O caminho URI JSON de inscrição padrão não é seguro. Configure um caminho URI complexo.\"\n\"emptyDnsDesc\" = \"Nenhum servidor DNS adicionado.\"\n\"emptyFakeDnsDesc\" = \"Nenhum servidor Fake DNS adicionado.\"\n\"emptyBalancersDesc\" = \"Nenhum balanceador adicionado.\"\n\"emptyReverseDesc\" = \"Nenhum proxy reverso adicionado.\"\n\"somethingWentWrong\" = \"Algo deu errado\"\n\n[subscription]\n\"title\" = \"Informações da assinatura\"\n\"subId\" = \"ID da assinatura\"\n\"status\" = \"Status\"\n\"downloaded\" = \"Baixado\"\n\"uploaded\" = \"Enviado\"\n\"expiry\" = \"Validade\"\n\"totalQuota\" = \"Cota total\"\n\"individualLinks\" = \"Links individuais\"\n\"active\" = \"Ativo\"\n\"inactive\" = \"Inativo\"\n\"unlimited\" = \"Ilimitado\"\n\"noExpiry\" = \"Sem validade\"\n\n[menu]\n\"theme\" = \"Tema\"\n\"dark\" = \"Escuro\"\n\"ultraDark\" = \"Ultra Escuro\"\n\"dashboard\" = \"Visão Geral\"\n\"inbounds\" = \"Inbounds\"\n\"settings\" = \"Panel Settings\"\n\"xray\" = \"Xray Configs\"\n\"logout\" = \"Sair\"\n\"link\" = \"Gerenciar\"\n\n[pages.login]\n\"hello\" = \"Olá\"\n\"title\" = \"Bem-vindo\"\n\"loginAgain\" = \"Sua sessão expirou, faça login novamente\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"O formato dos dados de entrada é inválido.\"\n\"emptyUsername\" = \"Nome de usuário é obrigatório\"\n\"emptyPassword\" = \"Senha é obrigatória\"\n\"wrongUsernameOrPassword\" = \"Nome de usuário, senha ou código de dois fatores inválido.\"\n\"successLogin\" = \"Você entrou na sua conta com sucesso.\"\n\n[pages.index]\n\"title\" = \"Visão Geral\"\n\"cpu\" = \"CPU\"\n\"logicalProcessors\" = \"Processadores lógicos\"\n\"frequency\" = \"Frequência\"\n\"swap\" = \"Swap\"\n\"storage\" = \"Armazenamento\"\n\"memory\" = \"RAM\"\n\"threads\" = \"Threads\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"Parar\"\n\"restartXray\" = \"Reiniciar\"\n\"xraySwitch\" = \"Versão\"\n\"xraySwitchClick\" = \"Escolha a versão para a qual deseja alternar.\"\n\"xraySwitchClickDesk\" = \"Escolha com cuidado, pois versões mais antigas podem não ser compatíveis com as configurações atuais.\"\n\"xrayStatusUnknown\" = \"Desconhecido\"\n\"xrayStatusRunning\" = \"Em execução\"\n\"xrayStatusStop\" = \"Parado\"\n\"xrayStatusError\" = \"Erro\"\n\"xrayErrorPopoverTitle\" = \"Ocorreu um erro ao executar o Xray\"\n\"operationHours\" = \"Tempo de Atividade\"\n\"systemLoad\" = \"Carga do Sistema\"\n\"systemLoadDesc\" = \"Média de carga do sistema nos últimos 1, 5 e 15 minutos\"\n\"connectionCount\" = \"Estatísticas de Conexão\"\n\"ipAddresses\" = \"Endereços IP\"\n\"toggleIpVisibility\" = \"Alternar visibilidade do IP\"\n\"overallSpeed\" = \"Velocidade geral\"\n\"upload\" = \"Upload\"\n\"download\" = \"Download\"\n\"totalData\" = \"Dados totais\"\n\"sent\" = \"Enviado\"\n\"received\" = \"Recebido\"\n\"documentation\" = \"Documentação\"\n\"xraySwitchVersionDialog\" = \"Você realmente deseja alterar a versão do Xray?\"\n\"xraySwitchVersionDialogDesc\" = \"Isso mudará a versão do Xray para #version#.\"\n\"xraySwitchVersionPopover\" = \"Xray atualizado com sucesso\"\n\"geofileUpdateDialog\" = \"Você realmente deseja atualizar o geofile?\"\n\"geofileUpdateDialogDesc\" = \"Isso atualizará o arquivo #filename#.\"\n\"geofilesUpdateDialogDesc\" = \"Isso atualizará todos os arquivos.\"\n\"geofilesUpdateAll\" = \"Atualizar tudo\"\n\"geofileUpdatePopover\" = \"Geofile atualizado com sucesso\"\n\"dontRefresh\" = \"Instalação em andamento, por favor não atualize a página\"\n\"logs\" = \"Logs\"\n\"config\" = \"Configuração\"\n\"backup\" = \"Backup\"\n\"backupTitle\" = \"Backup e Restauração do Banco de Dados\"\n\"exportDatabase\" = \"Backup\"\n\"exportDatabaseDesc\" = \"Clique para baixar um arquivo .db contendo um backup do seu banco de dados atual para o seu dispositivo.\"\n\"importDatabase\" = \"Restaurar\"\n\"importDatabaseDesc\" = \"Clique para selecionar e enviar um arquivo .db do seu dispositivo para restaurar seu banco de dados a partir de um backup.\"\n\"importDatabaseSuccess\" = \"O banco de dados foi importado com sucesso\"\n\"importDatabaseError\" = \"Ocorreu um erro ao importar o banco de dados\"\n\"readDatabaseError\" = \"Ocorreu um erro ao ler o banco de dados\"\n\"getDatabaseError\" = \"Ocorreu um erro ao recuperar o banco de dados\"\n\"getConfigError\" = \"Ocorreu um erro ao recuperar o arquivo de configuração\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"Tráfego Total\"\n\"allTimeTrafficUsage\" = \"Uso total de todos os tempos\"\n\"title\" = \"Inbounds\"\n\"totalDownUp\" = \"Total Enviado/Recebido\"\n\"totalUsage\" = \"Uso Total\"\n\"inboundCount\" = \"Total de Inbounds\"\n\"operate\" = \"Menu\"\n\"enable\" = \"Ativado\"\n\"remark\" = \"Observação\"\n\"protocol\" = \"Protocolo\"\n\"port\" = \"Porta\"\n\"portMap\" = \"Porta Mapeada\"\n\"traffic\" = \"Tráfego\"\n\"details\" = \"Detalhes\"\n\"transportConfig\" = \"Transporte\"\n\"expireDate\" = \"Duração\"\n\"createdAt\" = \"Criado\"\n\"updatedAt\" = \"Atualizado\"\n\"resetTraffic\" = \"Redefinir Tráfego\"\n\"addInbound\" = \"Adicionar Inbound\"\n\"generalActions\" = \"Ações Gerais\"\n\"autoRefresh\" = \"Atualização automática\"\n\"autoRefreshInterval\" = \"Intervalo\"\n\"modifyInbound\" = \"Modificar Inbound\"\n\"deleteInbound\" = \"Excluir Inbound\"\n\"deleteInboundContent\" = \"Tem certeza de que deseja excluir o inbound?\"\n\"deleteClient\" = \"Excluir Cliente\"\n\"deleteClientContent\" = \"Tem certeza de que deseja excluir o cliente?\"\n\"resetTrafficContent\" = \"Tem certeza de que deseja redefinir o tráfego?\"\n\"copyLink\" = \"Copiar URL\"\n\"address\" = \"Endereço\"\n\"network\" = \"Rede\"\n\"destinationPort\" = \"Porta de Destino\"\n\"targetAddress\" = \"Endereço de Destino\"\n\"monitorDesc\" = \"Deixe em branco para ouvir todos os IPs\"\n\"meansNoLimit\" = \"= Ilimitado. (unidade: GB)\"\n\"totalFlow\" = \"Fluxo Total\"\n\"leaveBlankToNeverExpire\" = \"Deixe em branco para nunca expirar\"\n\"noRecommendKeepDefault\" = \"Recomenda-se manter o padrão\"\n\"certificatePath\" = \"Caminho\"\n\"certificateContent\" = \"Conteúdo\"\n\"publicKey\" = \"Chave Pública\"\n\"privatekey\" = \"Chave Privada\"\n\"clickOnQRcode\" = \"Clique no Código QR para Copiar\"\n\"client\" = \"Cliente\"\n\"export\" = \"Exportar Todos os URLs\"\n\"clone\" = \"Clonar\"\n\"cloneInbound\" = \"Clonar\"\n\"cloneInboundContent\" = \"Todas as configurações deste inbound, exceto Porta, IP de Escuta e Clientes, serão aplicadas ao clone.\"\n\"cloneInboundOk\" = \"Clonar\"\n\"resetAllTraffic\" = \"Redefinir Tráfego de Todos os Inbounds\"\n\"resetAllTrafficTitle\" = \"Redefinir Tráfego de Todos os Inbounds\"\n\"resetAllTrafficContent\" = \"Tem certeza de que deseja redefinir o tráfego de todos os inbounds?\"\n\"resetInboundClientTraffics\" = \"Redefinir Tráfego dos Clientes\"\n\"resetInboundClientTrafficTitle\" = \"Redefinir Tráfego dos Clientes\"\n\"resetInboundClientTrafficContent\" = \"Tem certeza de que deseja redefinir o tráfego dos clientes deste inbound?\"\n\"resetAllClientTraffics\" = \"Redefinir Tráfego de Todos os Clientes\"\n\"resetAllClientTrafficTitle\" = \"Redefinir Tráfego de Todos os Clientes\"\n\"resetAllClientTrafficContent\" = \"Tem certeza de que deseja redefinir o tráfego de todos os clientes?\"\n\"delDepletedClients\" = \"Excluir Clientes Esgotados\"\n\"delDepletedClientsTitle\" = \"Excluir Clientes Esgotados\"\n\"delDepletedClientsContent\" = \"Tem certeza de que deseja excluir todos os clientes esgotados?\"\n\"email\" = \"Email\"\n\"emailDesc\" = \"Por favor, forneça um endereço de e-mail único.\"\n\"IPLimit\" = \"Limite de IP\"\n\"IPLimitDesc\" = \"Desativa o inbound se o número ultrapassar o valor definido. (0 = desativar)\"\n\"IPLimitlog\" = \"Log de IP\"\n\"IPLimitlogDesc\" = \"O histórico de IPs. (para ativar o inbound após a desativação, limpe o log)\"\n\"IPLimitlogclear\" = \"Limpar o Log\"\n\"setDefaultCert\" = \"Definir Certificado pelo Painel\"\n\"telegramDesc\" = \"Por favor, forneça o ID do Chat do Telegram. (use o comando '/id' no bot) ou (@userinfobot)\"\n\"subscriptionDesc\" = \"Para encontrar seu URL de assinatura, navegue até 'Detalhes'. Além disso, você pode usar o mesmo nome para vários clientes.\"\n\"info\" = \"Informações\"\n\"same\" = \"Igual\"\n\"inboundData\" = \"Dados do Inbound\"\n\"exportInbound\" = \"Exportar Inbound\"\n\"import\" = \"Importar\"\n\"importInbound\" = \"Importar um Inbound\"\n\"periodicTrafficResetTitle\" = \"Reset de Tráfego\"\n\"periodicTrafficResetDesc\" = \"Reinicia automaticamente o contador de tráfego em intervalos especificados\"\n\"lastReset\" = \"Último Reset\"\n\n[pages.client]\n\"add\" = \"Adicionar Cliente\"\n\"edit\" = \"Editar Cliente\"\n\"submitAdd\" = \"Adicionar Cliente\"\n\"submitEdit\" = \"Salvar Alterações\"\n\"clientCount\" = \"Número de Clientes\"\n\"bulk\" = \"Adicionar Vários\"\n\"method\" = \"Método\"\n\"first\" = \"Primeiro\"\n\"last\" = \"Último\"\n\"prefix\" = \"Prefixo\"\n\"postfix\" = \"Sufixo\"\n\"delayedStart\" = \"Iniciar Após Primeiro Uso\"\n\"expireDays\" = \"Duração\"\n\"days\" = \"Dia(s)\"\n\"renew\" = \"Renovação Automática\"\n\"renewDesc\" = \"Renovação automática após expiração. (0 = desativado)(unidade: dia)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"Nunca\"\n\"daily\" = \"Diariamente\"\n\"weekly\" = \"Semanalmente\"\n\"monthly\" = \"Mensalmente\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"Obter\"\n\"updateSuccess\" = \"A atualização foi bem-sucedida\"\n\"logCleanSuccess\" = \"O log foi limpo\"\n\"inboundsUpdateSuccess\" = \"Entradas atualizadas com sucesso\"\n\"inboundUpdateSuccess\" = \"Entrada atualizada com sucesso\"\n\"inboundCreateSuccess\" = \"Entrada criada com sucesso\"\n\"inboundDeleteSuccess\" = \"Entrada excluída com sucesso\"\n\"inboundClientAddSuccess\" = \"Cliente(s) de entrada adicionado(s)\"\n\"inboundClientDeleteSuccess\" = \"Cliente de entrada excluído\"\n\"inboundClientUpdateSuccess\" = \"Cliente de entrada atualizado\"\n\"delDepletedClientsSuccess\" = \"Todos os clientes esgotados foram excluídos\"\n\"resetAllClientTrafficSuccess\" = \"Todo o tráfego do cliente foi reiniciado\"\n\"resetAllTrafficSuccess\" = \"Todo o tráfego foi reiniciado\"\n\"resetInboundClientTrafficSuccess\" = \"O tráfego foi reiniciado\"\n\"trafficGetError\" = \"Erro ao obter tráfegos\"\n\"getNewX25519CertError\" = \"Erro ao obter o certificado X25519.\"\n\"getNewmldsa65Error\" = \"Erro ao obter o certificado mldsa65.\"\n\"getNewVlessEncError\" = \"Erro ao obter o certificado VlessEnc.\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"Requisição\"\n\"response\" = \"Resposta\"\n\"name\" = \"Nome\"\n\"value\" = \"Valor\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"Versão\"\n\"method\" = \"Método\"\n\"path\" = \"Caminho\"\n\"status\" = \"Status\"\n\"statusDescription\" = \"Descrição do Status\"\n\"requestHeader\" = \"Cabeçalho da Requisição\"\n\"responseHeader\" = \"Cabeçalho da Resposta\"\n\n[pages.settings]\n\"title\" = \"Configurações do Painel\"\n\"save\" = \"Salvar\"\n\"infoDesc\" = \"Toda alteração feita aqui precisa ser salva. Reinicie o painel para aplicar as alterações.\"\n\"restartPanel\" = \"Reiniciar Painel\"\n\"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.\"\n\"restartPanelSuccess\" = \"O painel foi reiniciado com sucesso\"\n\"actions\" = \"Ações\"\n\"resetDefaultConfig\" = \"Redefinir para Padrão\"\n\"panelSettings\" = \"Geral\"\n\"securitySettings\" = \"Autenticação\"\n\"TGBotSettings\" = \"Bot do Telegram\"\n\"panelListeningIP\" = \"IP de Escuta\"\n\"panelListeningIPDesc\" = \"O endereço IP para o painel web. (deixe em branco para escutar em todos os IPs)\"\n\"panelListeningDomain\" = \"Domínio de Escuta\"\n\"panelListeningDomainDesc\" = \"O nome de domínio para o painel web. (deixe em branco para escutar em todos os domínios e IPs)\"\n\"panelPort\" = \"Porta de Escuta\"\n\"panelPortDesc\" = \"O número da porta para o painel web. (deve ser uma porta não usada)\"\n\"publicKeyPath\" = \"Caminho da Chave Pública\"\n\"publicKeyPathDesc\" = \"O caminho do arquivo de chave pública para o painel web. (começa com ‘/‘)\"\n\"privateKeyPath\" = \"Caminho da Chave Privada\"\n\"privateKeyPathDesc\" = \"O caminho do arquivo de chave privada para o painel web. (começa com ‘/‘)\"\n\"panelUrlPath\" = \"Caminho URI\"\n\"panelUrlPathDesc\" = \"O caminho URI para o painel web. (começa com ‘/‘ e termina com ‘/‘)\"\n\"pageSize\" = \"Tamanho da Paginação\"\n\"pageSizeDesc\" = \"Definir o tamanho da página para a tabela de entradas. (0 = desativado)\"\n\"remarkModel\" = \"Modelo de Observação & Caractere de Separação\"\n\"datepicker\" = \"Tipo de Calendário\"\n\"datepickerPlaceholder\" = \"Selecionar data\"\n\"datepickerDescription\" = \"Tarefas agendadas serão executadas com base neste calendário.\"\n\"sampleRemark\" = \"Exemplo de Observação\"\n\"oldUsername\" = \"Nome de Usuário Atual\"\n\"currentPassword\" = \"Senha Atual\"\n\"newUsername\" = \"Novo Nome de Usuário\"\n\"newPassword\" = \"Nova Senha\"\n\"telegramBotEnable\" = \"Ativar Bot do Telegram\"\n\"telegramBotEnableDesc\" = \"Ativa o bot do Telegram.\"\n\"telegramToken\" = \"Token do Telegram\"\n\"telegramTokenDesc\" = \"O token do bot do Telegram obtido de '@BotFather'.\"\n\"telegramProxy\" = \"Proxy SOCKS\"\n\"telegramProxyDesc\" = \"Ativa o proxy SOCKS5 para conectar ao Telegram. (ajuste as configurações conforme o guia)\"\n\"telegramAPIServer\" = \"API Server do Telegram\"\n\"telegramAPIServerDesc\" = \"O servidor API do Telegram a ser usado. Deixe em branco para usar o servidor padrão.\"\n\"telegramChatId\" = \"ID de Chat do Administrador\"\n\"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)\"\n\"telegramNotifyTime\" = \"Hora da Notificação\"\n\"telegramNotifyTimeDesc\" = \"O horário de notificação do bot do Telegram configurado para relatórios periódicos. (use o formato de tempo do crontab)\"\n\"tgNotifyBackup\" = \"Backup do Banco de Dados\"\n\"tgNotifyBackupDesc\" = \"Enviar arquivo de backup do banco de dados junto com o relatório.\"\n\"tgNotifyLogin\" = \"Notificação de Login\"\n\"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.\"\n\"sessionMaxAge\" = \"Duração da Sessão\"\n\"sessionMaxAgeDesc\" = \"A duração pela qual você pode permanecer logado. (unidade: minuto)\"\n\"expireTimeDiff\" = \"Notificação de Expiração\"\n\"expireTimeDiffDesc\" = \"Receba notificações sobre a data de expiração ao atingir esse limite. (unidade: dia)\"\n\"trafficDiff\" = \"Notificação de Limite de Tráfego\"\n\"trafficDiffDesc\" = \"Receba notificações sobre o limite de tráfego ao atingir esse limite. (unidade: GB)\"\n\"tgNotifyCpu\" = \"Notificação de Carga da CPU\"\n\"tgNotifyCpuDesc\" = \"Receba notificações se a carga da CPU ultrapassar esse limite. (unidade: %)\"\n\"timeZone\" = \"Fuso Horário\"\n\"timeZoneDesc\" = \"As tarefas agendadas serão executadas com base nesse fuso horário.\"\n\"subSettings\" = \"Assinatura\"\n\"subEnable\" = \"Ativar Serviço de Assinatura\"\n\"subEnableDesc\" = \"Ativa o serviço de assinatura.\"\n\"subJsonEnable\" = \"Ativar/Desativar o endpoint de assinatura JSON de forma independente.\"\n\"subTitle\" = \"Título da Assinatura\"\n\"subTitleDesc\" = \"Título exibido no cliente VPN\"\n\"subSupportUrl\" = \"URL de Suporte\"\n\"subSupportUrlDesc\" = \"Link de suporte técnico exibido no cliente VPN\"\n\"subProfileUrl\" = \"URL de Perfil\"\n\"subProfileUrlDesc\" = \"Um link para o seu site exibido no cliente VPN\"\n\"subAnnounce\" = \"Anúncio\"\n\"subAnnounceDesc\" = \"O texto do anúncio exibido no cliente VPN\"\n\"subEnableRouting\" = \"Ativar roteamento\"\n\"subEnableRoutingDesc\" = \"Configuração global para habilitar o roteamento no cliente VPN. (Apenas para Happ)\"\n\"subRoutingRules\" = \"Regras de roteamento\"\n\"subRoutingRulesDesc\" = \"Regras de roteamento globais para o cliente VPN. (Apenas para Happ)\"\n\"subListen\" = \"IP de Escuta\"\n\"subListenDesc\" = \"O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)\"\n\"subPort\" = \"Porta de Escuta\"\n\"subPortDesc\" = \"O número da porta para o serviço de assinatura. (deve ser uma porta não usada)\"\n\"subCertPath\" = \"Caminho da Chave Pública\"\n\"subCertPathDesc\" = \"O caminho do arquivo de chave pública para o serviço de assinatura. (começa com ‘/‘)\"\n\"subKeyPath\" = \"Caminho da Chave Privada\"\n\"subKeyPathDesc\" = \"O caminho do arquivo de chave privada para o serviço de assinatura. (começa com ‘/‘)\"\n\"subPath\" = \"Caminho URI\"\n\"subPathDesc\" = \"O caminho URI para o serviço de assinatura. (começa com ‘/‘ e termina com ‘/‘)\"\n\"subDomain\" = \"Domínio de Escuta\"\n\"subDomainDesc\" = \"O nome de domínio para o serviço de assinatura. (deixe em branco para escutar em todos os domínios e IPs)\"\n\"subUpdates\" = \"Intervalos de Atualização\"\n\"subUpdatesDesc\" = \"Os intervalos de atualização da URL de assinatura nos aplicativos de cliente. (unidade: hora)\"\n\"subEncrypt\" = \"Codificar\"\n\"subEncryptDesc\" = \"O conteúdo retornado pelo serviço de assinatura será codificado em Base64.\"\n\"subShowInfo\" = \"Mostrar Informações de Uso\"\n\"subShowInfoDesc\" = \"O tráfego restante e a data serão exibidos nos aplicativos de cliente.\"\n\"subURI\" = \"URI de Proxy Reverso\"\n\"subURIDesc\" = \"O caminho URI da URL de assinatura para uso por trás de proxies.\"\n\"externalTrafficInformEnable\" = \"Informações de tráfego externo\"\n\"externalTrafficInformEnableDesc\" = \"Informar a API externa sobre cada atualização de tráfego.\"\n\"externalTrafficInformURI\" = \"URI de informação de tráfego externo\"\n\"externalTrafficInformURIDesc\" = \"As atualizações de tráfego são enviadas para este URI.\"\n\"fragment\" = \"Fragmentação\"\n\"fragmentDesc\" = \"Ativa a fragmentação para o pacote TLS hello.\"\n\"fragmentSett\" = \"Configurações de Fragmentação\"\n\"noisesDesc\" = \"Ativar Noises.\"\n\"noisesSett\" = \"Configurações de Noises\"\n\"mux\" = \"Mux\"\n\"muxDesc\" = \"Transmitir múltiplos fluxos de dados independentes dentro de um fluxo de dados estabelecido.\"\n\"muxSett\" = \"Configurações de Mux\"\n\"direct\" = \"Conexão Direta\"\n\"directDesc\" = \"Estabelece conexões diretamente com domínios ou intervalos de IP de um país específico.\"\n\"notifications\" = \"Notificações\"\n\"certs\" = \"Certificados\"\n\"externalTraffic\" = \"Tráfego Externo\"\n\"dateAndTime\" = \"Data e Hora\"\n\"proxyAndServer\" = \"Proxy e Servidor\"\n\"intervals\" = \"Intervalos\"\n\"information\" = \"Informação\"\n\"language\" = \"Idioma\"\n\"telegramBotLanguage\" = \"Idioma do Bot do Telegram\"\n\n[pages.xray]\n\"title\" = \"Configurações Xray\"\n\"save\" = \"Salvar\"\n\"restart\" = \"Reiniciar Xray\"\n\"restartSuccess\" = \"Xray foi reiniciado com sucesso\"\n\"stopSuccess\" = \"Xray foi interrompido com sucesso\"\n\"restartError\" = \"Ocorreu um erro ao reiniciar o Xray.\"\n\"stopError\" = \"Ocorreu um erro ao parar o Xray.\"\n\"basicTemplate\" = \"Básico\"\n\"advancedTemplate\" = \"Avançado\"\n\"generalConfigs\" = \"Geral\"\n\"generalConfigsDesc\" = \"Essas opções determinam ajustes gerais.\"\n\"logConfigs\" = \"Log\"\n\"logConfigsDesc\" = \"Os logs podem afetar a eficiência do servidor. É recomendável habilitá-los com sabedoria apenas se necessário.\"\n\"blockConfigsDesc\" = \"Essas opções bloqueiam tráfego com base em protocolos e sites específicos solicitados.\"\n\"basicRouting\" = \"Roteamento Básico\"\n\"blockConnectionsConfigsDesc\" = \"Essas opções bloquearão o tráfego com base no país solicitado.\"\n\"directConnectionsConfigsDesc\" = \"Uma conexão direta garante que o tráfego específico não seja roteado por outro servidor.\"\n\"blockips\" = \"Bloquear IPs\"\n\"blockdomains\" = \"Bloquear Domínios\"\n\"directips\" = \"IPs Diretos\"\n\"directdomains\" = \"Domínios Diretos\"\n\"ipv4Routing\" = \"Roteamento IPv4\"\n\"ipv4RoutingDesc\" = \"Essas opções roteam o tráfego para um destino específico via IPv4.\"\n\"warpRouting\" = \"Roteamento WARP\"\n\"warpRoutingDesc\" = \"Essas opções roteam o tráfego para um destino específico via WARP.\"\n\"Template\" = \"Modelo de Configuração Avançada do Xray\"\n\"TemplateDesc\" = \"O arquivo final de configuração do Xray será gerado com base neste modelo.\"\n\"FreedomStrategy\" = \"Estratégia do Protocolo Freedom\"\n\"FreedomStrategyDesc\" = \"Definir a estratégia de saída para a rede no Protocolo Freedom.\"\n\"RoutingStrategy\" = \"Estratégia Geral de Roteamento\"\n\"RoutingStrategyDesc\" = \"Definir a estratégia geral de roteamento de tráfego para resolver todas as solicitações.\"\n\"outboundTestUrl\" = \"URL de teste de outbound\"\n\"outboundTestUrlDesc\" = \"URL usada ao testar conectividade do outbound\"\n\"Torrent\" = \"Bloquear Protocolo BitTorrent\"\n\"Inbounds\" = \"Inbounds\"\n\"InboundsDesc\" = \"Aceitar clientes específicos.\"\n\"Outbounds\" = \"Outbounds\"\n\"Balancers\" = \"Balanceadores\"\n\"OutboundsDesc\" = \"Definir o caminho de saída do tráfego.\"\n\"Routings\" = \"Regras de Roteamento\"\n\"RoutingsDesc\" = \"A prioridade de cada regra é importante!\"\n\"completeTemplate\" = \"Todos\"\n\"logLevel\" = \"Nível de Log\"\n\"logLevelDesc\" = \"O nível de log para erros, indicando a informação que precisa ser registrada.\"\n\"accessLog\" = \"Log de Acesso\"\n\"accessLogDesc\" = \"O caminho do arquivo para o log de acesso. O valor especial 'none' desativa os logs de acesso.\"\n\"errorLog\" = \"Log de Erros\"\n\"errorLogDesc\" = \"O caminho do arquivo para o log de erros. O valor especial 'none' desativa os logs de erro.\"\n\"dnsLog\" = \"Log DNS\"\n\"dnsLogDesc\" = \"Se ativar logs de consulta DNS\"\n\"maskAddress\" = \"Mascarar Endereço\"\n\"maskAddressDesc\" = \"Máscara de endereço IP, quando ativado, substitui automaticamente o endereço IP que aparece no log.\"\n\"statistics\" = \"Estatísticas\"\n\"statsInboundUplink\" = \"Estatísticas de Upload de Entrada\"\n\"statsInboundUplinkDesc\" = \"Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de entrada.\"\n\"statsInboundDownlink\" = \"Estatísticas de Download de Entrada\"\n\"statsInboundDownlinkDesc\" = \"Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de entrada.\"\n\"statsOutboundUplink\" = \"Estatísticas de Upload de Saída\"\n\"statsOutboundUplinkDesc\" = \"Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de saída.\"\n\"statsOutboundDownlink\" = \"Estatísticas de Download de Saída\"\n\"statsOutboundDownlinkDesc\" = \"Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de saída.\"\n\n[pages.xray.rules]\n\"first\" = \"Primeiro\"\n\"last\" = \"Último\"\n\"up\" = \"Cima\"\n\"down\" = \"Baixo\"\n\"source\" = \"Fonte\"\n\"dest\" = \"Destino\"\n\"inbound\" = \"Entrada\"\n\"outbound\" = \"Saída\"\n\"balancer\" = \"Balanceador\"\n\"info\" = \"Info\"\n\"add\" = \"Adicionar Regra\"\n\"edit\" = \"Editar Regra\"\n\"useComma\" = \"Itens separados por vírgula\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"Adicionar Saída\"\n\"addReverse\" = \"Adicionar Reverso\"\n\"editOutbound\" = \"Editar Saída\"\n\"editReverse\" = \"Editar Reverso\"\n\"tag\" = \"Tag\"\n\"tagDesc\" = \"Tag Única\"\n\"address\" = \"Endereço\"\n\"reverse\" = \"Reverso\"\n\"domain\" = \"Domínio\"\n\"type\" = \"Tipo\"\n\"bridge\" = \"Ponte\"\n\"portal\" = \"Portal\"\n\"link\" = \"Link\"\n\"intercon\" = \"Interconexão\"\n\"settings\" = \"Configurações\"\n\"accountInfo\" = \"Informações da Conta\"\n\"outboundStatus\" = \"Status de Saída\"\n\"sendThrough\" = \"Enviar Através de\"\n\"test\" = \"Testar\"\n\"testResult\" = \"Resultado do teste\"\n\"testing\" = \"Testando conexão...\"\n\"testSuccess\" = \"Teste bem-sucedido\"\n\"testFailed\" = \"Teste falhou\"\n\"testError\" = \"Falha ao testar saída\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"Adicionar Balanceador\"\n\"editBalancer\" = \"Editar Balanceador\"\n\"balancerStrategy\" = \"Estratégia\"\n\"balancerSelectors\" = \"Seletores\"\n\"tag\" = \"Tag\"\n\"tagDesc\" = \"Tag Única\"\n\"balancerDesc\" = \"Não é possível usar balancerTag e outboundTag ao mesmo tempo. Se usados simultaneamente, apenas outboundTag funcionará.\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"Chave Secreta\"\n\"publicKey\" = \"Chave Pública\"\n\"allowedIPs\" = \"IPs Permitidos\"\n\"endpoint\" = \"Ponto Final\"\n\"psk\" = \"Chave Pré-Compartilhada\"\n\"domainStrategy\" = \"Estratégia de Domínio\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"O nome da interface TUN. O padrão é 'xray0'\"\n\"mtuDesc\" = \"Unidade Máxima de Transmissão. O tamanho máximo dos pacotes de dados. O padrão é 1500\"\n\"userLevel\" = \"Nível do Usuário\"\n\"userLevelDesc\" = \"Todas as conexões feitas através deste inbound usarão este nível de usuário. O padrão é 0\"\n\n[pages.xray.dns]\n\"enable\" = \"Ativar DNS\"\n\"enableDesc\" = \"Ativar o servidor DNS integrado\"\n\"tag\" = \"Tag de Entrada DNS\"\n\"tagDesc\" = \"Esta tag estará disponível como uma tag de Entrada nas regras de roteamento.\"\n\"clientIp\" = \"IP do Cliente\"\n\"clientIpDesc\" = \"Usado para notificar o servidor sobre a localização IP especificada durante consultas DNS\"\n\"disableCache\" = \"Desativar cache\"\n\"disableCacheDesc\" = \"Desativa o cache de DNS\"\n\"disableFallback\" = \"Desativar Fallback\"\n\"disableFallbackDesc\" = \"Desativa consultas DNS de fallback\"\n\"disableFallbackIfMatch\" = \"Desativar Fallback Se Corresponder\"\n\"disableFallbackIfMatchDesc\" = \"Desativa consultas DNS de fallback quando a lista de domínios correspondentes do servidor DNS é atingida\"\n\"enableParallelQuery\" = \"Habilitar Consulta Paralela\"\n\"enableParallelQueryDesc\" = \"Habilitar consultas DNS paralelas para múltiplos servidores para resolução mais rápida\"\n\"strategy\" = \"Estratégia de Consulta\"\n\"strategyDesc\" = \"Estratégia geral para resolver nomes de domínio\"\n\"add\" = \"Adicionar Servidor\"\n\"edit\" = \"Editar Servidor\"\n\"domains\" = \"Domínios\"\n\"expectIPs\" = \"IPs Esperadas\"\n\"unexpectIPs\" = \"IPs inesperados\"\n\"useSystemHosts\" = \"Usar Hosts do sistema\"\n\"useSystemHostsDesc\" = \"Usar o arquivo hosts de um sistema instalado\"\n\"usePreset\" = \"Usar modelo\"\n\"dnsPresetTitle\" = \"Modelos DNS\"\n\"dnsPresetFamily\" = \"Familiar\"\n\n[pages.xray.fakedns]\n\"add\" = \"Adicionar Fake DNS\"\n\"edit\" = \"Editar Fake DNS\"\n\"ipPool\" = \"Sub-rede do Pool de IP\"\n\"poolSize\" = \"Tamanho do Pool\"\n\n[pages.settings.security]\n\"admin\" = \"Credenciais de administrador\"\n\"twoFactor\" = \"Autenticação de dois fatores\"\n\"twoFactorEnable\" = \"Ativar 2FA\"\n\"twoFactorEnableDesc\" = \"Adiciona uma camada extra de autenticação para mais segurança.\"\n\"twoFactorModalSetTitle\" = \"Ativar autenticação de dois fatores\"\n\"twoFactorModalDeleteTitle\" = \"Desativar autenticação de dois fatores\"\n\"twoFactorModalSteps\" = \"Para configurar a autenticação de dois fatores, siga alguns passos:\"\n\"twoFactorModalFirstStep\" = \"1. Escaneie este QR code no aplicativo de autenticação ou copie o token próximo ao QR code e cole no aplicativo\"\n\"twoFactorModalSecondStep\" = \"2. Digite o código do aplicativo\"\n\"twoFactorModalRemoveStep\" = \"Digite o código do aplicativo para remover a autenticação de dois fatores.\"\n\"twoFactorModalChangeCredentialsTitle\" = \"Alterar credenciais\"\n\"twoFactorModalChangeCredentialsStep\" = \"Insira o código do aplicativo para alterar as credenciais do administrador.\"\n\"twoFactorModalSetSuccess\" = \"A autenticação de dois fatores foi estabelecida com sucesso\"\n\"twoFactorModalDeleteSuccess\" = \"A autenticação de dois fatores foi excluída com sucesso\"\n\"twoFactorModalError\" = \"Código incorreto\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"Os parâmetros foram alterados.\"\n\"getSettings\" = \"Ocorreu um erro ao recuperar os parâmetros.\"\n\"modifyUserError\" = \"Ocorreu um erro ao alterar as credenciais do administrador.\"\n\"modifyUser\" = \"Você alterou com sucesso as credenciais do administrador.\"\n\"originalUserPassIncorrect\" = \"O nome de usuário ou senha atual é inválido\"\n\"userPassMustBeNotEmpty\" = \"O novo nome de usuário e senha não podem estar vazios\"\n\"getOutboundTrafficError\" = \"Erro ao obter tráfego de saída\"\n\"resetOutboundTrafficError\" = \"Erro ao redefinir tráfego de saída\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ Teclado fechado!\"\n\"noResult\" = \"❗ Nenhum resultado!\"\n\"noQuery\" = \"❌ Consulta não encontrada! Por favor, use o comando novamente!\"\n\"wentWrong\" = \"❌ Algo deu errado!\"\n\"noIpRecord\" = \"❗ Nenhum registro de IP!\"\n\"noInbounds\" = \"❗ Nenhum inbound encontrado!\"\n\"unlimited\" = \"♾ Ilimitado (Reset)\"\n\"add\" = \"Adicionar\"\n\"month\" = \"Mês\"\n\"months\" = \"Meses\"\n\"day\" = \"Dia\"\n\"days\" = \"Dias\"\n\"hours\" = \"Horas\"\n\"minutes\" = \"Minutos\"\n\"unknown\" = \"Desconhecido\"\n\"inbounds\" = \"Inbounds\"\n\"clients\" = \"Clientes\"\n\"offline\" = \"🔴 Offline\"\n\"online\" = \"🟢 Online\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ Comando desconhecido.\"\n\"pleaseChoose\" = \"👇 Escolha:\\r\\n\"\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\"\n\"start\" = \"👋 Olá <i>{{ .Firstname }}</i>.\\r\\n\"\n\"welcome\" = \"🤖 Bem-vindo ao bot de gerenciamento do <b>{{ .Hostname }}</b>.\\r\\n\"\n\"status\" = \"✅ Bot está OK!\"\n\"usage\" = \"❗ Por favor, forneça um texto para pesquisar!\"\n\"getID\" = \"🆔 Seu ID: <code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"Para reiniciar o Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\nPara pesquisar por um email de cliente:\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nPara pesquisar por inbounds (com estatísticas do cliente):\\r\\n<code>/inbound [Remark]</code>\\r\\n\\r\\nTelegram Chat ID:\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"Para pesquisar por estatísticas, use o seguinte comando:\\r\\n\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nTelegram Chat ID:\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ Operação bem-sucedida!\"\n\"restartFailed\" = \"❗ Erro na operação.\\r\\n\\r\\n<code>Erro: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core não está em execução.\"\n\"startDesc\" = \"Mostrar menu principal\"\n\"helpDesc\" = \"Ajuda do bot\"\n\"statusDesc\" = \"Verificar status do bot\"\n\"idDesc\" = \"Mostrar seu ID do Telegram\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 A carga da CPU {{ .Percent }}% excede o limite de {{ .Threshold }}%\"\n\"selectUserFailed\" = \"❌ Erro na seleção do usuário!\"\n\"userSaved\" = \"✅ Usuário do Telegram salvo.\"\n\"loginSuccess\" = \"✅ Conectado ao painel com sucesso.\\r\\n\"\n\"loginFailed\" = \"❗️Tentativa de login no painel falhou.\\r\\n\"\n\"2faFailed\" = \"Falha no 2FA\"\n\"report\" = \"🕰 Relatórios agendados: {{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ Data&Hora: {{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 Host: {{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 Versão 3X-UI: {{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Versão Xray: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IPs:\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ Tempo de atividade: {{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 Carga do sistema: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 RAM: {{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP: {{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP: {{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 Tráfego: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Status: {{ .State }}\\r\\n\"\n\"username\" = \"👤 Nome de usuário: {{ .Username }}\\r\\n\"\n\"password\" = \"👤 Senha: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ Hora: {{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 Inbound: {{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 Porta: {{ .Port }}\\r\\n\"\n\"expire\" = \"📅 Data de expiração: {{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 Expira em: {{ .Time }}\\r\\n\"\n\"active\" = \"💡 Ativo: {{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 Ativado: {{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 Status da conexão: {{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 Última vez online: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 Email: {{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 Upload: ↑{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 Download: ↓{{ .Download }}\\r\\n\"\n\"total\" = \"📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 Usuário do Telegram: {{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 {{ .Type }} esgotado:\\r\\n\"\n\"exhaustedCount\" = \"🚨 Contagem de {{ .Type }} esgotado:\\r\\n\"\n\"onlinesCount\" = \"🌐 Clientes online: {{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 Desativado: {{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 Esgotar em breve: {{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 Hora do backup: {{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 Atualizado em: {{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ Sim\"\n\"no\" = \"❌ Não\"\n\"received_id\" = \"🔑📥 ID atualizado.\"\n\"received_password\" = \"🔑📥 Senha atualizada.\"\n\"received_email\" = \"📧📥 E-mail atualizado.\"\n\"received_comment\" = \"💬📥 Comentário atualizado.\"\n\"id_prompt\" = \"🔑 ID Padrão: {{ .ClientId }}\\n\\nDigite seu ID.\"\n\"pass_prompt\" = \"🔑 Senha Padrão: {{ .ClientPassword }}\\n\\nDigite sua senha.\"\n\"email_prompt\" = \"📧 E-mail Padrão: {{ .ClientEmail }}\\n\\nDigite seu e-mail.\"\n\"comment_prompt\" = \"💬 Comentário Padrão: {{ .ClientComment }}\\n\\nDigite seu comentário.\"\n\"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!\"\n\"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!\"\n\"cancel\" = \"❌ Processo Cancelado! \\n\\nVocê pode iniciar novamente a qualquer momento com /start. 🔄\"\n\"error_add_client\" = \"⚠️ Erro:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"Tudo bem, vou manter o valor padrão. 😊\"\n\"incorrect_input\" = \"Sua entrada não é válida.\\nAs frases devem ser contínuas, sem espaços.\\nExemplo correto: aaaaaa\\nExemplo incorreto: aaa aaa 🚫\"\n\"AreYouSure\" = \"Você tem certeza? 🤔\"\n\"SuccessResetTraffic\" = \"📧 Email: {{ .ClientEmail }}\\n🏁 Resultado: ✅ Sucesso\"\n\"FailedResetTraffic\" = \"📧 Email: {{ .ClientEmail }}\\n🏁 Resultado: ❌ Falhou \\n\\n🛠️ Erro: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 Processo de redefinição de tráfego concluído para todos os clientes.\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ Fechar teclado\"\n\"cancel\" = \"❌ Cancelar\"\n\"cancelReset\" = \"❌ Cancelar redefinição\"\n\"cancelIpLimit\" = \"❌ Cancelar limite de IP\"\n\"confirmResetTraffic\" = \"✅ Confirmar redefinição de tráfego?\"\n\"confirmClearIps\" = \"✅ Confirmar limpar IPs?\"\n\"confirmRemoveTGUser\" = \"✅ Confirmar remover usuário do Telegram?\"\n\"confirmToggle\" = \"✅ Confirmar ativar/desativar usuário?\"\n\"dbBackup\" = \"Obter backup do DB\"\n\"serverUsage\" = \"Uso do servidor\"\n\"getInbounds\" = \"Obter Inbounds\"\n\"depleteSoon\" = \"Esgotar em breve\"\n\"clientUsage\" = \"Obter uso\"\n\"onlines\" = \"Clientes online\"\n\"commands\" = \"Comandos\"\n\"refresh\" = \"🔄 Atualizar\"\n\"clearIPs\" = \"❌ Limpar IPs\"\n\"removeTGUser\" = \"❌ Remover usuário do Telegram\"\n\"selectTGUser\" = \"👤 Selecionar usuário do Telegram\"\n\"selectOneTGUser\" = \"👤 Selecione um usuário do Telegram:\"\n\"resetTraffic\" = \"📈 Redefinir tráfego\"\n\"resetExpire\" = \"📅 Alterar data de expiração\"\n\"ipLog\" = \"🔢 Log de IP\"\n\"ipLimit\" = \"🔢 Limite de IP\"\n\"setTGUser\" = \"👤 Definir usuário do Telegram\"\n\"toggle\" = \"🔘 Ativar / Desativar\"\n\"custom\" = \"🔢 Personalizado\"\n\"confirmNumber\" = \"✅ Confirmar: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ Confirmar adicionar: {{ .Num }}\"\n\"limitTraffic\" = \"🚧 Limite de tráfego\"\n\"getBanLogs\" = \"Obter logs de banimento\"\n\"allClients\" = \"Todos os clientes\"\n\"addClient\" = \"Adicionar Cliente\"\n\"submitDisable\" = \"Enviar como Desativado ☑️\"\n\"submitEnable\" = \"Enviar como Ativado ✅\"\n\"use_default\" = \"🏷️ Usar padrão\"\n\"change_id\" = \"⚙️🔑 ID\"\n\"change_password\" = \"⚙️🔑 Senha\"\n\"change_email\" = \"⚙️📧 E-mail\"\n\"change_comment\" = \"⚙️💬 Comentário\"\n\"ResetAllTraffics\" = \"Redefinir Todo o Tráfego\"\n\"SortedTrafficUsageReport\" = \"Relatório de Uso de Tráfego Ordenado\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ Operação bem-sucedida!\"\n\"errorOperation\" = \"❗ Erro na operação.\"\n\"getInboundsFailed\" = \"❌ Falha ao obter inbounds.\"\n\"getClientsFailed\" = \"❌ Falha ao obter clientes.\"\n\"canceled\" = \"❌ {{ .Email }}: Operação cancelada.\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}: Cliente atualizado com sucesso.\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}: IPs atualizados com sucesso.\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}: Usuário do Telegram do cliente atualizado com sucesso.\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}: Tráfego redefinido com sucesso.\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: Limite de tráfego salvo com sucesso.\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}: Dias de expiração redefinidos com sucesso.\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}: Limite de IP {{ .Count }} salvo com sucesso.\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}: IPs limpos com sucesso.\"\n\"getIpLog\" = \"✅ {{ .Email }}: Obter log de IP.\"\n\"getUserInfo\" = \"✅ {{ .Email }}: Obter informações do usuário do Telegram.\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}: Usuário do Telegram removido com sucesso.\"\n\"enableSuccess\" = \"✅ {{ .Email }}: Ativado com sucesso.\"\n\"disableSuccess\" = \"✅ {{ .Email }}: Desativado com sucesso.\"\n\"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: <code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"Escolha um cliente para Inbound {{ .Inbound }}\"\n\"chooseInbound\" = \"Escolha um Inbound\"\n"
  },
  {
    "path": "web/translation/translate.ru_RU.toml",
    "content": "\"username\" = \"Имя пользователя\"\n\"password\" = \"Пароль\"\n\"login\" = \"Войти\"\n\"confirm\" = \"Подтвердить\"\n\"cancel\" = \"Отмена\"\n\"close\" = \"Закрыть\"\n\"create\" = \"Создать\"\n\"update\" = \"Обновить\"\n\"copy\" = \"Копировать\"\n\"copied\" = \"Скопировано\"\n\"download\" = \"Скачать\"\n\"remark\" = \"Примечание\"\n\"enable\" = \"Включить\"\n\"protocol\" = \"Протокол\"\n\"search\" = \"Поиск\"\n\"filter\" = \"Фильтр\"\n\"loading\" = \"Загрузка...\"\n\"second\" = \"Секунда\"\n\"minute\" = \"Минута\"\n\"hour\" = \"Час\"\n\"day\" = \"День\"\n\"check\" = \"Проверить\"\n\"indefinite\" = \"Бесконечно\"\n\"unlimited\" = \"Безлимит\"\n\"none\" = \"Пусто\"\n\"qrCode\" = \"QR-код\"\n\"info\" = \"Информация\"\n\"edit\" = \"Изменить\"\n\"delete\" = \"Удалить\"\n\"reset\" = \"Сбросить\"\n\"noData\" = \"Нет данных.\"\n\"copySuccess\" = \"Скопировано\"\n\"sure\" = \"Да\"\n\"encryption\" = \"Шифрование\"\n\"useIPv4ForHost\" = \"Использовать IPv4 для подключения к хосту\"\n\"transmission\" = \"Транспорт\"\n\"host\" = \"Хост\"\n\"path\" = \"Путь\"\n\"camouflage\" = \"Маскировка\"\n\"status\" = \"Статус\"\n\"enabled\" = \"Включено\"\n\"disabled\" = \"Отключено\"\n\"depleted\" = \"Исчерпано\"\n\"depletingSoon\" = \"Почти исчерпано\"\n\"offline\" = \"Офлайн\"\n\"online\" = \"Онлайн\"\n\"domainName\" = \"Домен\"\n\"monitor\" = \"Мониторинг IP\"\n\"certificate\" = \"SSL-сертификат\"\n\"fail\" = \"Сбой\"\n\"comment\" = \"Комментарий\"\n\"success\" = \"Успешно\"\n\"lastOnline\" = \"Был(а) в сети\"\n\"getVersion\" = \"Узнать версию\"\n\"install\" = \"Установка\"\n\"clients\" = \"Клиенты\"\n\"usage\" = \"Использование\"\n\"twoFactorCode\" = \"Код 2FA\"\n\"remained\" = \"Остаток\"\n\"security\" = \"Безопасность\"\n\"secAlertTitle\" = \"Предупреждение системы безопасности\"\n\"secAlertSsl\" = \"Соединение не защищено. Не вводите конфиденциальные данные до установки SSL-сертификата.\"\n\"secAlertConf\" = \"Некоторые настройки уязвимы. Рекомендуется усилить защиту для предотвращения атак.\"\n\"secAlertSSL\" = \"Подключение к панели не защищено. Установите SSL-сертификат для защиты данных.\"\n\"secAlertPanelPort\" = \"Порт панели по умолчанию небезопасен. Установите нестандартный или случайный порт.\"\n\"secAlertPanelURI\" = \"Адрес панели по умолчанию небезопасен. Настройте уникальный и сложный URI.\"\n\"secAlertSubURI\" = \"URI подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес.\"\n\"secAlertSubJsonURI\" = \"URI JSON-подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес.\"\n\"emptyDnsDesc\" = \"Нет добавленных DNS-серверов.\"\n\"emptyFakeDnsDesc\" = \"Нет добавленных Fake DNS-серверов.\"\n\"emptyBalancersDesc\" = \"Нет добавленных балансировщиков.\"\n\"emptyReverseDesc\" = \"Нет добавленных реверс-прокси.\"\n\"somethingWentWrong\" = \"Что-то пошло не так\"\n\n[subscription]\n\"title\" = \"Информация о подписке\"\n\"subId\" = \"ID подписки\"\n\"status\" = \"Статус\"\n\"downloaded\" = \"Загружено\"\n\"uploaded\" = \"Отправлено\"\n\"expiry\" = \"Срок действия\"\n\"totalQuota\" = \"Общий лимит\"\n\"individualLinks\" = \"Индивидуальные ссылки\"\n\"active\" = \"Активна\"\n\"inactive\" = \"Неактивна\"\n\"unlimited\" = \"Неограниченно\"\n\"noExpiry\" = \"Бессрочно\"\n\n[menu]\n\"theme\" = \"Тема\"\n\"dark\" = \"Темная\"\n\"ultraDark\" = \"Очень темная\"\n\"dashboard\" = \"Дашборд\"\n\"inbounds\" = \"Подключения\"\n\"settings\" = \"Настройки\"\n\"xray\" = \"Настройки Xray\"\n\"logout\" = \"Выход\"\n\"link\" = \"Управление\"\n\n[pages.login]\n\"hello\" = \"Привет!\"\n\"title\" = \"Добро пожаловать!\"\n\"loginAgain\" = \"Сессия истекла. Войдите в систему снова\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"Недопустимый формат данных\"\n\"emptyUsername\" = \"Введите имя пользователя\"\n\"emptyPassword\" = \"Введите пароль\"\n\"wrongUsernameOrPassword\" = \"Неверные данные учетной записи.\"\n\"successLogin\" = \"Вход выполнен успешно\"\n\n[pages.index]\n\"title\" = \"Дашборд\"\n\"cpu\" = \"ЦП\"\n\"logicalProcessors\" = \"Логические процессоры\"\n\"frequency\" = \"Частота\"\n\"swap\" = \"Файл подкачки\"\n\"storage\" = \"Диск\"\n\"memory\" = \"ОЗУ\"\n\"threads\" = \"Потоки\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"Остановить\"\n\"restartXray\" = \"Перезапустить\"\n\"xraySwitch\" = \"Выбор версии\"\n\"xraySwitchClick\" = \"Выберите нужную версию\"\n\"xraySwitchClickDesk\" = \"Важно: старые версии могут не поддерживать текущие настройки\"\n\"xrayStatusUnknown\" = \"Неизвестно\"\n\"xrayStatusRunning\" = \"Запущен\"\n\"xrayStatusStop\" = \"Остановлен\"\n\"xrayStatusError\" = \"Ошибка\"\n\"xrayErrorPopoverTitle\" = \"Ошибка при запуске Xray\"\n\"operationHours\" = \"Время работы системы\"\n\"systemLoad\" = \"Нагрузка на систему\"\n\"systemLoadDesc\" = \"Средняя загрузка системы за последние 1, 5 и 15 минут\"\n\"connectionCount\" = \"Количество соединений\"\n\"ipAddresses\" = \"IP-адреса сервера\"\n\"toggleIpVisibility\" = \"Скрыть или показать IP-адреса сервера\"\n\"overallSpeed\" = \"Общая скорость передачи трафика\"\n\"upload\" = \"Отправка\"\n\"download\" = \"Загрузка\"\n\"totalData\" = \"Общий объем трафика\"\n\"sent\" = \"Отправлено\"\n\"received\" = \"Получено\"\n\"documentation\" = \"Документация\"\n\"xraySwitchVersionDialog\" = \"Переключить версию Xray\"\n\"xraySwitchVersionDialogDesc\" = \"Вы точно хотите сменить версию Xray?\"\n\"xraySwitchVersionPopover\" = \"Xray успешно обновлён\"\n\"geofileUpdateDialog\" = \"Вы действительно хотите обновить геофайл?\"\n\"geofileUpdateDialogDesc\" = \"Это обновит файл #filename#.\"\n\"geofilesUpdateDialogDesc\" = \"Это обновит все геофайлы.\"\n\"geofilesUpdateAll\" = \"Обновить все\"\n\"geofileUpdatePopover\" = \"Геофайлы успешно обновлены\"\n\"dontRefresh\" = \"Установка в процессе. Не обновляйте страницу\"\n\"logs\" = \"Журнал\"\n\"config\" = \"Конфигурация\"\n\"backup\" = \"Резервная копия\"\n\"backupTitle\" = \"Резервная копия базы данных\"\n\"exportDatabase\" = \"Экспорт базы данных\"\n\"exportDatabaseDesc\" = \"Нажмите, чтобы скачать файл .db, содержащий резервную копию вашей текущей базы данных на ваше устройство.\"\n\"importDatabase\" = \"Импорт базы данных\"\n\"importDatabaseDesc\" = \"Нажмите, чтобы выбрать и загрузить файл .db с вашего устройства для восстановления базы данных из резервной копии.\"\n\"importDatabaseSuccess\" = \"База данных успешно импортирована\"\n\"importDatabaseError\" = \"Произошла ошибка при импорте базы данных\"\n\"readDatabaseError\" = \"Произошла ошибка при чтении базы данных\"\n\"getDatabaseError\" = \"Произошла ошибка при получении базы данных\"\n\"getConfigError\" = \"Произошла ошибка при получении конфигурационного файла\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"Общий трафик\"\n\"allTimeTrafficUsage\" = \"Общее использование за все время\"\n\"title\" = \"Подключения\"\n\"totalDownUp\" = \"Отправлено/получено\"\n\"totalUsage\" = \"Всего трафика\"\n\"inboundCount\" = \"Всего подключений\"\n\"operate\" = \"Меню\"\n\"enable\" = \"Включить\"\n\"remark\" = \"Примечание\"\n\"protocol\" = \"Протокол\"\n\"port\" = \"Порт\"\n\"portMap\" = \"Порт-маппинг\"\n\"traffic\" = \"Трафик\"\n\"details\" = \"Подробнее\"\n\"transportConfig\" = \"Транспорт\"\n\"expireDate\" = \"Дата окончания\"\n\"createdAt\" = \"Создано\"\n\"updatedAt\" = \"Обновлено\"\n\"resetTraffic\" = \"Сброс трафика\"\n\"addInbound\" = \"Создать подключение\"\n\"generalActions\" = \"Общие действия\"\n\"autoRefresh\" = \"Автообновление\"\n\"autoRefreshInterval\" = \"Интервал\"\n\"modifyInbound\" = \"Изменить подключение\"\n\"deleteInbound\" = \"Удалить подключение\"\n\"deleteInboundContent\" = \"Вы уверены, что хотите удалить подключение?\"\n\"deleteClient\" = \"Удалить клиента\"\n\"deleteClientContent\" = \"Вы уверены, что хотите удалить клиента?\"\n\"resetTrafficContent\" = \"Вы уверены, что хотите сбросить трафик?\"\n\"copyLink\" = \"Копировать ссылку\"\n\"address\" = \"Адрес\"\n\"network\" = \"Сеть\"\n\"destinationPort\" = \"Порт назначения\"\n\"targetAddress\" = \"Целевой адрес\"\n\"monitorDesc\" = \"Оставьте пустым для прослушивания всех IP-адресов\"\n\"meansNoLimit\" = \"= Без ограничений (значение: ГБ)\"\n\"totalFlow\" = \"Общий расход\"\n\"leaveBlankToNeverExpire\" = \"Оставьте пустым, чтобы было бесконечным\"\n\"noRecommendKeepDefault\" = \"Рекомендуется оставить настройки по умолчанию\"\n\"certificatePath\" = \"Путь к сертификату\"\n\"certificateContent\" = \"Содержимое сертификата\"\n\"publicKey\" = \"Публичный ключ\"\n\"privatekey\" = \"Приватный ключ\"\n\"clickOnQRcode\" = \"Нажмите на QR-код, чтобы скопировать\"\n\"client\" = \"Клиент\"\n\"export\" = \"Экспорт ссылок\"\n\"clone\" = \"Клонировать\"\n\"cloneInbound\" = \"Клонировать\"\n\"cloneInboundContent\" = \"Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания\"\n\"cloneInboundOk\" = \"Клонировано\"\n\"resetAllTraffic\" = \"Сброс трафика всех подключений\"\n\"resetAllTrafficTitle\" = \"Сброс трафика всех подключений\"\n\"resetAllTrafficContent\" = \"Вы уверены, что хотите сбросить трафик всех подключений?\"\n\"resetInboundClientTraffics\" = \"Сброс трафика клиента\"\n\"resetInboundClientTrafficTitle\" = \"Сброс трафика клиентов\"\n\"resetInboundClientTrafficContent\" = \"Вы уверены, что хотите сбросить трафик для этих клиентов?\"\n\"resetAllClientTraffics\" = \"Сброс трафика всех клиентов\"\n\"resetAllClientTrafficTitle\" = \"Сброс трафика всех клиентов\"\n\"resetAllClientTrafficContent\" = \"Вы уверены, что хотите сбросить трафик всех клиентов?\"\n\"delDepletedClients\" = \"Удалить отключенных клиентов\"\n\"delDepletedClientsTitle\" = \"Удаление отключенных клиентов\"\n\"delDepletedClientsContent\" = \"Вы уверены, что хотите удалить всех отключенных клиентов?\"\n\"email\" = \"Email\"\n\"emailDesc\" = \"Пожалуйста, укажите уникальный Email\"\n\"IPLimit\" = \"Лимит по количеству IP\"\n\"IPLimitDesc\" = \"Ограничение числа одновременных подключений с разных IP (0 – отключить)\"\n\"IPLimitlog\" = \"Лог IP-адресов\"\n\"IPLimitlogDesc\" = \"Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)\"\n\"IPLimitlogclear\" = \"Очистить лог\"\n\"setDefaultCert\" = \"Установить сертификат панели\"\n\"telegramDesc\" = \"Пожалуйста, укажите Chat ID Telegram. (используйте команду '/id' в боте) или (@userinfobot)\"\n\"subscriptionDesc\" = \"Вы можете найти свою ссылку подписки в разделе 'Подробнее'\"\n\"info\" = \"Информация\"\n\"same\" = \"Тот же\"\n\"inboundData\" = \"Данные подключений\"\n\"exportInbound\" = \"Экспорт подключений\"\n\"import\" = \"Импортировать\"\n\"importInbound\" = \"Импорт подключений\"\n\"periodicTrafficResetTitle\" = \"Сброс трафика\"\n\"periodicTrafficResetDesc\" = \"Автоматический сброс счетчика трафика через указанные интервалы\"\n\"lastReset\" = \"Последний сброс\"\n\n[pages.client]\n\"add\" = \"Добавить клиента\"\n\"edit\" = \"Редактировать клиента\"\n\"submitAdd\" = \"Добавить\"\n\"submitEdit\" = \"Сохранить изменения\"\n\"clientCount\" = \"Количество клиентов\"\n\"bulk\" = \"Добавить несколько\"\n\"method\" = \"Метод\"\n\"first\" = \"Первый\"\n\"last\" = \"Последний\"\n\"prefix\" = \"Префикс\"\n\"postfix\" = \"Постфикс\"\n\"delayedStart\" = \"Начало использования\"\n\"expireDays\" = \"Длительность\"\n\"days\" = \"дней\"\n\"renew\" = \"Автопродление\"\n\"renewDesc\" = \"Автопродление после истечения срока действия. (0 = отключить)(единица: день)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"Никогда\"\n\"daily\" = \"Ежедневно\"\n\"weekly\" = \"Еженедельно\"\n\"monthly\" = \"Ежемесячно\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"Получить\"\n\"updateSuccess\" = \"Обновление прошло успешно\"\n\"logCleanSuccess\" = \"Лог был очищен\"\n\"inboundsUpdateSuccess\" = \"Подключения успешно обновлены\"\n\"inboundUpdateSuccess\" = \"Подключение успешно обновлено\"\n\"inboundCreateSuccess\" = \"Подключение успешно создано\"\n\"inboundDeleteSuccess\" = \"Подключение успешно удалено\"\n\"inboundClientAddSuccess\" = \"Клиент(ы) подключения добавлен(ы)\"\n\"inboundClientDeleteSuccess\" = \"Клиент подключения удалён\"\n\"inboundClientUpdateSuccess\" = \"Клиент подключения обновлён\"\n\"delDepletedClientsSuccess\" = \"Все исчерпанные клиенты удалены\"\n\"resetAllClientTrafficSuccess\" = \"Весь трафик клиента сброшен\"\n\"resetAllTrafficSuccess\" = \"Весь трафик сброшен\"\n\"resetInboundClientTrafficSuccess\" = \"Трафик сброшен\"\n\"trafficGetError\" = \"Ошибка получения данных о трафике\"\n\"getNewX25519CertError\" = \"Ошибка при получении сертификата X25519.\"\n\"getNewmldsa65Error\" = \"Ошибка при получении сертификата mldsa65.\"\n\"getNewVlessEncError\" = \"Ошибка при получении сертификата VlessEnc.\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"Запрос\"\n\"response\" = \"Ответ\"\n\"name\" = \"Имя\"\n\"value\" = \"Значение\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"Версия\"\n\"method\" = \"Метод\"\n\"path\" = \"Путь\"\n\"status\" = \"Статус\"\n\"statusDescription\" = \"Описание статуса\"\n\"requestHeader\" = \"Заголовок запроса\"\n\"responseHeader\" = \"Заголовок ответа\"\n\n[pages.settings]\n\"title\" = \"Настройки\"\n\"save\" = \"Сохранить\"\n\"infoDesc\" = \"Сохраните изменения и перезапустите панель для их применения.\"\n\"restartPanel\" = \"Перезапуск панели\"\n\"restartPanelDesc\" = \"Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера\"\n\"restartPanelSuccess\" = \"Панель успешно перезапущена\"\n\"actions\" = \"Действия\"\n\"resetDefaultConfig\" = \"Восстановить настройки по умолчанию\"\n\"panelSettings\" = \"Панель\"\n\"securitySettings\" = \"Учетная запись\"\n\"TGBotSettings\" = \"Telegram-Бот\"\n\"panelListeningIP\" = \"IP-адрес для управления панелью\"\n\"panelListeningIPDesc\" = \"Оставьте пустым для подключения с любого IP\"\n\"panelListeningDomain\" = \"Домен панели\"\n\"panelListeningDomainDesc\" = \"Оставьте пустым для подключения с любых доменов и IP.\"\n\"panelPort\" = \"Порт панели\"\n\"panelPortDesc\" = \"Порт, на котором работает панель\"\n\"publicKeyPath\" = \"Путь к файлу публичного ключа сертификата панели\"\n\"publicKeyPathDesc\" = \"Введите полный путь, начинающийся с '/'\"\n\"privateKeyPath\" = \"Путь к файлу приватного ключа сертификата панели\"\n\"privateKeyPathDesc\" = \"Введите полный путь, начинающийся с '/'\"\n\"panelUrlPath\" = \"Корневой путь URL адреса панели\"\n\"panelUrlPathDesc\" = \"Должен начинаться с '/' и заканчиваться '/'\"\n\"pageSize\" = \"Размер нумерации страниц\"\n\"pageSizeDesc\" = \"Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить\"\n\"remarkModel\" = \"Модель примечания и символ разделения\"\n\"datepicker\" = \"Тип календаря\"\n\"datepickerPlaceholder\" = \"Выберите дату\"\n\"datepickerDescription\" = \"Запланированные задачи будут выполняться в соответствии с этим календарем.\"\n\"sampleRemark\" = \"Пример примечания\"\n\"oldUsername\" = \"Текущий логин\"\n\"currentPassword\" = \"Текущий пароль\"\n\"newUsername\" = \"Новый логин\"\n\"newPassword\" = \"Новый пароль\"\n\"telegramBotEnable\" = \"Включить Telegram бота\"\n\"telegramBotEnableDesc\" = \"Доступ к функциям панели через Telegram-бота\"\n\"telegramToken\" = \"Токен Telegram бота\"\n\"telegramTokenDesc\" = \"Необходимо получить токен у менеджера ботов Telegram @botfather\"\n\"telegramProxy\" = \"Прокси-сервер Socks5\"\n\"telegramProxyDesc\" = \"Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству.\"\n\"telegramAPIServer\" = \"API-сервер Telegram\"\n\"telegramAPIServerDesc\" = \"Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию.\"\n\"telegramChatId\" = \"User ID администратора бота\"\n\"telegramChatIdDesc\" = \"Один или несколько User ID администратора(-ов) Telegram-бота. Для получения User ID используйте @userinfobot или команду '/id' в боте.\"\n\"telegramNotifyTime\" = \"Частота уведомлений для администраторов от бота\"\n\"telegramNotifyTimeDesc\" = \"Укажите интервал уведомлений в формате Crontab\"\n\"tgNotifyBackup\" = \"Резервное копирование базы данных\"\n\"tgNotifyBackupDesc\" = \"Отправлять уведомление с файлом резервной копии базы данных\"\n\"tgNotifyLogin\" = \"Уведомление о входе\"\n\"tgNotifyLoginDesc\" = \"Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель.\"\n\"sessionMaxAge\" = \"Продолжительность сессии\"\n\"sessionMaxAgeDesc\" = \"Продолжительность сессии в системе (значение: минута)\"\n\"expireTimeDiff\" = \"Задержка уведомления об истечении сессии\"\n\"expireTimeDiffDesc\" = \"Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)\"\n\"trafficDiff\" = \"Порог трафика для уведомления\"\n\"trafficDiffDesc\" = \"Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)\"\n\"tgNotifyCpu\" = \"Порог нагрузки на ЦП для уведомления\"\n\"tgNotifyCpuDesc\" = \"Уведомление администраторов в Telegram, если нагрузка на ЦП превышает этот порог (значение: %)\"\n\"timeZone\" = \"Часовой пояс\"\n\"timeZoneDesc\" = \"Запланированные задачи выполняются в соответствии со временем в этом часовом поясе\"\n\"subSettings\" = \"Подписка\"\n\"subEnable\" = \"Включить подписку\"\n\"subEnableDesc\" = \"Функция подписки с отдельной конфигурацией\"\n\"subJsonEnable\" = \"Включить/отключить JSON-эндпоинт подписки независимо.\"\n\"subTitle\" = \"Заголовок подписки\"\n\"subTitleDesc\" = \"Название подписки, которое видит клиент в VPN-клиенте\"\n\"subSupportUrl\" = \"URL поддержки\"\n\"subSupportUrlDesc\" = \"Ссылка на техническую поддержку, отображаемая в VPN-клиенте\"\n\"subProfileUrl\" = \"URL профиля\"\n\"subProfileUrlDesc\" = \"Ссылка на ваш сайт, отображаемая в VPN-клиенте\"\n\"subAnnounce\" = \"Объявление\"\n\"subAnnounceDesc\" = \"Текст объявления, отображаемый в VPN-клиенте\"\n\"subEnableRouting\" = \"Включить маршрутизацию\"\n\"subEnableRoutingDesc\" = \"Глобальная настройка для включения маршрутизации в VPN-клиенте. (Только для Happ)\"\n\"subRoutingRules\" = \"Правила маршрутизации\"\n\"subRoutingRulesDesc\" = \"Глобальные правила маршрутизации для VPN-клиента. (Только для Happ)\"\n\"subListen\" = \"Прослушивание IP\"\n\"subListenDesc\" = \"Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса\"\n\"subPort\" = \"Порт подписки\"\n\"subPortDesc\" = \"Номер порта для обслуживания службы подписки не должен использоваться на сервере\"\n\"subCertPath\" = \"Путь к файлу публичного ключа сертификата подписки\"\n\"subCertPathDesc\" = \"Введите полный путь, начинающийся с '/'\"\n\"subKeyPath\" = \"Путь к файлу приватного ключа сертификата подписки\"\n\"subKeyPathDesc\" = \"Введите полный путь, начинающийся с '/'\"\n\"subPath\" = \"Корневой путь URL-адреса подписки\"\n\"subPathDesc\" = \"Должен начинаться с '/' и заканчиваться на '/'\"\n\"subDomain\" = \"Домен прослушивания\"\n\"subDomainDesc\" = \"Оставьте пустым по умолчанию, чтобы слушать все домены и IP-адреса\"\n\"subUpdates\" = \"Интервалы обновления подписки\"\n\"subUpdatesDesc\" = \"Интервал между обновлениями в клиентском приложении (в часах)\"\n\"subEncrypt\" = \"Шифровать конфиги\"\n\"subEncryptDesc\" = \"Шифровать возвращенные конфиги в подписке\"\n\"subShowInfo\" = \"Показать информацию об использовании\"\n\"subShowInfoDesc\" = \"Отображать остаток трафика и дату окончания после имени конфигурации\"\n\"subURI\" = \"URI обратного прокси\"\n\"subURIDesc\" = \"Изменить базовый URI URL-адреса подписки для использования за прокси-серверами\"\n\"externalTrafficInformEnable\" = \"Информация о внешнем трафике\"\n\"externalTrafficInformEnableDesc\" = \"Информировать внешний API о каждом обновлении трафика\"\n\"externalTrafficInformURI\" = \"URI информации о внешнем трафике\"\n\"externalTrafficInformURIDesc\" = \"Обновления трафика отправляются на этот URI\"\n\"fragment\" = \"Фрагментация\"\n\"fragmentDesc\" = \"Включить фрагментацию TLS-хэндшейка\"\n\"fragmentSett\" = \"Настройки фрагментации\"\n\"noisesDesc\" = \"Включить Noises.\"\n\"noisesSett\" = \"Настройки Noises\"\n\"mux\" = \"Mux\"\n\"muxDesc\" = \"Передача нескольких независимых потоков данных в одном соединении.\"\n\"muxSett\" = \"Настройки Mux\"\n\"direct\" = \"Прямое подключение\"\n\"directDesc\" = \"Устанавливает прямые соединения с доменами или IP-адресами определённой страны.\"\n\"notifications\" = \"Уведомления\"\n\"certs\" = \"Сертификаты\"\n\"externalTraffic\" = \"Внешний трафик\"\n\"dateAndTime\" = \"Дата и время\"\n\"proxyAndServer\" = \"Прокси и сервер\"\n\"intervals\" = \"Интервалы\"\n\"information\" = \"Информация\"\n\"language\" = \"Язык интерфейса\"\n\"telegramBotLanguage\" = \"Язык Telegram-бота\"\n\n[pages.xray]\n\"title\" = \"Настройки Xray\"\n\"save\" = \"Сохранить\"\n\"restart\" = \"Перезапуск Xray\"\n\"restartSuccess\" = \"Xray успешно перезапущен\"\n\"stopSuccess\" = \"Xray успешно остановлен\"\n\"restartError\" = \"Произошла ошибка при перезапуске Xray.\"\n\"stopError\" = \"Произошла ошибка при остановке Xray.\"\n\"basicTemplate\" = \"Основное\"\n\"advancedTemplate\" = \"Расширенный шаблон\"\n\"generalConfigs\" = \"Основные настройки\"\n\"generalConfigsDesc\" = \"Эти параметры описывают общие настройки\"\n\"logConfigs\" = \"Логи\"\n\"logConfigsDesc\" = \"Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!\"\n\"blockConfigsDesc\" = \"Настройте, чтобы клиенты не имели доступа к определенным протоколам\"\n\"basicRouting\" = \"Базовые соединения\"\n\"blockConnectionsConfigsDesc\" = \"Эти параметры будут блокировать трафик в зависимости от страны назначения.\"\n\"directConnectionsConfigsDesc\" = \"Прямое соединение означает, что определенный трафик не будет перенаправлен через другой сервер.\"\n\"blockips\" = \"Заблокированные IP-адреса\"\n\"blockdomains\" = \"Заблокированные домены\"\n\"directips\" = \"Прямые IP-адреса\"\n\"directdomains\" = \"Прямые домены\"\n\"ipv4Routing\" = \"Правила IPv4\"\n\"ipv4RoutingDesc\" = \"Эти параметры позволят клиентам маршрутизироваться к целевым доменам только через IPv4\"\n\"warpRouting\" = \"Правила WARP\"\n\"warpRoutingDesc\" = \" Эти опции будут направлять трафик в зависимости от конкретного пункта назначения через WARP.\"\n\"Template\" = \"Шаблон конфигурации Xray\"\n\"TemplateDesc\" = \"На основе шаблона создаётся конфигурационный файл Xray.\"\n\"FreedomStrategy\" = \"Настройка стратегии протокола Freedom\"\n\"FreedomStrategyDesc\" = \"Установка стратегии вывода сети в протоколе Freedom\"\n\"RoutingStrategy\" = \"Настройка маршрутизации доменов\"\n\"RoutingStrategyDesc\" = \"Установка общей стратегии маршрутизации разрешения DNS\"\n\"outboundTestUrl\" = \"URL для теста исходящего\"\n\"outboundTestUrlDesc\" = \"URL для проверки подключения исходящего\"\n\"Torrent\" = \"Заблокировать BitTorrent\"\n\"Inbounds\" = \"Входящие подключения\"\n\"InboundsDesc\" = \"Изменение шаблона конфигурации для подключения определенных клиентов\"\n\"Outbounds\" = \"Исходящие подключения\"\n\"Balancers\" = \"Балансировщик\"\n\"OutboundsDesc\" = \"Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера\"\n\"Routings\" = \"Маршрутизация\"\n\"RoutingsDesc\" = \"Важен приоритет каждого правила!\"\n\"completeTemplate\" = \"Все\"\n\"logLevel\" = \"Уровень логов\"\n\"logLevelDesc\" = \"Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать.\"\n\"accessLog\" = \"Логи доступа\"\n\"accessLogDesc\" = \"Путь к файлу журнала доступа. Специальное значение «none» отключает логи доступа.\"\n\"errorLog\" = \"Логи ошибок\"\n\"errorLogDesc\" = \"Путь к файлу логов ошибок. Специальное значение «none» отключает логи ошибок.\"\n\"dnsLog\" = \"Логи DNS\"\n\"dnsLogDesc\" = \"Включить логи запросов DNS\"\n\"maskAddress\" = \"Маскировка адреса\"\n\"maskAddressDesc\" = \"При активации реальный IP-адрес заменяется на маскировочный в логах.\"\n\"statistics\" = \"Статистика\"\n\"statsInboundUplink\" = \"Статистика входящего аплинка\"\n\"statsInboundUplinkDesc\" = \"Включает сбор статистики для исходящего трафика всех входящих прокси.\"\n\"statsInboundDownlink\" = \"Статистика входящего даунлинка\"\n\"statsInboundDownlinkDesc\" = \"Включает сбор статистики для входящего трафика всех входящих прокси.\"\n\"statsOutboundUplink\" = \"Статистика исходящего аплинка\"\n\"statsOutboundUplinkDesc\" = \"Включает сбор статистики для исходящего трафика всех исходящих прокси.\"\n\"statsOutboundDownlink\" = \"Статистика исходящего даунлинка\"\n\"statsOutboundDownlinkDesc\" = \"Включает сбор статистики для входящего трафика всех исходящих прокси.\"\n\n[pages.xray.rules]\n\"first\" = \"Первый\"\n\"last\" = \"Последний\"\n\"up\" = \"Поднять вверх\"\n\"down\" = \"Опустить вниз\"\n\"source\" = \"Источник\"\n\"dest\" = \"Пункт назначения\"\n\"inbound\" = \"Входящее подключение\"\n\"outbound\" = \"Исходящее подключение\"\n\"balancer\" = \"Балансировщик\"\n\"info\" = \"Информация\"\n\"add\" = \"Создать правило\"\n\"edit\" = \"Редактировать правило\"\n\"useComma\" = \"Элементы, разделённые запятыми\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"Создать исходящее подключение\"\n\"addReverse\" = \"Создать реверс-прокси\"\n\"editOutbound\" = \"Изменить исходящее подключение\"\n\"editReverse\" = \"Редактировать реверс-прокси\"\n\"tag\" = \"Тег\"\n\"tagDesc\" = \"Уникальный тег\"\n\"address\" = \"Адрес\"\n\"reverse\" = \"Реверс-прокси\"\n\"domain\" = \"Домен\"\n\"type\" = \"Тип\"\n\"bridge\" = \"Мост\"\n\"portal\" = \"Портал\"\n\"link\" = \"Ссылка\"\n\"intercon\" = \"Соединение\"\n\"settings\" = \"Настройки\"\n\"accountInfo\" = \"Информация об учетной записи\"\n\"outboundStatus\" = \"Статус исходящего подключения\"\n\"sendThrough\" = \"Отправить через\"\n\"test\" = \"Тест\"\n\"testResult\" = \"Результат теста\"\n\"testing\" = \"Тестирование соединения...\"\n\"testSuccess\" = \"Тест успешен\"\n\"testFailed\" = \"Тест не пройден\"\n\"testError\" = \"Не удалось протестировать исходящее подключение\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"Создать балансировщик\"\n\"editBalancer\" = \"Редактировать балансировщик\"\n\"balancerStrategy\" = \"Стратегия\"\n\"balancerSelectors\" = \"Селекторы\"\n\"tag\" = \"Тег\"\n\"tagDesc\" = \"Уникальный тег\"\n\"balancerDesc\" = \"Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag.\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"Секретный ключ\"\n\"publicKey\" = \"Публичный ключ\"\n\"allowedIPs\" = \"Разрешенные IP-адреса\"\n\"endpoint\" = \"Конечная точка\"\n\"psk\" = \"Общий ключ\"\n\"domainStrategy\" = \"Стратегия домена\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"Имя интерфейса TUN. Значение по умолчанию - 'xray0'\"\n\"mtuDesc\" = \"Максимальная единица передачи. Максимальный размер пакетов данных. Значение по умолчанию - 1500\"\n\"userLevel\" = \"Уровень пользователя\"\n\"userLevelDesc\" = \"Все соединения, установленные через этот входящий поток, будут использовать этот уровень пользователя. Значение по умолчанию - 0\"\n\n[pages.xray.dns]\n\"enable\" = \"Включить DNS\"\n\"enableDesc\" = \"Включить встроенный DNS-сервер\"\n\"tag\" = \"Название тега DNS\"\n\"tagDesc\" = \"Этот тег будет доступен как входящий тег в правилах маршрутизации.\"\n\"clientIp\" = \"IP клиента\"\n\"clientIpDesc\" = \"Используется для уведомления сервера о указанном местоположении IP во время DNS-запросов\"\n\"disableCache\" = \"Отключить кэш\"\n\"disableCacheDesc\" = \"Отключает кэширование DNS\"\n\"disableFallback\" = \"Отключить резервный DNS\"\n\"disableFallbackDesc\" = \"Отключает резервные DNS-запросы\"\n\"disableFallbackIfMatch\" = \"Отключить резервный DNS при совпадении\"\n\"disableFallbackIfMatchDesc\" = \"Отключает резервные DNS-запросы при совпадении списка доменов DNS-сервера\"\n\"enableParallelQuery\" = \"Включить параллельные запросы\"\n\"enableParallelQueryDesc\" = \"Включить параллельные DNS-запросы к нескольким серверам для более быстрого разрешения\"\n\"strategy\" = \"Стратегия запроса\"\n\"strategyDesc\" = \"Общая стратегия разрешения доменных имен\"\n\"add\" = \"Создать DNS\"\n\"edit\" = \"Редактировать DNS\"\n\"domains\" = \"Домены\"\n\"expectIPs\" = \"Ожидаемые IP\"\n\"unexpectIPs\" = \"Неожидаемые IP\"\n\"useSystemHosts\" = \"Использовать системные Hosts\"\n\"useSystemHostsDesc\" = \"Использовать файл hosts из установленной системы\"\n\"usePreset\" = \"Использовать шаблон\"\n\"dnsPresetTitle\" = \"Шаблоны DNS\"\n\"dnsPresetFamily\" = \"Семейный\"\n\n[pages.xray.fakedns]\n\"add\" = \"Создать Fake DNS\"\n\"edit\" = \"Редактировать Fake DNS\"\n\"ipPool\" = \"Подсеть пула IP\"\n\"poolSize\" = \"Размер пула\"\n\n[pages.settings.security]\n\"admin\" = \"Учетные данные администратора\"\n\"twoFactor\" = \"Двухфакторная аутентификация\"\n\"twoFactorEnable\" = \"Включить 2FA\"\n\"twoFactorEnableDesc\" = \"Добавляет дополнительный уровень аутентификации для повышения безопасности.\"\n\"twoFactorModalSetTitle\" = \"Включить двухфакторную аутентификацию\"\n\"twoFactorModalDeleteTitle\" = \"Отключить двухфакторную аутентификацию\"\n\"twoFactorModalSteps\" = \"Для настройки двухфакторной аутентификации выполните несколько шагов:\"\n\"twoFactorModalFirstStep\" = \"1. Отсканируйте этот QR-код в приложении для аутентификации или скопируйте токен рядом с QR-кодом и вставьте его в приложение\"\n\"twoFactorModalSecondStep\" = \"2. Введите код из приложения\"\n\"twoFactorModalRemoveStep\" = \"Введите код из приложения, чтобы отключить двухфакторную аутентификацию.\"\n\"twoFactorModalChangeCredentialsTitle\" = \"Изменить учетные данные\"\n\"twoFactorModalChangeCredentialsStep\" = \"Введите код из приложения, чтобы изменить учетные данные администратора.\"\n\"twoFactorModalSetSuccess\" = \"Двухфакторная аутентификация была успешно установлена\"\n\"twoFactorModalDeleteSuccess\" = \"Двухфакторная аутентификация была успешно удалена\"\n\"twoFactorModalError\" = \"Неверный код\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"Настройки изменены\"\n\"getSettings\" = \"Произошла ошибка при получении параметров.\"\n\"modifyUserError\" = \"Произошла ошибка при изменении учетных данных администратора.\"\n\"modifyUser\" = \"Вы успешно изменили учетные данные администратора.\"\n\"originalUserPassIncorrect\" = \"Неверное имя пользователя или пароль\"\n\"userPassMustBeNotEmpty\" = \"Новое имя пользователя и новый пароль должны быть заполнены\"\n\"getOutboundTrafficError\" = \"Ошибка получения трафика исходящего подключения\"\n\"resetOutboundTrafficError\" = \"Ошибка сброса трафика исходящего подключения\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ Клавиатура закрыта.\"\n\"noResult\" = \"❗ Нет результатов.\"\n\"noQuery\" = \"❌ Запрос не найден. Пожалуйста, повторите команду.\"\n\"wentWrong\" = \"❌ Что-то пошло не так...\"\n\"noIpRecord\" = \"❗ Нет записей об IP-адресе.\"\n\"noInbounds\" = \"❗ У вас не настроено ни одного входящего подключения.\"\n\"unlimited\" = \"♾ Безлимит\"\n\"add\" = \"Добавить\"\n\"month\" = \"Месяц\"\n\"months\" = \"Месяцев\"\n\"day\" = \"День\"\n\"days\" = \"Дней\"\n\"hours\" = \"Часов\"\n\"minutes\" = \"Минуты\"\n\"unknown\" = \"Неизвестно\"\n\"inbounds\" = \"Входящие подключения\"\n\"clients\" = \"Клиенты\"\n\"offline\" = \"🔴 Офлайн\"\n\"online\" = \"🟢 Онлайн\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ Неизвестная команда\"\n\"pleaseChoose\" = \"👇 Пожалуйста, выберите:\\r\\n\"\n\"help\" = \"🤖 Добро пожаловать! Этот бот предназначен для предоставления вам данных с сервера и позволяет вносить изменения на него.\\r\\n\\r\\n\"\n\"start\" = \"👋 Привет, <i>{{ .Firstname }}</i>.\\r\\n\"\n\"welcome\" = \"🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>!\\r\\n\"\n\"status\" = \"✅ Бот функционирует нормально.\"\n\"usage\" = \"❗ Пожалуйста, укажите email для поиска.\"\n\"getID\" = \"🆔 Ваш User ID: <code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"🔃 Для перезапуска Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\n🔎 Для поиска клиента по email:\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\n📊 Для поиска входящих подключений (со статистикой клиентов):\\r\\n<code>/inbound [имя подключения]</code>\\r\\n\\r\\n🆔 Ваш Telegram User ID:\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"💲 Для просмотра информации о вашей подписке используйте команду:\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\n🆔 Ваш Telegram User ID:\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ Ядро Xray успешно перезапущено.\"\n\"restartFailed\" = \"❗ Ошибка при перезапуске Xray-core.\\r\\n\\r\\n<code>Ошибка: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core не запущен.\"\n\"startDesc\" = \"Показать главное меню\"\n\"helpDesc\" = \"Справка по боту\"\n\"statusDesc\" = \"Проверить статус бота\"\n\"idDesc\" = \"Показать ваш Telegram ID\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%\"\n\"selectUserFailed\" = \"❌ Ошибка при выборе пользователя.\"\n\"userSaved\" = \"✅ Пользователь Telegram сохранен.\"\n\"loginSuccess\" = \"✅ Успешный вход в панель.\\r\\n\"\n\"loginFailed\" = \"❗️ Ошибка входа в панель.\\r\\n\"\n\"2faFailed\" = \"Ошибка 2FA\"\n\"report\" = \"🕰 Запланированные отчеты: {{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ Дата и время: {{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 Имя хоста: {{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 Версия X-UI: {{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Версия Xray: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IP-адреса:\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 ОЗУ сервера: {{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 Количество TCP-соединений: {{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 Количество UDP-соединений: {{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Состояние Xray: {{ .State }}\\r\\n\"\n\"username\" = \"👤 Имя пользователя: {{ .Username }}\\r\\n\"\n\"password\" = \"👤 Пароль: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ Время: {{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 Входящее подключение: {{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 Порт: {{ .Port }}\\r\\n\"\n\"expire\" = \"📅 Дата окончания: {{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 Окончание через: {{ .Time }}\\r\\n\"\n\"active\" = \"💡 Активен: {{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 Активен: {{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 Статус соединения: {{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 Был(а) в сети: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 Email: {{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 Исходящий трафик: ↑{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 Входящий трафик: ↓{{ .Download }}\\r\\n\"\n\"total\" = \"📊 Всего: ↑↓{{ .UpDown }} из {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 Telegram User ID: {{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 Исчерпаны {{ .Type }}:\\r\\n\"\n\"exhaustedCount\" = \"🚨 Количество исчерпанных {{ .Type }}:\\r\\n\"\n\"onlinesCount\" = \"🌐 Клиентов онлайн: {{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 Отключено: {{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 Клиенты, у которых скоро исчерпание: {{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 Время резервного копирования: {{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 Обновлено: {{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ Да\"\n\"no\" = \"❌ Нет\"\n\"received_id\" = \"🔑📥 ID обновлён.\"\n\"received_password\" = \"🔑📥 Пароль обновлён.\"\n\"received_email\" = \"📧📥 Email обновлен.\"\n\"received_comment\" = \"💬📥 Комментарий обновлён.\"\n\"id_prompt\" = \"🔑 Стандартный ID: {{ .ClientId }}\\n\\nВведите ваш ID.\"\n\"pass_prompt\" = \"🔑 Стандартный пароль: {{ .ClientPassword }}\\n\\nВведите ваш пароль.\"\n\"email_prompt\" = \"📧 Стандартный email: {{ .ClientEmail }}\\n\\nВведите ваш email.\"\n\"comment_prompt\" = \"💬 Стандартный комментарий: {{ .ClientComment }}\\n\\nВведите ваш комментарий.\"\n\"inbound_client_data_id\" = \"🔄 Входящие подключения: {{ .InboundRemark }}\\n\\n🔑 ID: {{ .ClientId }}\\n📧 Email: {{ .ClientEmail }}\\n📊 Трафик: {{ .ClientTraffic }}\\n📅 Срок действия: {{ .ClientExp }}\\n💬 Комментарий: {{ .ClientComment }}\\n\\nТеперь вы можете добавить клиента в входящее подключение!\"\n\"inbound_client_data_pass\" = \"🔄 Входящие подключения: {{ .InboundRemark }}\\n\\n🔑 Пароль: {{ .ClientPass }}\\n📧 Email: {{ .ClientEmail }}\\n📊 Трафик: {{ .ClientTraffic }}\\n📅 Срок действия: {{ .ClientExp }}\\n💬 Комментарий: {{ .ClientComment }}\\n\\nТеперь вы можете добавить клиента в входящее подключение!\"\n\"cancel\" = \"❌ Процесс отменён! \\n\\nВы можете снова начать с /start в любое время. 🔄\"\n\"error_add_client\" = \"⚠️ Ошибка:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"Используется значение по умолчанию👌\"\n\"incorrect_input\" = \"Ваш ввод недействителен.\\nФразы должны быть непрерывными без пробелов.\\nПравильный пример: aaaaaa\\nНеправильный пример: aaa aaa 🚫\"\n\"AreYouSure\" = \"Вы уверены? 🤔\"\n\"SuccessResetTraffic\" = \"📧 Почта: {{ .ClientEmail }}\\n🏁 Результат: ✅ Успешно\"\n\"FailedResetTraffic\" = \"📧 Почта: {{ .ClientEmail }}\\n🏁 Результат: ❌ Неудача \\n\\n🛠️ Ошибка: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 Сброс трафика завершён для всех клиентов.\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ Закрыть клавиатуру\"\n\"cancel\" = \"❌ Отмена\"\n\"cancelReset\" = \"❌ Отменить сброс\"\n\"cancelIpLimit\" = \"❌ Отменить лимит IP\"\n\"confirmResetTraffic\" = \"✅ Подтвердить сброс трафика?\"\n\"confirmClearIps\" = \"✅ Подтвердить очистку IP?\"\n\"confirmRemoveTGUser\" = \"✅ Подтвердить удаление пользователя Telegram?\"\n\"confirmToggle\" = \"✅ Подтвердить вкл/выкл пользователя?\"\n\"dbBackup\" = \"📂 Бэкап БД\"\n\"serverUsage\" = \"💻 Состояние сервера\"\n\"getInbounds\" = \"🔌 Входящие подключения\"\n\"depleteSoon\" = \"⚠️ Скоро конец\"\n\"clientUsage\" = \"Статистика клиента\"\n\"onlines\" = \"🟢 Онлайн\"\n\"commands\" = \"🖱️ Команды\"\n\"refresh\" = \"🔄 Обновить\"\n\"clearIPs\" = \"❌ Очистить IP\"\n\"removeTGUser\" = \"❌ Удалить пользователя Telegram\"\n\"selectTGUser\" = \"👤 Выбрать пользователя Telegram\"\n\"selectOneTGUser\" = \"👤 Выберите пользователя Telegram:\"\n\"resetTraffic\" = \"📈 Сбросить трафик\"\n\"resetExpire\" = \"📅 Изменить дату окончания\"\n\"ipLog\" = \"🔢 Лог IP\"\n\"ipLimit\" = \"🔢 Лимит IP\"\n\"setTGUser\" = \"👤 Установить пользователя Telegram\"\n\"toggle\" = \"🔘 Вкл./Выкл.\"\n\"custom\" = \"🔢 Свой\"\n\"confirmNumber\" = \"✅ Подтвердить: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ Подтвердить добавление: {{ .Num }}\"\n\"limitTraffic\" = \"🚧 Лимит трафика\"\n\"getBanLogs\" = \"📄 Лог банов\"\n\"allClients\" = \"👥 Все клиенты\"\n\"addClient\" = \"➕ Новый клиент\"\n\"submitDisable\" = \"Добавить отключенным ☑️\"\n\"submitEnable\" = \"Добавить включенным ✅\"\n\"use_default\" = \"🏷️ Использовать по умолчанию\"\n\"change_id\" = \"⚙️🔑 ID\"\n\"change_password\" = \"⚙️🔑 Пароль\"\n\"change_email\" = \"⚙️📧 Email\"\n\"change_comment\" = \"⚙️💬 Комментарий\"\n\"ResetAllTraffics\" = \"Сбросить весь трафик\"\n\"SortedTrafficUsageReport\" = \"Отсортированный отчет об использовании трафика\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ Успешно!\"\n\"errorOperation\" = \"❗ Ошибка в операции.\"\n\"getInboundsFailed\" = \"❌ Не удалось получить входящие подключения.\"\n\"getClientsFailed\" = \"❌ Не удалось получить клиентов.\"\n\"canceled\" = \"❌ {{ .Email }}: Операция отменена.\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}: Клиент успешно обновлен.\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}: IP-адреса успешно обновлены.\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен.\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}: Трафик успешно сброшен.\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: Лимит трафика успешно установлен.\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}: Срок действия успешно сброшен.\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен.\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}: IP-адреса успешно очищены.\"\n\"getIpLog\" = \"✅ {{ .Email }}: Получен лог IP.\"\n\"getUserInfo\" = \"✅ {{ .Email }}: Получена информация о пользователе Telegram.\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}: Пользователь Telegram успешно удален.\"\n\"enableSuccess\" = \"✅ {{ .Email }}: Включено успешно.\"\n\"disableSuccess\" = \"✅ {{ .Email }}: Отключено успешно.\"\n\"askToAddUserId\" = \"❌ Ваша конфигурация не найдена!\\r\\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\\r\\n\\r\\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"Выберите клиента для входящего подключения {{ .Inbound }}\"\n\"chooseInbound\" = \"Выберите входящее подключение\"\n"
  },
  {
    "path": "web/translation/translate.tr_TR.toml",
    "content": "\"username\" = \"Kullanıcı Adı\"\n\"password\" = \"Şifre\"\n\"login\" = \"Giriş Yap\"\n\"confirm\" = \"Onayla\"\n\"cancel\" = \"İptal\"\n\"close\" = \"Kapat\"\n\"create\" = \"Oluştur\"\n\"update\" = \"Güncelle\"\n\"copy\" = \"Kopyala\"\n\"copied\" = \"Kopyalandı\"\n\"download\" = \"İndir\"\n\"remark\" = \"Açıklama\"\n\"enable\" = \"Etkin\"\n\"protocol\" = \"Protokol\"\n\"search\" = \"Ara\"\n\"filter\" = \"Filtrele\"\n\"loading\" = \"Yükleniyor...\"\n\"second\" = \"Saniye\"\n\"minute\" = \"Dakika\"\n\"hour\" = \"Saat\"\n\"day\" = \"Gün\"\n\"check\" = \"Kontrol Et\"\n\"indefinite\" = \"Belirsiz\"\n\"unlimited\" = \"Sınırsız\"\n\"none\" = \"Hiçbiri\"\n\"qrCode\" = \"QR Kod\"\n\"info\" = \"Daha Fazla Bilgi\"\n\"edit\" = \"Düzenle\"\n\"delete\" = \"Sil\"\n\"reset\" = \"Sıfırla\"\n\"noData\" = \"Veri yok.\"\n\"copySuccess\" = \"Başarıyla Kopyalandı\"\n\"sure\" = \"Emin misiniz\"\n\"encryption\" = \"Şifreleme\"\n\"useIPv4ForHost\" = \"Ana bilgisayar için IPv4 kullan\"\n\"transmission\" = \"İletim\"\n\"host\" = \"Sunucu\"\n\"path\" = \"Yol\"\n\"camouflage\" = \"Kandırma\"\n\"status\" = \"Durum\"\n\"enabled\" = \"Etkin\"\n\"disabled\" = \"Devre Dışı\"\n\"depleted\" = \"Bitti\"\n\"depletingSoon\" = \"Bitmek Üzere\"\n\"offline\" = \"Çevrimdışı\"\n\"online\" = \"Çevrimiçi\"\n\"domainName\" = \"Alan Adı\"\n\"monitor\" = \"Dinleme IP\"\n\"certificate\" = \"Dijital Sertifika\"\n\"fail\" = \"Başarısız\"\n\"comment\" = \"Yorum\"\n\"success\" = \"Başarılı\"\n\"lastOnline\" = \"Son çevrimiçi\"\n\"getVersion\" = \"Sürümü Al\"\n\"install\" = \"Yükle\"\n\"clients\" = \"Müşteriler\"\n\"usage\" = \"Kullanım\"\n\"twoFactorCode\" = \"Kod\"\n\"remained\" = \"Kalan\"\n\"security\" = \"Güvenlik\"\n\"secAlertTitle\" = \"Güvenlik Uyarısı\"\n\"secAlertSsl\" = \"Bu bağlantı güvenli değil. Verilerin korunması için TLS etkinleştirilene kadar hassas bilgiler girmekten kaçının.\"\n\"secAlertConf\" = \"Bazı ayarlar saldırılara açıktır. Olası ihlalleri önlemek için güvenlik protokollerini güçlendirmeniz önerilir.\"\n\"secAlertSSL\" = \"Panelde güvenli bağlantı yok. Verilerin korunması için TLS sertifikası yükleyin.\"\n\"secAlertPanelPort\" = \"Panel varsayılan portu savunmasız. Rastgele veya belirli bir port yapılandırın.\"\n\"secAlertPanelURI\" = \"Panel varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın.\"\n\"secAlertSubURI\" = \"Abonelik varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın.\"\n\"secAlertSubJsonURI\" = \"Abonelik JSON varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın.\"\n\"emptyDnsDesc\" = \"Eklenmiş DNS sunucusu yok.\"\n\"emptyFakeDnsDesc\" = \"Eklenmiş Fake DNS sunucusu yok.\"\n\"emptyBalancersDesc\" = \"Eklenmiş dengeleyici yok.\"\n\"emptyReverseDesc\" = \"Eklenmiş ters proxy yok.\"\n\"somethingWentWrong\" = \"Bir şeyler yanlış gitti\"\n\n[subscription]\n\"title\" = \"Abonelik Bilgisi\"\n\"subId\" = \"Abonelik Kimliği\"\n\"status\" = \"Durum\"\n\"downloaded\" = \"İndirilen\"\n\"uploaded\" = \"Yüklenen\"\n\"expiry\" = \"Son Kullanma\"\n\"totalQuota\" = \"Toplam Kota\"\n\"individualLinks\" = \"Bireysel Bağlantılar\"\n\"active\" = \"Aktif\"\n\"inactive\" = \"Pasif\"\n\"unlimited\" = \"Sınırsız\"\n\"noExpiry\" = \"Süresiz\"\n\n[menu]\n\"theme\" = \"Tema\"\n\"dark\" = \"Koyu\"\n\"ultraDark\" = \"Ultra Koyu\"\n\"dashboard\" = \"Genel Bakış\"\n\"inbounds\" = \"Gelenler\"\n\"settings\" = \"Panel Ayarları\"\n\"xray\" = \"Xray Yapılandırmaları\"\n\"logout\" = \"Çıkış Yap\"\n\"link\" = \"Yönet\"\n\n[pages.login]\n\"hello\" = \"Merhaba\"\n\"title\" = \"Hoş Geldiniz\"\n\"loginAgain\" = \"Oturum süreniz doldu, lütfen tekrar giriş yapın\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"Girdi verisi formatı geçersiz.\"\n\"emptyUsername\" = \"Kullanıcı adı gerekli\"\n\"emptyPassword\" = \"Şifre gerekli\"\n\"wrongUsernameOrPassword\" = \"Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu.\"\n\"successLogin\" = \"Hesabınıza başarıyla giriş yaptınız.\"\n\n[pages.index]\n\"title\" = \"Genel Bakış\"\n\"cpu\" = \"İşlemci\"\n\"logicalProcessors\" = \"Mantıksal işlemciler\"\n\"frequency\" = \"Frekans\"\n\"swap\" = \"Takas\"\n\"storage\" = \"Depolama\"\n\"memory\" = \"RAM\"\n\"threads\" = \"İş parçacıkları\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"Durdur\"\n\"restartXray\" = \"Yeniden Başlat\"\n\"xraySwitch\" = \"Sürüm\"\n\"xraySwitchClick\" = \"Geçiş yapmak istediğiniz sürümü seçin.\"\n\"xraySwitchClickDesk\" = \"Dikkatli seçin, eski sürümler mevcut yapılandırmalarla uyumlu olmayabilir.\"\n\"xrayStatusUnknown\" = \"Bilinmiyor\"\n\"xrayStatusRunning\" = \"Çalışıyor\"\n\"xrayStatusStop\" = \"Durduruldu\"\n\"xrayStatusError\" = \"Hata\"\n\"xrayErrorPopoverTitle\" = \"Xray çalıştırılırken bir hata oluştu\"\n\"operationHours\" = \"Çalışma Süresi\"\n\"systemLoad\" = \"Sistem Yükü\"\n\"systemLoadDesc\" = \"Geçmiş 1, 5 ve 15 dakika için sistem yük ortalaması\"\n\"connectionCount\" = \"Bağlantı İstatistikleri\"\n\"ipAddresses\" = \"IP adresleri\"\n\"toggleIpVisibility\" = \"IP görünürlüğünü değiştir\"\n\"overallSpeed\" = \"Genel hız\"\n\"upload\" = \"Yükleme\"\n\"download\" = \"İndirme\"\n\"totalData\" = \"Toplam veri\"\n\"sent\" = \"Gönderilen\"\n\"received\" = \"Alınan\"\n\"documentation\" = \"Dokümantasyon\"\n\"xraySwitchVersionDialog\" = \"Xray sürümünü gerçekten değiştirmek istiyor musunuz?\"\n\"xraySwitchVersionDialogDesc\" = \"Bu işlem Xray sürümünü #version# olarak değiştirecektir.\"\n\"xraySwitchVersionPopover\" = \"Xray başarıyla güncellendi\"\n\"geofileUpdateDialog\" = \"Geofile'ı gerçekten güncellemek istiyor musunuz?\"\n\"geofileUpdateDialogDesc\" = \"Bu işlem #filename# dosyasını güncelleyecektir.\"\n\"geofilesUpdateDialogDesc\" = \"Bu, tüm dosyaları güncelleyecektir.\"\n\"geofilesUpdateAll\" = \"Tümünü güncelle\"\n\"geofileUpdatePopover\" = \"Geofile başarıyla güncellendi\"\n\"dontRefresh\" = \"Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin\"\n\"logs\" = \"Günlükler\"\n\"config\" = \"Yapılandırma\"\n\"backup\" = \"Yedek\"\n\"backupTitle\" = \"Veritabanı Yedekleme & Geri Yükleme\"\n\"exportDatabase\" = \"Yedekle\"\n\"exportDatabaseDesc\" = \"Mevcut veritabanınızın yedeğini içeren bir .db dosyasını cihazınıza indirmek için tıklayın.\"\n\"importDatabase\" = \"Geri Yükle\"\n\"importDatabaseDesc\" = \"Cihazınızdan bir .db dosyası seçip yükleyerek veritabanınızı yedekten geri yüklemek için tıklayın.\"\n\"importDatabaseSuccess\" = \"Veritabanı başarıyla içe aktarıldı\"\n\"importDatabaseError\" = \"Veritabanı içe aktarılırken bir hata oluştu\"\n\"readDatabaseError\" = \"Veritabanı okunurken bir hata oluştu\"\n\"getDatabaseError\" = \"Veritabanı alınırken bir hata oluştu\"\n\"getConfigError\" = \"Yapılandırma dosyası alınırken bir hata oluştu\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"Toplam Trafik\"\n\"allTimeTrafficUsage\" = \"Tüm Zamanların Toplam Kullanımı\"\n\"title\" = \"Gelenler\"\n\"totalDownUp\" = \"Toplam Gönderilen/Alınan\"\n\"totalUsage\" = \"Toplam Kullanım\"\n\"inboundCount\" = \"Toplam Gelen\"\n\"operate\" = \"Menü\"\n\"enable\" = \"Etkin\"\n\"remark\" = \"Açıklama\"\n\"protocol\" = \"Protokol\"\n\"port\" = \"Port\"\n\"portMap\" = \"Port Atama\"\n\"traffic\" = \"Trafik\"\n\"details\" = \"Detaylar\"\n\"transportConfig\" = \"Taşıma\"\n\"expireDate\" = \"Süre\"\n\"createdAt\" = \"Oluşturuldu\"\n\"updatedAt\" = \"Güncellendi\"\n\"resetTraffic\" = \"Trafiği Sıfırla\"\n\"addInbound\" = \"Gelen Ekle\"\n\"generalActions\" = \"Genel Eylemler\"\n\"autoRefresh\" = \"Otomatik yenileme\"\n\"autoRefreshInterval\" = \"Aralık\"\n\"modifyInbound\" = \"Geleni Düzenle\"\n\"deleteInbound\" = \"Geleni Sil\"\n\"deleteInboundContent\" = \"Geleni silmek istediğinizden emin misiniz?\"\n\"deleteClient\" = \"Müşteriyi Sil\"\n\"deleteClientContent\" = \"Müşteriyi silmek istediğinizden emin misiniz?\"\n\"resetTrafficContent\" = \"Trafiği sıfırlamak istediğinizden emin misiniz?\"\n\"copyLink\" = \"URL'yi Kopyala\"\n\"address\" = \"Adres\"\n\"network\" = \"Ağ\"\n\"destinationPort\" = \"Hedef Port\"\n\"targetAddress\" = \"Hedef Adres\"\n\"monitorDesc\" = \"Tüm IP'leri dinlemek için boş bırakın\"\n\"meansNoLimit\" = \"= Sınırsız. (birim: GB)\"\n\"totalFlow\" = \"Toplam Akış\"\n\"leaveBlankToNeverExpire\" = \"Hiçbir zaman sona ermemesi için boş bırakın\"\n\"noRecommendKeepDefault\" = \"Varsayılanı korumanız önerilir\"\n\"certificatePath\" = \"Dosya Yolu\"\n\"certificateContent\" = \"Dosya İçeriği\"\n\"publicKey\" = \"Genel Anahtar\"\n\"privatekey\" = \"Özel Anahtar\"\n\"clickOnQRcode\" = \"Kopyalamak için QR Kodu Tıklayın\"\n\"client\" = \"Müşteri\"\n\"export\" = \"Tüm URL'leri Dışa Aktar\"\n\"clone\" = \"Klonla\"\n\"cloneInbound\" = \"Klonla\"\n\"cloneInboundContent\" = \"Bu gelenin tüm ayarları, Port, Dinleme IP ve Müşteriler hariç, klona uygulanacaktır.\"\n\"cloneInboundOk\" = \"Klonla\"\n\"resetAllTraffic\" = \"Tüm Gelen Trafiğini Sıfırla\"\n\"resetAllTrafficTitle\" = \"Tüm Gelen Trafiğini Sıfırla\"\n\"resetAllTrafficContent\" = \"Tüm gelenlerin trafiğini sıfırlamak istediğinizden emin misiniz?\"\n\"resetInboundClientTraffics\" = \"Müşteri Trafiklerini Sıfırla\"\n\"resetInboundClientTrafficTitle\" = \"Müşteri Trafiklerini Sıfırla\"\n\"resetInboundClientTrafficContent\" = \"Bu gelenin müşterilerinin trafiğini sıfırlamak istediğinizden emin misiniz?\"\n\"resetAllClientTraffics\" = \"Tüm Müşteri Trafiklerini Sıfırla\"\n\"resetAllClientTrafficTitle\" = \"Tüm Müşteri Trafiklerini Sıfırla\"\n\"resetAllClientTrafficContent\" = \"Tüm müşterilerin trafiğini sıfırlamak istediğinizden emin misiniz?\"\n\"delDepletedClients\" = \"Bitmiş Müşterileri Sil\"\n\"delDepletedClientsTitle\" = \"Bitmiş Müşterileri Sil\"\n\"delDepletedClientsContent\" = \"Tüm bitmiş müşterileri silmek istediğinizden emin misiniz?\"\n\"email\" = \"E-posta\"\n\"emailDesc\" = \"Lütfen benzersiz bir e-posta adresi sağlayın.\"\n\"IPLimit\" = \"IP Limiti\"\n\"IPLimitDesc\" = \"Sayının aşılması durumunda gelen devre dışı bırakılır. (0 = devre dışı)\"\n\"IPLimitlog\" = \"IP Günlüğü\"\n\"IPLimitlogDesc\" = \"IP geçmiş günlüğü. (devre dışı bırakıldıktan sonra gelini etkinleştirmek için günlüğü temizleyin)\"\n\"IPLimitlogclear\" = \"Günlüğü Temizle\"\n\"setDefaultCert\" = \"Panelden Sertifikayı Ayarla\"\n\"telegramDesc\" = \"Lütfen Telegram Sohbet Kimliği sağlayın. (botta '/id' komutunu kullanın) veya (@userinfobot)\"\n\"subscriptionDesc\" = \"Abonelik URL'inizi bulmak için 'Detaylar'a gidin. Ayrıca, aynı adı birden fazla müşteri için kullanabilirsiniz.\"\n\"info\" = \"Bilgi\"\n\"same\" = \"Aynı\"\n\"inboundData\" = \"Gelenin Verileri\"\n\"exportInbound\" = \"Geleni Dışa Aktar\"\n\"import\" = \"İçe Aktar\"\n\"importInbound\" = \"Bir Gelen İçe Aktar\"\n\"periodicTrafficResetTitle\" = \"Trafik Sıfırlama\"\n\"periodicTrafficResetDesc\" = \"Belirtilen aralıklarla trafik sayacını otomatik olarak sıfırla\"\n\"lastReset\" = \"Son Sıfırlama\"\n\n[pages.client]\n\"add\" = \"Müşteri Ekle\"\n\"edit\" = \"Müşteriyi Düzenle\"\n\"submitAdd\" = \"Müşteri Ekle\"\n\"submitEdit\" = \"Değişiklikleri Kaydet\"\n\"clientCount\" = \"Müşteri Sayısı\"\n\"bulk\" = \"Toplu Ekle\"\n\"method\" = \"Yöntem\"\n\"first\" = \"İlk\"\n\"last\" = \"Son\"\n\"prefix\" = \"Önek\"\n\"postfix\" = \"Sonek\"\n\"delayedStart\" = \"İlk Kullanımdan Sonra Başlat\"\n\"expireDays\" = \"Süre\"\n\"days\" = \"Gün\"\n\"renew\" = \"Otomatik Yenile\"\n\"renewDesc\" = \"Süresi dolduktan sonra otomatik yenileme. (0 = devre dışı)(birim: gün)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"Asla\"\n\"daily\" = \"Günlük\"\n\"weekly\" = \"Haftalık\"\n\"monthly\" = \"Aylık\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"Elde Et\"\n\"updateSuccess\" = \"Güncelleme başarılı oldu\"\n\"logCleanSuccess\" = \"Günlük temizlendi\"\n\"inboundsUpdateSuccess\" = \"Gelen bağlantılar başarıyla güncellendi\"\n\"inboundUpdateSuccess\" = \"Gelen bağlantı başarıyla güncellendi\"\n\"inboundCreateSuccess\" = \"Gelen bağlantı başarıyla oluşturuldu\"\n\"inboundDeleteSuccess\" = \"Gelen bağlantı başarıyla silindi\"\n\"inboundClientAddSuccess\" = \"Gelen bağlantı istemci(leri) eklendi\"\n\"inboundClientDeleteSuccess\" = \"Gelen bağlantı istemcisi silindi\"\n\"inboundClientUpdateSuccess\" = \"Gelen bağlantı istemcisi güncellendi\"\n\"delDepletedClientsSuccess\" = \"Tüm tükenmiş istemciler silindi\"\n\"resetAllClientTrafficSuccess\" = \"İstemcinin tüm trafiği sıfırlandı\"\n\"resetAllTrafficSuccess\" = \"Tüm trafik sıfırlandı\"\n\"resetInboundClientTrafficSuccess\" = \"Trafik sıfırlandı\"\n\"trafficGetError\" = \"Trafik bilgisi alınırken hata oluştu\"\n\"getNewX25519CertError\" = \"X25519 sertifikası alınırken hata oluştu.\"\n\"getNewmldsa65Error\" = \"mldsa65 sertifikası alınırken hata oluştu.\"\n\"getNewVlessEncError\" = \"VlessEnc sertifikası alınırken hata oluştu.\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"İstek\"\n\"response\" = \"Yanıt\"\n\"name\" = \"Ad\"\n\"value\" = \"Değer\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"Sürüm\"\n\"method\" = \"Yöntem\"\n\"path\" = \"Yol\"\n\"status\" = \"Durum\"\n\"statusDescription\" = \"Durum Açıklaması\"\n\"requestHeader\" = \"İstek Başlığı\"\n\"responseHeader\" = \"Yanıt Başlığı\"\n\n[pages.settings]\n\"title\" = \"Panel Ayarları\"\n\"save\" = \"Kaydet\"\n\"infoDesc\" = \"Burada yapılan her değişikliğin kaydedilmesi gerekir. Değişikliklerin uygulanması için paneli yeniden başlatın.\"\n\"restartPanel\" = \"Paneli Yeniden Başlat\"\n\"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.\"\n\"restartPanelSuccess\" = \"Panel başarıyla yeniden başlatıldı\"\n\"actions\" = \"Eylemler\"\n\"resetDefaultConfig\" = \"Varsayılana Sıfırla\"\n\"panelSettings\" = \"Genel\"\n\"securitySettings\" = \"Kimlik Doğrulama\"\n\"TGBotSettings\" = \"Telegram Bot\"\n\"panelListeningIP\" = \"Dinleme IP\"\n\"panelListeningIPDesc\" = \"Web paneli için IP adresi. (tüm IP'leri dinlemek için boş bırakın)\"\n\"panelListeningDomain\" = \"Dinleme Alan Adı\"\n\"panelListeningDomainDesc\" = \"Web paneli için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)\"\n\"panelPort\" = \"Dinleme Portu\"\n\"panelPortDesc\" = \"Web paneli için port numarası. (kullanılmayan bir port olmalıdır)\"\n\"publicKeyPath\" = \"Genel Anahtar Yolu\"\n\"publicKeyPathDesc\" = \"Web paneli için genel anahtar dosya yolu. ('/' ile başlar)\"\n\"privateKeyPath\" = \"Özel Anahtar Yolu\"\n\"privateKeyPathDesc\" = \"Web paneli için özel anahtar dosya yolu. ('/' ile başlar)\"\n\"panelUrlPath\" = \"URI Yolu\"\n\"panelUrlPathDesc\" = \"Web paneli için URI yolu. ('/' ile başlar ve '/' ile biter)\"\n\"pageSize\" = \"Sayfa Boyutu\"\n\"pageSizeDesc\" = \"Gelenler tablosu için sayfa boyutunu belirleyin. (0 = devre dışı)\"\n\"remarkModel\" = \"Açıklama Modeli & Ayırma Karakteri\"\n\"datepicker\" = \"Takvim Türü\"\n\"datepickerPlaceholder\" = \"Tarih Seçin\"\n\"datepickerDescription\" = \"Planlanmış görevler bu takvime göre çalışacaktır.\"\n\"sampleRemark\" = \"Örnek Açıklama\"\n\"oldUsername\" = \"Mevcut Kullanıcı Adı\"\n\"currentPassword\" = \"Mevcut Şifre\"\n\"newUsername\" = \"Yeni Kullanıcı Adı\"\n\"newPassword\" = \"Yeni Şifre\"\n\"telegramBotEnable\" = \"Telegram Botunu Etkinleştir\"\n\"telegramBotEnableDesc\" = \"Telegram botunu etkinleştirir.\"\n\"telegramToken\" = \"Telegram Token\"\n\"telegramTokenDesc\" = \"'@BotFather'dan alınan Telegram bot token.\"\n\"telegramProxy\" = \"SOCKS Proxy\"\n\"telegramProxyDesc\" = \"Telegram'a bağlanmak için SOCKS5 proxy'sini etkinleştirir. (ayarları kılavuzda belirtilen şekilde ayarlayın)\"\n\"telegramAPIServer\" = \"Telegram API Server\"\n\"telegramAPIServerDesc\" = \"Kullanılacak Telegram API sunucusu. Varsayılan sunucuyu kullanmak için boş bırakın.\"\n\"telegramChatId\" = \"Yönetici Sohbet Kimliği\"\n\"telegramChatIdDesc\" = \"Telegram Yönetici Sohbet Kimliği(leri). (virgülle ayrılmış)(buradan alın @userinfobot) veya (botta '/id' komutunu kullanın)\"\n\"telegramNotifyTime\" = \"Bildirim Zamanı\"\n\"telegramNotifyTimeDesc\" = \"Periyodik raporlar için ayarlanan Telegram bot bildirim zamanı. (crontab zaman formatını kullanın)\"\n\"tgNotifyBackup\" = \"Veritabanı Yedeği\"\n\"tgNotifyBackupDesc\" = \"Bir rapor ile birlikte veritabanı yedek dosyasını gönder.\"\n\"tgNotifyLogin\" = \"Giriş Bildirimi\"\n\"tgNotifyLoginDesc\" = \"Birisi web panelinize giriş yapmaya çalıştığında kullanıcı adı, IP adresi ve zaman hakkında bildirim alın.\"\n\"sessionMaxAge\" = \"Oturum Süresi\"\n\"sessionMaxAgeDesc\" = \"Giriş yaptıktan sonra oturum süresi. (birim: dakika)\"\n\"expireTimeDiff\" = \"Son Kullanma Tarihi Bildirimi\"\n\"expireTimeDiffDesc\" = \"Bu eşik seviyesine ulaşıldığında son kullanma tarihi hakkında bildirim alın. (birim: gün)\"\n\"trafficDiff\" = \"Trafik Sınırı Bildirimi\"\n\"trafficDiffDesc\" = \"Bu eşik seviyesine ulaşıldığında trafik sınırı hakkında bildirim alın. (birim: GB)\"\n\"tgNotifyCpu\" = \"CPU Yükü Bildirimi\"\n\"tgNotifyCpuDesc\" = \"CPU yükü bu eşik seviyesini aşarsa bildirim alın. (birim: %)\"\n\"timeZone\" = \"Saat Dilimi\"\n\"timeZoneDesc\" = \"Planlanmış görevler bu saat dilimine göre çalışacaktır.\"\n\"subSettings\" = \"Abonelik\"\n\"subEnable\" = \"Abonelik Hizmetini Etkinleştir\"\n\"subEnableDesc\" = \"Abonelik hizmetini etkinleştirir.\"\n\"subJsonEnable\" = \"JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak.\"\n\"subTitle\" = \"Abonelik Başlığı\"\n\"subTitleDesc\" = \"VPN istemcisinde gösterilen başlık\"\n\"subSupportUrl\" = \"Destek URL'si\"\n\"subSupportUrlDesc\" = \"VPN istemcisinde gösterilen teknik destek bağlantısı\"\n\"subProfileUrl\" = \"Profil URL'si\"\n\"subProfileUrlDesc\" = \"VPN istemcisinde görüntülenen web sitenize giden bağlantı\"\n\"subAnnounce\" = \"Duyuru\"\n\"subAnnounceDesc\" = \"VPN istemcisinde görüntülenen duyuru metni\"\n\"subEnableRouting\" = \"Yönlendirmeyi etkinleştir\"\n\"subEnableRoutingDesc\" = \"VPN istemcisinde yönlendirmeyi etkinleştirmek için genel ayar. (Yalnızca Happ için)\"\n\"subRoutingRules\" = \"Yönlendirme kuralları\"\n\"subRoutingRulesDesc\" = \"VPN istemcisi için genel yönlendirme kuralları. (Yalnızca Happ için)\"\n\"subListen\" = \"Dinleme IP\"\n\"subListenDesc\" = \"Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)\"\n\"subPort\" = \"Dinleme Portu\"\n\"subPortDesc\" = \"Abonelik hizmeti için port numarası. (kullanılmayan bir port olmalıdır)\"\n\"subCertPath\" = \"Genel Anahtar Yolu\"\n\"subCertPathDesc\" = \"Abonelik hizmeti için genel anahtar dosya yolu. ('/' ile başlar)\"\n\"subKeyPath\" = \"Özel Anahtar Yolu\"\n\"subKeyPathDesc\" = \"Abonelik hizmeti için özel anahtar dosya yolu. ('/' ile başlar)\"\n\"subPath\" = \"URI Yolu\"\n\"subPathDesc\" = \"Abonelik hizmeti için URI yolu. ('/' ile başlar ve '/' ile biter)\"\n\"subDomain\" = \"Dinleme Alan Adı\"\n\"subDomainDesc\" = \"Abonelik hizmeti için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)\"\n\"subUpdates\" = \"Güncelleme Aralıkları\"\n\"subUpdatesDesc\" = \"Müşteri uygulamalarındaki abonelik URL'sinin güncelleme aralıkları. (birim: saat)\"\n\"subEncrypt\" = \"Şifrele\"\n\"subEncryptDesc\" = \"Abonelik hizmetinin döndürülen içeriği Base64 ile şifrelenir.\"\n\"subShowInfo\" = \"Kullanım Bilgisini Göster\"\n\"subShowInfoDesc\" = \"Kalan trafik ve tarih müşteri uygulamalarında görüntülenir.\"\n\"subURI\" = \"Ters Proxy URI\"\n\"subURIDesc\" = \"Proxy arkasında kullanılacak abonelik URL'sinin URI yolu.\"\n\"externalTrafficInformEnable\" = \"Harici Trafik Bilgisi\"\n\"externalTrafficInformEnableDesc\" = \"Her trafik güncellemesinde harici API'yi bilgilendirin.\"\n\"externalTrafficInformURI\" = \"Harici Trafik Bilgisi URI'si\"\n\"externalTrafficInformURIDesc\" = \"Trafik güncellemeleri bu URI'ye gönderildi.\"\n\"fragment\" = \"Parçalama\"\n\"fragmentDesc\" = \"TLS merhaba paketinin parçalanmasını etkinleştir.\"\n\"fragmentSett\" = \"Parçalama Ayarları\"\n\"noisesDesc\" = \"Noises'i Etkinleştir.\"\n\"noisesSett\" = \"Noises Ayarları\"\n\"mux\" = \"Mux\"\n\"muxDesc\" = \"Kurulmuş bir veri akışında birden çok bağımsız veri akışını iletir.\"\n\"muxSett\" = \"Mux Ayarları\"\n\"direct\" = \"Doğrudan Bağlantı\"\n\"directDesc\" = \"Belirli bir ülkenin alan adları veya IP aralıkları ile doğrudan bağlantı kurar.\"\n\"notifications\" = \"Bildirimler\"\n\"certs\" = \"Sertifikalar\"\n\"externalTraffic\" = \"Harici Trafik\"\n\"dateAndTime\" = \"Tarih ve Saat\"\n\"proxyAndServer\" = \"Proxy ve Sunucu\"\n\"intervals\" = \"Aralıklar\"\n\"information\" = \"Bilgi\"\n\"language\" = \"Dil\"\n\"telegramBotLanguage\" = \"Telegram Bot Dili\"\n\n[pages.xray]\n\"title\" = \"Xray Yapılandırmaları\"\n\"save\" = \"Kaydet\"\n\"restart\" = \"Xray'i Yeniden Başlat\"\n\"restartSuccess\" = \"Xray başarıyla yeniden başlatıldı\"\n\"stopSuccess\" = \"Xray başarıyla durduruldu\"\n\"restartError\" = \"Xray yeniden başlatılırken bir hata oluştu.\"\n\"stopError\" = \"Xray durdurulurken bir hata oluştu.\"\n\"basicTemplate\" = \"Temeller\"\n\"advancedTemplate\" = \"Gelişmiş\"\n\"generalConfigs\" = \"Genel\"\n\"generalConfigsDesc\" = \"Bu seçenekler genel ayarlamaları belirler.\"\n\"logConfigs\" = \"Günlük\"\n\"logConfigsDesc\" = \"Günlükler sunucunuzun verimliliğini etkileyebilir. Yalnızca ihtiyaç durumunda akıllıca etkinleştirmeniz önerilir\"\n\"blockConfigsDesc\" = \"Bu seçenekler belirli istek protokolleri ve web siteleri temelinde trafiği engeller.\"\n\"basicRouting\" = \"Temel Yönlendirme\"\n\"blockConnectionsConfigsDesc\" = \"Bu seçenekler belirli bir istenen ülkeye göre trafiği engelleyecektir.\"\n\"directConnectionsConfigsDesc\" = \"Doğrudan bağlantı, belirli bir trafiğin başka bir sunucu üzerinden yönlendirilmediğini sağlar.\"\n\"blockips\" = \"IP'leri Engelle\"\n\"blockdomains\" = \"Alan Adlarını Engelle\"\n\"directips\" = \"Doğrudan IP'ler\"\n\"directdomains\" = \"Doğrudan Alan Adları\"\n\"ipv4Routing\" = \"IPv4 Yönlendirme\"\n\"ipv4RoutingDesc\" = \"Bu seçenekler belirli bir varış yerine IPv4 üzerinden trafiği yönlendirir.\"\n\"warpRouting\" = \"WARP Yönlendirme\"\n\"warpRoutingDesc\" = \"Bu seçenekler belirli bir varış yerine WARP üzerinden trafiği yönlendirir.\"\n\"Template\" = \"Gelişmiş Xray Yapılandırma Şablonu\"\n\"TemplateDesc\" = \"Nihai Xray yapılandırma dosyası bu şablona göre oluşturulacaktır.\"\n\"FreedomStrategy\" = \"Freedom Protokol Stratejisi\"\n\"FreedomStrategyDesc\" = \"Freedom Protokolünde ağın çıkış stratejisini ayarlayın.\"\n\"RoutingStrategy\" = \"Genel Yönlendirme Stratejisi\"\n\"RoutingStrategyDesc\" = \"Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın.\"\n\"outboundTestUrl\" = \"Outbound test URL\"\n\"outboundTestUrlDesc\" = \"Outbound bağlantı testinde kullanılan URL\"\n\"Torrent\" = \"BitTorrent Protokolünü Engelle\"\n\"Inbounds\" = \"Gelenler\"\n\"InboundsDesc\" = \"Belirli müşterileri kabul eder.\"\n\"Outbounds\" = \"Gidenler\"\n\"Balancers\" = \"Dengeler\"\n\"OutboundsDesc\" = \"Giden trafiğin yolunu ayarlayın.\"\n\"Routings\" = \"Yönlendirme Kuralları\"\n\"RoutingsDesc\" = \"Her kuralın önceliği önemlidir!\"\n\"completeTemplate\" = \"Tümü\"\n\"logLevel\" = \"Günlük Seviyesi\"\n\"logLevelDesc\" = \"Hata günlükleri için günlük seviyesi, kaydedilmesi gereken bilgileri belirtir.\"\n\"accessLog\" = \"Erişim Günlüğü\"\n\"accessLogDesc\" = \"Erişim günlüğü için dosya yolu. 'none' özel değeri erişim günlüklerini devre dışı bırakır\"\n\"errorLog\" = \"Hata Günlüğü\"\n\"errorLogDesc\" = \"Hata günlüğü için dosya yolu. 'none' özel değeri hata günlüklerini devre dışı bırakır\"\n\"dnsLog\" = \"DNS Günlüğü\"\n\"dnsLogDesc\" = \"DNS sorgu günlüklerini etkinleştirin\"\n\"maskAddress\" = \"Adres Maskesi\"\n\"maskAddressDesc\" = \"IP adresi maskesi, etkinleştirildiğinde, günlükte görünen IP adresini otomatik olarak değiştirecektir.\"\n\"statistics\" = \"İstatistikler\"\n\"statsInboundUplink\" = \"Gelen Yükleme İstatistikleri\"\n\"statsInboundUplinkDesc\" = \"Tüm gelen proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir.\"\n\"statsInboundDownlink\" = \"Gelen İndirme İstatistikleri\"\n\"statsInboundDownlinkDesc\" = \"Tüm gelen proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir.\"\n\"statsOutboundUplink\" = \"Giden Yükleme İstatistikleri\"\n\"statsOutboundUplinkDesc\" = \"Tüm giden proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir.\"\n\"statsOutboundDownlink\" = \"Giden İndirme İstatistikleri\"\n\"statsOutboundDownlinkDesc\" = \"Tüm giden proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir.\"\n\n[pages.xray.rules]\n\"first\" = \"İlk\"\n\"last\" = \"Son\"\n\"up\" = \"Yukarı\"\n\"down\" = \"Aşağı\"\n\"source\" = \"Kaynak\"\n\"dest\" = \"Hedef\"\n\"inbound\" = \"Gelen\"\n\"outbound\" = \"Giden\"\n\"balancer\" = \"Dengeler\"\n\"info\" = \"Bilgi\"\n\"add\" = \"Kural Ekle\"\n\"edit\" = \"Kuralı Düzenle\"\n\"useComma\" = \"Virgülle ayrılmış öğeler\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"Giden Ekle\"\n\"addReverse\" = \"Ters Ekle\"\n\"editOutbound\" = \"Gideni Düzenle\"\n\"editReverse\" = \"Tersi Düzenle\"\n\"tag\" = \"Etiket\"\n\"tagDesc\" = \"Benzersiz Etiket\"\n\"address\" = \"Adres\"\n\"reverse\" = \"Ters\"\n\"domain\" = \"Alan Adı\"\n\"type\" = \"Tür\"\n\"bridge\" = \"Köprü\"\n\"portal\" = \"Portal\"\n\"link\" = \"Bağlantı\"\n\"intercon\" = \"Bağlantı\"\n\"settings\" = \"Ayarlar\"\n\"accountInfo\" = \"Hesap Bilgileri\"\n\"outboundStatus\" = \"Giden Durumu\"\n\"sendThrough\" = \"Üzerinden Gönder\"\n\"test\" = \"Test\"\n\"testResult\" = \"Test Sonucu\"\n\"testing\" = \"Bağlantı test ediliyor...\"\n\"testSuccess\" = \"Test başarılı\"\n\"testFailed\" = \"Test başarısız\"\n\"testError\" = \"Giden test edilemedi\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"Dengeleyici Ekle\"\n\"editBalancer\" = \"Dengeleyiciyi Düzenle\"\n\"balancerStrategy\" = \"Strateji\"\n\"balancerSelectors\" = \"Seçiciler\"\n\"tag\" = \"Etiket\"\n\"tagDesc\" = \"Benzersiz Etiket\"\n\"balancerDesc\" = \"Dengeleyici Etiketi ve Giden Etiketi aynı anda kullanılamaz. Aynı anda kullanıldığında yalnızca giden etiketi çalışır.\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"Gizli Anahtar\"\n\"publicKey\" = \"Genel Anahtar\"\n\"allowedIPs\" = \"İzin Verilen IP'ler\"\n\"endpoint\" = \"Uç Nokta\"\n\"psk\" = \"Ön Paylaşılan Anahtar\"\n\"domainStrategy\" = \"Alan Adı Stratejisi\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"TUN arabiriminin adı. Varsayılan değer 'xray0'dir\"\n\"mtuDesc\" = \"Maksimum İletim Birimi. Veri paketlerinin maksimum boyutu. Varsayılan değer 1500'dür\"\n\"userLevel\" = \"Kullanıcı Seviyesi\"\n\"userLevelDesc\" = \"Bu giriş yoluyla yapılan tüm bağlantılar bu kullanıcı seviyesini kullanacaktır. Varsayılan değer 0'dır\"\n\n[pages.xray.dns]\n\"enable\" = \"DNS'yi Etkinleştir\"\n\"enableDesc\" = \"Dahili DNS sunucusunu etkinleştir\"\n\"tag\" = \"DNS Gelen Etiketi\"\n\"tagDesc\" = \"Bu etiket, yönlendirme kurallarında Gelen etiketi olarak kullanılabilir.\"\n\"clientIp\" = \"İstemci IP\"\n\"clientIpDesc\" = \"DNS sorguları sırasında belirtilen IP konumunu sunucuya bildirmek için kullanılır\"\n\"disableCache\" = \"Önbelleği devre dışı bırak\"\n\"disableCacheDesc\" = \"DNS önbelleğini devre dışı bırakır\"\n\"disableFallback\" = \"Yedeklemeyi devre dışı bırak\"\n\"disableFallbackDesc\" = \"Yedek DNS sorgularını devre dışı bırakır\"\n\"disableFallbackIfMatch\" = \"Eşleşirse Yedeklemeyi Devre Dışı Bırak\"\n\"disableFallbackIfMatchDesc\" = \"DNS sunucusunun eşleşen alan adı listesi vurulduğunda yedek DNS sorgularını devre dışı bırakır\"\n\"enableParallelQuery\" = \"Paralel Sorguyu Etkinleştir\"\n\"enableParallelQueryDesc\" = \"Daha hızlı çözümleme için birden fazla sunucuya paralel DNS sorgularını etkinleştir\"\n\"strategy\" = \"Sorgu Stratejisi\"\n\"strategyDesc\" = \"Alan adlarını çözmek için genel strateji\"\n\"add\" = \"Sunucu Ekle\"\n\"edit\" = \"Sunucuyu Düzenle\"\n\"domains\" = \"Alan Adları\"\n\"expectIPs\" = \"Beklenen IP'ler\"\n\"unexpectIPs\" = \"Beklenmeyen IP'ler\"\n\"useSystemHosts\" = \"Sistem Hosts'larını Kullan\"\n\"useSystemHostsDesc\" = \"Yüklü bir sistemden hosts dosyasını kullan\"\n\"usePreset\" = \"Şablon kullan\"\n\"dnsPresetTitle\" = \"DNS Şablonları\"\n\"dnsPresetFamily\" = \"Aile\"\n\n[pages.xray.fakedns]\n\"add\" = \"Sahte DNS Ekle\"\n\"edit\" = \"Sahte DNS'i Düzenle\"\n\"ipPool\" = \"IP Havuzu Alt Ağı\"\n\"poolSize\" = \"Havuz Boyutu\"\n\n[pages.settings.security]\n\"admin\" = \"Yönetici kimlik bilgileri\"\n\"twoFactor\" = \"İki adımlı doğrulama\"\n\"twoFactorEnable\" = \"2FA'yı Etkinleştir\"\n\"twoFactorEnableDesc\" = \"Daha fazla güvenlik için ek bir doğrulama katmanı ekler.\"\n\"twoFactorModalSetTitle\" = \"İki adımlı doğrulamayı etkinleştir\"\n\"twoFactorModalDeleteTitle\" = \"İki adımlı doğrulamayı devre dışı bırak\"\n\"twoFactorModalSteps\" = \"İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:\"\n\"twoFactorModalFirstStep\" = \"1. Bu QR kodunu doğrulama uygulamasında tarayın veya QR kodunun yanındaki token'ı kopyalayıp uygulamaya yapıştırın\"\n\"twoFactorModalSecondStep\" = \"2. Uygulamadaki kodu girin\"\n\"twoFactorModalRemoveStep\" = \"İki adımlı doğrulamayı kaldırmak için uygulamadaki kodu girin.\"\n\"twoFactorModalChangeCredentialsTitle\" = \"Kimlik bilgilerini değiştir\"\n\"twoFactorModalChangeCredentialsStep\" = \"Yönetici kimlik bilgilerini değiştirmek için uygulamadaki kodu girin.\"\n\"twoFactorModalSetSuccess\" = \"İki faktörlü kimlik doğrulama başarıyla kuruldu\"\n\"twoFactorModalDeleteSuccess\" = \"İki faktörlü kimlik doğrulama başarıyla silindi\"\n\"twoFactorModalError\" = \"Yanlış kod\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"Parametreler değiştirildi.\"\n\"getSettings\" = \"Parametreler alınırken bir hata oluştu.\"\n\"modifyUserError\" = \"Yönetici kimlik bilgileri değiştirilirken bir hata oluştu.\"\n\"modifyUser\" = \"Yönetici kimlik bilgilerini başarıyla değiştirdiniz.\"\n\"originalUserPassIncorrect\" = \"Mevcut kullanıcı adı veya şifre geçersiz\"\n\"userPassMustBeNotEmpty\" = \"Yeni kullanıcı adı ve şifre boş olamaz\"\n\"getOutboundTrafficError\" = \"Giden trafik alınırken hata\"\n\"resetOutboundTrafficError\" = \"Giden trafik sıfırlanırken hata\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ Klavye kapatıldı!\"\n\"noResult\" = \"❗ Sonuç yok!\"\n\"noQuery\" = \"❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!\"\n\"wentWrong\" = \"❌ Bir şeyler yanlış gitti!\"\n\"noIpRecord\" = \"❗ IP Kaydı Yok!\"\n\"noInbounds\" = \"❗ Gelen bağlantı bulunamadı!\"\n\"unlimited\" = \"♾ Sınırsız (Sıfırla)\"\n\"add\" = \"Ekle\"\n\"month\" = \"Ay\"\n\"months\" = \"Aylar\"\n\"day\" = \"Gün\"\n\"days\" = \"Günler\"\n\"hours\" = \"Saatler\"\n\"minutes\" = \"Dakika\"\n\"unknown\" = \"Bilinmeyen\"\n\"inbounds\" = \"Gelenler\"\n\"clients\" = \"İstemciler\"\n\"offline\" = \"🔴 Çevrimdışı\"\n\"online\" = \"🟢 Çevrimiçi\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ Bilinmeyen komut.\"\n\"pleaseChoose\" = \"👇 Lütfen seçin:\\r\\n\"\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\"\n\"start\" = \"👋 Merhaba <i>{{ .Firstname }}</i>.\\r\\n\"\n\"welcome\" = \"🤖 <b>{{ .Hostname }}</b> yönetim botuna hoş geldiniz.\\r\\n\"\n\"status\" = \"✅ Bot çalışıyor!\"\n\"usage\" = \"❗ Lütfen aramak için bir metin sağlayın!\"\n\"getID\" = \"🆔 Kimliğiniz: <code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"Xray Core'u yeniden başlatmak için:\\r\\n<code>/restart</code>\\r\\n\\r\\nBir müşteri e-postasını aramak için:\\r\\n<code>/usage [E-posta]</code>\\r\\n\\r\\nGelenleri aramak için (müşteri istatistikleri ile):\\r\\n<code>/inbound [Açıklama]</code>\\r\\n\\r\\nTelegram Sohbet Kimliği:\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"İstatistikleri aramak için şu komutu kullanın:\\r\\n\\r\\n<code>/usage [E-posta]</code>\\r\\n\\r\\nTelegram Sohbet Kimliği:\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ İşlem başarılı!\"\n\"restartFailed\" = \"❗ İşlem hatası.\\r\\n\\r\\n<code>Hata: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core çalışmıyor.\"\n\"startDesc\" = \"Ana menüyü göster\"\n\"helpDesc\" = \"Bot yardımı\"\n\"statusDesc\" = \"Bot durumunu kontrol et\"\n\"idDesc\" = \"Telegram ID'nizi göster\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 CPU Yükü {{ .Percent }}% eşiği {{ .Threshold }}%'yi aşıyor\"\n\"selectUserFailed\" = \"❌ Kullanıcı seçiminde hata!\"\n\"userSaved\" = \"✅ Telegram Kullanıcısı kaydedildi.\"\n\"loginSuccess\" = \"✅ Panele başarıyla giriş yapıldı.\\r\\n\"\n\"loginFailed\" = \"❗️Panele giriş denemesi başarısız oldu.\\r\\n\"\n\"2faFailed\" = \"2FA Hatası\"\n\"report\" = \"🕰 Planlanmış Raporlar: {{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ Tarih&Zaman: {{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 Sunucu: {{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 3X-UI Sürümü: {{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Xray Sürümü: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IP'ler:\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ Çalışma Süresi: {{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 Sistem Yükü: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 RAM: {{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP: {{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP: {{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 Trafik: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Durum: {{ .State }}\\r\\n\"\n\"username\" = \"👤 Kullanıcı Adı: {{ .Username }}\\r\\n\"\n\"password\" = \"👤 Şifre: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ Zaman: {{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 Gelen: {{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 Port: {{ .Port }}\\r\\n\"\n\"expire\" = \"📅 Son Kullanma Tarihi: {{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 Sona Erecek: {{ .Time }}\\r\\n\"\n\"active\" = \"💡 Aktif: {{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 Etkin: {{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 Bağlantı durumu: {{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 Son çevrimiçi: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 E-posta: {{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 Yükleme: ↑{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 İndirme: ↓{{ .Download }}\\r\\n\"\n\"total\" = \"📊 Toplam: ↑↓{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 Telegram Kullanıcısı: {{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 Tükenmiş {{ .Type }}:\\r\\n\"\n\"exhaustedCount\" = \"🚨 Tükenmiş {{ .Type }} sayısı:\\r\\n\"\n\"onlinesCount\" = \"🌐 Çevrimiçi Müşteriler: {{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 Devre Dışı: {{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 Yakında Tükenecek: {{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 Yedekleme Zamanı: {{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 Yenilendi: {{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ Evet\"\n\"no\" = \"❌ Hayır\"\n\"received_id\" = \"🔑📥 Kimlik güncellendi.\"\n\"received_password\" = \"🔑📥 Şifre güncellendi.\"\n\"received_email\" = \"📧📥 E-posta güncellendi.\"\n\"received_comment\" = \"💬📥 Yorum güncellendi.\"\n\"id_prompt\" = \"🔑 Varsayılan Kimlik: {{ .ClientId }}\\n\\nKimliğinizi girin.\"\n\"pass_prompt\" = \"🔑 Varsayılan Şifre: {{ .ClientPassword }}\\n\\nŞifrenizi girin.\"\n\"email_prompt\" = \"📧 Varsayılan E-posta: {{ .ClientEmail }}\\n\\nE-postanızı girin.\"\n\"comment_prompt\" = \"💬 Varsayılan Yorum: {{ .ClientComment }}\\n\\nYorumunuzu girin.\"\n\"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!\"\n\"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!\"\n\"cancel\" = \"❌ İşlem iptal edildi! \\n\\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄\"\n\"error_add_client\" = \"⚠️ Hata:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"Tamam, varsayılan değeri kullanacağım. 😊\"\n\"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 🚫\"\n\"AreYouSure\" = \"Emin misin? 🤔\"\n\"SuccessResetTraffic\" = \"📧 E-posta: {{ .ClientEmail }}\\n🏁 Sonuç: ✅ Başarılı\"\n\"FailedResetTraffic\" = \"📧 E-posta: {{ .ClientEmail }}\\n🏁 Sonuç: ❌ Başarısız \\n\\n🛠️ Hata: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 Tüm müşteriler için trafik sıfırlama işlemi tamamlandı.\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ Klavyeyi Kapat\"\n\"cancel\" = \"❌ İptal\"\n\"cancelReset\" = \"❌ Sıfırlamayı İptal Et\"\n\"cancelIpLimit\" = \"❌ IP Limitini İptal Et\"\n\"confirmResetTraffic\" = \"✅ Trafiği Sıfırlamayı Onayla?\"\n\"confirmClearIps\" = \"✅ IP'leri Temizlemeyi Onayla?\"\n\"confirmRemoveTGUser\" = \"✅ Telegram Kullanıcısını Kaldırmayı Onayla?\"\n\"confirmToggle\" = \"✅ Kullanıcıyı Etkinleştirme/Devre Dışı Bırakmayı Onayla?\"\n\"dbBackup\" = \"Veritabanı Yedeği Al\"\n\"serverUsage\" = \"Sunucu Kullanımı\"\n\"getInbounds\" = \"Gelenleri Al\"\n\"depleteSoon\" = \"Yakında Tükenecek\"\n\"clientUsage\" = \"Kullanımı Al\"\n\"onlines\" = \"Çevrimiçi Müşteriler\"\n\"commands\" = \"Komutlar\"\n\"refresh\" = \"🔄 Yenile\"\n\"clearIPs\" = \"❌ IP'leri Temizle\"\n\"removeTGUser\" = \"❌ Telegram Kullanıcısını Kaldır\"\n\"selectTGUser\" = \"👤 Telegram Kullanıcısını Seç\"\n\"selectOneTGUser\" = \"👤 Bir Telegram Kullanıcısını Seçin:\"\n\"resetTraffic\" = \"📈 Trafiği Sıfırla\"\n\"resetExpire\" = \"📅 Son Kullanma Tarihini Değiştir\"\n\"ipLog\" = \"🔢 IP Günlüğü\"\n\"ipLimit\" = \"🔢 IP Limiti\"\n\"setTGUser\" = \"👤 Telegram Kullanıcısını Ayarla\"\n\"toggle\" = \"🔘 Etkinleştir / Devre Dışı Bırak\"\n\"custom\" = \"🔢 Özel\"\n\"confirmNumber\" = \"✅ Onayla: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ Ekleme onayı: {{ .Num }}\"\n\"limitTraffic\" = \"🚧 Trafik Sınırı\"\n\"getBanLogs\" = \"Yasak Günlüklerini Al\"\n\"allClients\" = \"Tüm Müşteriler\"\n\"addClient\" = \"Müşteri Ekle\"\n\"submitDisable\" = \"Devre Dışı Olarak Gönder ☑️\"\n\"submitEnable\" = \"Etkin Olarak Gönder ✅\"\n\"use_default\" = \"🏷️ Varsayılanı Kullan\"\n\"change_id\" = \"⚙️🔑 Kimlik\"\n\"change_password\" = \"⚙️🔑 Şifre\"\n\"change_email\" = \"⚙️📧 E-posta\"\n\"change_comment\" = \"⚙️💬 Yorum\"\n\"ResetAllTraffics\" = \"Tüm Trafikleri Sıfırla\"\n\"SortedTrafficUsageReport\" = \"Sıralı Trafik Kullanım Raporu\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ İşlem başarılı!\"\n\"errorOperation\" = \"❗ İşlemde hata.\"\n\"getInboundsFailed\" = \"❌ Gelenler alınamadı.\"\n\"getClientsFailed\" = \"❌ Müşteriler alınamadı.\"\n\"canceled\" = \"❌ {{ .Email }}: İşlem iptal edildi.\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}: Müşteri başarıyla yenilendi.\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}: IP'ler başarıyla yenilendi.\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}: Müşterinin Telegram Kullanıcısı başarıyla yenilendi.\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}: Trafik başarıyla sıfırlandı.\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: Trafik limiti başarıyla kaydedildi.\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}: Son kullanma günleri başarıyla sıfırlandı.\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}: IP limiti {{ .Count }} başarıyla kaydedildi.\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}: IP'ler başarıyla temizlendi.\"\n\"getIpLog\" = \"✅ {{ .Email }}: IP Günlüğü alındı.\"\n\"getUserInfo\" = \"✅ {{ .Email }}: Telegram Kullanıcı Bilgisi alındı.\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}: Telegram Kullanıcısı başarıyla kaldırıldı.\"\n\"enableSuccess\" = \"✅ {{ .Email }}: Başarıyla etkinleştirildi.\"\n\"disableSuccess\" = \"✅ {{ .Email }}: Başarıyla devre dışı bırakıldı.\"\n\"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: <code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"Gelen {{ .Inbound }} için bir Müşteri Seçin\"\n\"chooseInbound\" = \"Bir Gelen Seçin\"\n"
  },
  {
    "path": "web/translation/translate.uk_UA.toml",
    "content": "\"username\" = \"Ім'я користувача\"\n\"password\" = \"Пароль\"\n\"login\" = \"Увійти\"\n\"confirm\" = \"Підтвердити\"\n\"cancel\" = \"Скасувати\"\n\"close\" = \"Закрити\"\n\"create\" = \"Створити\"\n\"update\" = \"Оновити\"\n\"copy\" = \"Копіювати\"\n\"copied\" = \"Скопійовано\"\n\"download\" = \"Завантажити\"\n\"remark\" = \"Примітка\"\n\"enable\" = \"Увімкнути\"\n\"protocol\" = \"Протокол\"\n\"search\" = \"Пошук\"\n\"filter\" = \"Фільтр\"\n\"loading\" = \"Завантаження...\"\n\"second\" = \"Секунда\"\n\"minute\" = \"Хвилина\"\n\"hour\" = \"Година\"\n\"day\" = \"День\"\n\"check\" = \"Перевірка\"\n\"indefinite\" = \"Безстроково\"\n\"unlimited\" = \"Безлімітний\"\n\"none\" = \"Немає\"\n\"qrCode\" = \"QR-Код\"\n\"info\" = \"Більше інформації\"\n\"edit\" = \"Редагувати\"\n\"delete\" = \"Видалити\"\n\"reset\" = \"Скидання\"\n\"noData\" = \"Немає даних.\"\n\"copySuccess\" = \"Скопійовано успішно\"\n\"sure\" = \"Звичайно\"\n\"encryption\" = \"Шифрування\"\n\"useIPv4ForHost\" = \"Використовувати IPv4 для хоста\"\n\"transmission\" = \"Протокол передачи\"\n\"host\" = \"Хост\"\n\"path\" = \"Шлях\"\n\"camouflage\" = \"Маскування\"\n\"status\" = \"Статус\"\n\"enabled\" = \"Увімкнено\"\n\"disabled\" = \"Вимкнено\"\n\"depleted\" = \"Вичерпано\"\n\"depletingSoon\" = \"Вичерпується\"\n\"offline\" = \"Офлайн\"\n\"online\" = \"Онлайн\"\n\"domainName\" = \"Доменне ім`я\"\n\"monitor\" = \"Слухати IP\"\n\"certificate\" = \"Цифровий сертифікат\"\n\"fail\" = \"Помилка\"\n\"comment\" = \"Коментар\"\n\"success\" = \"Успішно\"\n\"lastOnline\" = \"Був(ла) онлайн\"\n\"getVersion\" = \"Отримати версію\"\n\"install\" = \"Встановити\"\n\"clients\" = \"Клієнти\"\n\"usage\" = \"Використання\"\n\"twoFactorCode\" = \"Код\"\n\"remained\" = \"Залишилося\"\n\"security\" = \"Беспека\"\n\"secAlertTitle\" = \"Попередження системи безпеки\"\n\"secAlertSsl\" = \"Це з'єднання не є безпечним. Будь ласка, уникайте введення конфіденційної інформації, поки TLS не буде активовано для захисту даних.\"\n\"secAlertConf\" = \"Деякі налаштування вразливі до атак. Рекомендується посилити протоколи безпеки, щоб запобігти можливим порушенням.\"\n\"secAlertSSL\" = \"Панель не має безпечного з'єднання. Будь ласка, встановіть сертифікат TLS для захисту даних.\"\n\"secAlertPanelPort\" = \"Стандартний порт панелі вразливий. Будь ласка, сконфігуруйте випадковий або конкретний порт.\"\n\"secAlertPanelURI\" = \"Стандартний URI-шлях панелі небезпечний. Будь ласка, сконфігуруйте складний URI-шлях.\"\n\"secAlertSubURI\" = \"Стандартний URI-шлях підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях.\"\n\"secAlertSubJsonURI\" = \"Стандартний URI-шлях JSON підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях.\"\n\"emptyDnsDesc\" = \"Немає доданих DNS-серверів.\"\n\"emptyFakeDnsDesc\" = \"Немає доданих Fake DNS-серверів.\"\n\"emptyBalancersDesc\" = \"Немає доданих балансувальників.\"\n\"emptyReverseDesc\" = \"Немає доданих зворотних проксі.\"\n\"somethingWentWrong\" = \"Щось пішло не так\"\n\n[subscription]\n\"title\" = \"Інформація про підписку\"\n\"subId\" = \"ID підписки\"\n\"status\" = \"Статус\"\n\"downloaded\" = \"Завантажено\"\n\"uploaded\" = \"Відвантажено\"\n\"expiry\" = \"Термін дії\"\n\"totalQuota\" = \"Загальна квота\"\n\"individualLinks\" = \"Окремі посилання\"\n\"active\" = \"Активна\"\n\"inactive\" = \"Неактивна\"\n\"unlimited\" = \"Безліміт\"\n\"noExpiry\" = \"Без строку\"\n\n[menu]\n\"theme\" = \"Тема\"\n\"dark\" = \"Темна\"\n\"ultraDark\" = \"Ультра темна\"\n\"dashboard\" = \"Огляд\"\n\"inbounds\" = \"Вхідні\"\n\"settings\" = \"Параметри панелі\"\n\"xray\" = \"Конфігурації Xray\"\n\"logout\" = \"Вийти\"\n\"link\" = \"Керувати\"\n\n[pages.login]\n\"hello\" = \"Привіт\"\n\"title\" = \"Привітання!\"\n\"loginAgain\" = \"Ваш сеанс закінчився, увійдіть знову\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"Формат вхідних даних недійсний.\"\n\"emptyUsername\" = \"Потрібне ім'я користувача\"\n\"emptyPassword\" = \"Потрібен пароль\"\n\"wrongUsernameOrPassword\" = \"Невірне ім’я користувача, пароль або код двофакторної аутентифікації.\"\n\"successLogin\" = \"Ви успішно увійшли до свого облікового запису.\"\n\n[pages.index]\n\"title\" = \"Огляд\"\n\"cpu\" = \"ЦП\"\n\"logicalProcessors\" = \"Логічні процесори\"\n\"frequency\" = \"Частота\"\n\"swap\" = \"Своп\"\n\"storage\" = \"Сховище\"\n\"memory\" = \"ОЗП\"\n\"threads\" = \"Потоки\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"Зупинити\"\n\"restartXray\" = \"Перезапустити\"\n\"xraySwitch\" = \"Версія\"\n\"xraySwitchClick\" = \"Виберіть версію, на яку ви хочете перейти.\"\n\"xraySwitchClickDesk\" = \"Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями.\"\n\"xrayStatusUnknown\" = \"Невідомо\"\n\"xrayStatusRunning\" = \"Запущено\"\n\"xrayStatusStop\" = \"Зупинено\"\n\"xrayStatusError\" = \"Помилка\"\n\"xrayErrorPopoverTitle\" = \"Під час роботи Xray сталася помилка\"\n\"operationHours\" = \"Час роботи\"\n\"systemLoad\" = \"Завантаження системи\"\n\"systemLoadDesc\" = \"Середнє завантаження системи за останні 1, 5 і 15 хвилин\"\n\"connectionCount\" = \"Статистика з'єднання\"\n\"ipAddresses\" = \"IP-адреси\"\n\"toggleIpVisibility\" = \"Перемкнути видимість IP\"\n\"overallSpeed\" = \"Загальна швидкість\"\n\"upload\" = \"Відправка\"\n\"download\" = \"Завантаження\"\n\"totalData\" = \"Загальний обсяг даних\"\n\"sent\" = \"Відправлено\"\n\"received\" = \"Отримано\"\n\"documentation\" = \"Документація\"\n\"xraySwitchVersionDialog\" = \"Ви дійсно хочете змінити версію Xray?\"\n\"xraySwitchVersionDialogDesc\" = \"Це змінить версію Xray на #version#.\"\n\"xraySwitchVersionPopover\" = \"Xray успішно оновлено\"\n\"geofileUpdateDialog\" = \"Ви дійсно хочете оновити геофайл?\"\n\"geofileUpdateDialogDesc\" = \"Це оновить файл #filename#.\"\n\"geofilesUpdateDialogDesc\" = \"Це оновить усі геофайли.\"\n\"geofilesUpdateAll\" = \"Оновити все\"\n\"geofileUpdatePopover\" = \"Геофайл успішно оновлено\"\n\"dontRefresh\" = \"Інсталяція триває, будь ласка, не оновлюйте цю сторінку\"\n\"logs\" = \"Журнали\"\n\"config\" = \"Конфігурація\"\n\"backup\" = \"Резервна копія\"\n\"backupTitle\" = \"Резервне копіювання та відновлення бази даних\"\n\"exportDatabase\" = \"Резервна копія\"\n\"exportDatabaseDesc\" = \"Натисніть, щоб завантажити файл .db, що містить резервну копію вашої поточної бази даних на ваш пристрій.\"\n\"importDatabase\" = \"Відновити\"\n\"importDatabaseDesc\" = \"Натисніть, щоб вибрати та завантажити файл .db з вашого пристрою для відновлення бази даних з резервної копії.\"\n\"importDatabaseSuccess\" = \"Базу даних успішно імпортовано\"\n\"importDatabaseError\" = \"Виникла помилка під час імпорту бази даних\"\n\"readDatabaseError\" = \"Виникла помилка під час читання бази даних\"\n\"getDatabaseError\" = \"Виникла помилка під час отримання бази даних\"\n\"getConfigError\" = \"Виникла помилка під час отримання файлу конфігурації\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"Загальний трафік\"\n\"allTimeTrafficUsage\" = \"Загальне використання за весь час\"\n\"title\" = \"Вхідні\"\n\"totalDownUp\" = \"Всього надісланих/отриманих\"\n\"totalUsage\" = \"Всього використанно\"\n\"inboundCount\" = \"Загальна кількість вхідних\"\n\"operate\" = \"Меню\"\n\"enable\" = \"Увімкнено\"\n\"remark\" = \"Примітка\"\n\"protocol\" = \"Протокол\"\n\"port\" = \"Порт\"\n\"portMap\" = \"Порт-перехід\"\n\"traffic\" = \"Трафік\"\n\"details\" = \"Деталі\"\n\"transportConfig\" = \"Транспорт\"\n\"expireDate\" = \"Тривалість\"\n\"createdAt\" = \"Створено\"\n\"updatedAt\" = \"Оновлено\"\n\"resetTraffic\" = \"Скинути трафік\"\n\"addInbound\" = \"Додати вхідний\"\n\"generalActions\" = \"Загальні дії\"\n\"autoRefresh\" = \"Автооновлення\"\n\"autoRefreshInterval\" = \"Інтервал\"\n\"modifyInbound\" = \"Змінити вхідний\"\n\"deleteInbound\" = \"Видалити вхідні\"\n\"deleteInboundContent\" = \"Ви впевнені, що хочете видалити вхідні?\"\n\"deleteClient\" = \"Видалити клієнта\"\n\"deleteClientContent\" = \"Ви впевнені, що хочете видалити клієнт?\"\n\"resetTrafficContent\" = \"Ви впевнені, що хочете скинути трафік?\"\n\"copyLink\" = \"Копіювати URL\"\n\"address\" = \"Адреса\"\n\"network\" = \"Мережа\"\n\"destinationPort\" = \"Порт призначення\"\n\"targetAddress\" = \"Цільова адреса\"\n\"monitorDesc\" = \"Залиште порожнім, щоб слухати всі IP-адреси\"\n\"meansNoLimit\" = \"= Необмежено. (одиниця: ГБ)\"\n\"totalFlow\" = \"Загальна витрата\"\n\"leaveBlankToNeverExpire\" = \"Залиште порожнім, щоб ніколи не закінчувався\"\n\"noRecommendKeepDefault\" = \"Рекомендується зберегти значення за замовчуванням\"\n\"certificatePath\" = \"Шлях до файлу\"\n\"certificateContent\" = \"Вміст файлу\"\n\"publicKey\" = \"Публічний ключ\"\n\"privatekey\" = \"Закритий ключ\"\n\"clickOnQRcode\" = \"Натисніть QR-код, щоб скопіювати\"\n\"client\" = \"Клієнт\"\n\"export\" = \"Експортувати всі URL-адреси\"\n\"clone\" = \"Клон\"\n\"cloneInbound\" = \"Клонувати\"\n\"cloneInboundContent\" = \"Усі налаштування цього вхідного потоку, крім порту, IP-адреси прослуховування та клієнтів, будуть застосовані до клону.\"\n\"cloneInboundOk\" = \"Клонувати\"\n\"resetAllTraffic\" = \"Скинути весь вхідний трафік\"\n\"resetAllTrafficTitle\" = \"Скинути весь вхідний трафік\"\n\"resetAllTrafficContent\" = \"Ви впевнені, що бажаєте скинути трафік усіх вхідних?\"\n\"resetInboundClientTraffics\" = \"Скинути трафік клієнтів\"\n\"resetInboundClientTrafficTitle\" = \"Скинути трафік клієнтів\"\n\"resetInboundClientTrafficContent\" = \"Ви впевнені, що бажаєте скинути трафік клієнтів цього вхідного потоку?\"\n\"resetAllClientTraffics\" = \"Скинути весь трафік клієнтів\"\n\"resetAllClientTrafficTitle\" = \"Скинути весь трафік клієнтів\"\n\"resetAllClientTrafficContent\" = \"Ви впевнені, що бажаєте скинути трафік усіх клієнтів?\"\n\"delDepletedClients\" = \"Видалити вичерпані клієнти\"\n\"delDepletedClientsTitle\" = \"Видалити вичерпані клієнти\"\n\"delDepletedClientsContent\" = \"Ви впевнені, що хочете видалити всі вичерпані клієнти?\"\n\"email\" = \"Електронна пошта\"\n\"emailDesc\" = \"Будь ласка, надайте унікальну адресу електронної пошти.\"\n\"IPLimit\" = \"Обмеження IP\"\n\"IPLimitDesc\" = \"Вимикає вхідний, якщо кількість перевищує встановлене значення. (0 = вимкнено)\"\n\"IPLimitlog\" = \"Журнал IP\"\n\"IPLimitlogDesc\" = \"Журнал історії IP-адрес. (щоб увімкнути вхідну після вимкнення, очистіть журнал)\"\n\"IPLimitlogclear\" = \"Очистити журнал\"\n\"setDefaultCert\" = \"Установити сертифікат з панелі\"\n\"telegramDesc\" = \"Будь ласка, вкажіть ID чату Telegram. (використовуйте команду '/id' у боті) або (@userinfobot)\"\n\"subscriptionDesc\" = \"Щоб знайти URL-адресу вашої підписки, перейдіть до «Деталі». Крім того, ви можете використовувати одне ім'я для кількох клієнтів.\"\n\"info\" = \"Інформація\"\n\"same\" = \"Те саме\"\n\"inboundData\" = \"Вхідні дані\"\n\"exportInbound\" = \"Експортувати вхідні\"\n\"import\" = \"Імпорт\"\n\"importInbound\" = \"Імпортувати вхідний\"\n\"periodicTrafficResetTitle\" = \"Скидання трафіку\"\n\"periodicTrafficResetDesc\" = \"Автоматично скидати лічильник трафіку через певні проміжки часу\"\n\"lastReset\" = \"Останнє скидання\"\n\n[pages.client]\n\"add\" = \"Додати клієнта\"\n\"edit\" = \"Редагувати клієнта\"\n\"submitAdd\" = \"Додати клієнта\"\n\"submitEdit\" = \"Зберегти зміни\"\n\"clientCount\" = \"Кількість клієнтів\"\n\"bulk\" = \"Додати групу\"\n\"method\" = \"Метод\"\n\"first\" = \"Перший\"\n\"last\" = \"Останній\"\n\"prefix\" = \"Префікс\"\n\"postfix\" = \"Постфікс\"\n\"delayedStart\" = \"Початок використання\"\n\"expireDays\" = \"Тривалість\"\n\"days\" = \"Дні(в)\"\n\"renew\" = \"Автоматичне оновлення\"\n\"renewDesc\" = \"Автоматичне поновлення після закінчення терміну дії. (0 = вимкнено)(одиниця: день)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"Ніколи\"\n\"daily\" = \"Щодня\"\n\"weekly\" = \"Щотижня\"\n\"monthly\" = \"Щомісяця\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"Отримати\"\n\"updateSuccess\" = \"Оновлення пройшло успішно\"\n\"logCleanSuccess\" = \"Журнал очищено\"\n\"inboundsUpdateSuccess\" = \"Вхідні підключення успішно оновлено\"\n\"inboundUpdateSuccess\" = \"Вхідне підключення успішно оновлено\"\n\"inboundCreateSuccess\" = \"Вхідне підключення успішно створено\"\n\"inboundDeleteSuccess\" = \"Вхідне підключення успішно видалено\"\n\"inboundClientAddSuccess\" = \"Клієнт(и) вхідного підключення додано\"\n\"inboundClientDeleteSuccess\" = \"Клієнта вхідного підключення видалено\"\n\"inboundClientUpdateSuccess\" = \"Клієнта вхідного підключення оновлено\"\n\"delDepletedClientsSuccess\" = \"Усі вичерпані клієнти видалені\"\n\"resetAllClientTrafficSuccess\" = \"Весь трафік клієнта скинуто\"\n\"resetAllTrafficSuccess\" = \"Весь трафік скинуто\"\n\"resetInboundClientTrafficSuccess\" = \"Трафік скинуто\"\n\"trafficGetError\" = \"Помилка отримання даних про трафік\"\n\"getNewX25519CertError\" = \"Помилка при отриманні сертифіката X25519.\"\n\"getNewmldsa65Error\" = \"Помилка при отриманні сертифіката mldsa65.\"\n\"getNewVlessEncError\" = \"Помилка при отриманні сертифіката VlessEnc.\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"Запит\"\n\"response\" = \"Відповідь\"\n\"name\" = \"Ім'я\"\n\"value\" = \"Значення\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"Версія\"\n\"method\" = \"Метод\"\n\"path\" = \"Шлях\"\n\"status\" = \"Статус\"\n\"statusDescription\" = \"Опис стану\"\n\"requestHeader\" = \"Заголовок запиту\"\n\"responseHeader\" = \"Заголовок відповіді\"\n\n[pages.settings]\n\"title\" = \"Параметри панелі\"\n\"save\" = \"Зберегти\"\n\"infoDesc\" = \"Кожна внесена тут зміна повинна бути збережена. Перезапустіть панель, щоб застосувати зміни.\"\n\"restartPanel\" = \"Перезапустити панель\"\n\"restartPanelDesc\" = \"Ви впевнені, що бажаєте перезапустити панель? Якщо ви не можете отримати доступ до панелі після перезапуску, будь ласка, перегляньте інформацію журналу панелі на сервері.\"\n\"restartPanelSuccess\" = \"Панель успішно перезапущено\"\n\"actions\" = \"Дії\"\n\"resetDefaultConfig\" = \"Відновити значення за замовчуванням\"\n\"panelSettings\" = \"Загальні\"\n\"securitySettings\" = \"Автентифікація\"\n\"TGBotSettings\" = \"Telegram Бот\"\n\"panelListeningIP\" = \"Слухати IP\"\n\"panelListeningIPDesc\" = \"IP-адреса для веб-панелі. (залиште порожнім, щоб слухати всі IP-адреси)\"\n\"panelListeningDomain\" = \"Домен прослуховування\"\n\"panelListeningDomainDesc\" = \"Доменне ім'я для веб-панелі. (залиште порожнім, щоб слухати всі домени та IP-адреси)\"\n\"panelPort\" = \"Порт прослуховування\"\n\"panelPortDesc\" = \"Номер порту для веб-панелі. (має бути невикористаний порт)\"\n\"publicKeyPath\" = \"Шлях відкритого ключа\"\n\"publicKeyPathDesc\" = \"Шлях до файлу відкритого ключа для веб-панелі. (починається з ‘/‘)\"\n\"privateKeyPath\" = \"Шлях приватного ключа\"\n\"privateKeyPathDesc\" = \"Шлях до файлу приватного ключа для веб-панелі. (починається з ‘/‘)\"\n\"panelUrlPath\" = \"Шлях URL\"\n\"panelUrlPathDesc\" = \"Шлях URL для веб-панелі. (починається з ‘/‘ і закінчується ‘/‘)\"\n\"pageSize\" = \"Розмір сторінки\"\n\"pageSizeDesc\" = \"Визначити розмір сторінки для вхідної таблиці. (0 = вимкнено)\"\n\"remarkModel\" = \"Модель зауваження та роздільний символ\"\n\"datepicker\" = \"Тип календаря\"\n\"datepickerPlaceholder\" = \"Виберіть дату\"\n\"datepickerDescription\" = \"Заплановані завдання виконуватимуться на основі цього календаря.\"\n\"sampleRemark\" = \"Зразок зауваження\"\n\"oldUsername\" = \"Поточне ім'я користувача\"\n\"currentPassword\" = \"Поточний пароль\"\n\"newUsername\" = \"Нове ім'я користувача\"\n\"newPassword\" = \"Новий пароль\"\n\"telegramBotEnable\" = \"Увімкнути Telegram Bot\"\n\"telegramBotEnableDesc\" = \"Вмикає бота Telegram.\"\n\"telegramToken\" = \"Telegram Токен\"\n\"telegramTokenDesc\" = \"Токен бота Telegram, отриманий від '@BotFather'.\"\n\"telegramProxy\" = \"SOCKS Проксі\"\n\"telegramProxyDesc\" = \"Вмикає проксі-сервер SOCKS5 для підключення до Telegram. (відкоригуйте параметри відповідно до посібника)\"\n\"telegramAPIServer\" = \"Сервер Telegram API\"\n\"telegramAPIServerDesc\" = \"Сервер Telegram API для використання. Залиште поле порожнім, щоб використовувати сервер за умовчанням.\"\n\"telegramChatId\" = \"Ідентифікатор чату адміністратора\"\n\"telegramChatIdDesc\" = \"Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут @userinfobot) або (використовуйте команду '/id' у боті)\"\n\"telegramNotifyTime\" = \"Час сповіщення\"\n\"telegramNotifyTimeDesc\" = \"Час повідомлення бота Telegram, встановлений для періодичних звітів. (використовуйте формат часу crontab)\"\n\"tgNotifyBackup\" = \"Резервне копіювання бази даних\"\n\"tgNotifyBackupDesc\" = \"Надіслати файл резервної копії бази даних зі звітом.\"\n\"tgNotifyLogin\" = \"Сповіщення про вхід\"\n\"tgNotifyLoginDesc\" = \"Отримувати сповіщення про ім'я користувача, IP-адресу та час щоразу, коли хтось намагається увійти у вашу веб-панель.\"\n\"sessionMaxAge\" = \"Тривалість сеансу\"\n\"sessionMaxAgeDesc\" = \"Тривалість, протягом якої ви можете залишатися в системі. (одиниця: хвилина)\"\n\"expireTimeDiff\" = \"Повідомлення про дату закінчення\"\n\"expireTimeDiffDesc\" = \"Отримувати сповіщення про термін дії при досягненні цього порогу. (одиниця: день)\"\n\"trafficDiff\" = \"Повідомлення про обмеження трафіку\"\n\"trafficDiffDesc\" = \"Отримувати сповіщення про обмеження трафіку при досягненні цього порогу. (одиниця: ГБ)\"\n\"tgNotifyCpu\" = \"Сповіщення про завантаження ЦП\"\n\"tgNotifyCpuDesc\" = \"Отримувати сповіщення, якщо навантаження ЦП перевищує це порогове значення. (одиниця: %)\"\n\"timeZone\" = \"Часовий пояс\"\n\"timeZoneDesc\" = \"Заплановані завдання виконуватимуться на основі цього часового поясу.\"\n\"subSettings\" = \"Підписка\"\n\"subEnable\" = \"Увімкнути службу підписки\"\n\"subEnableDesc\" = \"Вмикає службу підписки.\"\n\"subJsonEnable\" = \"Увімкнути/вимкнути JSON-кінець підписки незалежно.\"\n\"subTitle\" = \"Назва Підписки\"\n\"subTitleDesc\" = \"Назва, яка відображається у VPN-клієнті\"\n\"subSupportUrl\" = \"URL підтримки\"\n\"subSupportUrlDesc\" = \"Посилання на технічну підтримку, що відображається у VPN-клієнті\"\n\"subProfileUrl\" = \"URL профілю\"\n\"subProfileUrlDesc\" = \"Посилання на ваш вебсайт, що відображається у VPN-клієнті\"\n\"subAnnounce\" = \"Оголошення\"\n\"subAnnounceDesc\" = \"Текст оголошення, що відображається у VPN-клієнті\"\n\"subEnableRouting\" = \"Увімкнути маршрутизацію\"\n\"subEnableRoutingDesc\" = \"Глобальне налаштування для увімкнення маршрутизації у VPN-клієнті. (Тільки для Happ)\"\n\"subRoutingRules\" = \"Правила маршрутизації\"\n\"subRoutingRulesDesc\" = \"Глобальні правила маршрутизації для VPN-клієнта. (Тільки для Happ)\"\n\"subListen\" = \"Слухати IP\"\n\"subListenDesc\" = \"IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)\"\n\"subPort\" = \"Слухати порт\"\n\"subPortDesc\" = \"Номер порту для служби підписки. (має бути невикористаний порт)\"\n\"subCertPath\" = \"Шлях відкритого ключа\"\n\"subCertPathDesc\" = \"Шлях до файлу відкритого ключа для служби підписки. (починається з ‘/‘)\"\n\"subKeyPath\" = \"Шлях приватного ключа\"\n\"subKeyPathDesc\" = \"Шлях до файлу приватного ключа для служби підписки. (починається з ‘/‘)\"\n\"subPath\" = \"Шлях URI\"\n\"subPathDesc\" = \"Шлях URI для служби підписки. (починається з ‘/‘ і закінчується ‘/‘)\"\n\"subDomain\" = \"Домен прослуховування\"\n\"subDomainDesc\" = \"Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси)\"\n\"subUpdates\" = \"Інтервали оновлення\"\n\"subUpdatesDesc\" = \"Інтервали оновлення URL-адреси підписки в клієнтських програмах. (одиниця: година)\"\n\"subEncrypt\" = \"Закодувати\"\n\"subEncryptDesc\" = \"Повернений вміст послуги підписки матиме кодування Base64.\"\n\"subShowInfo\" = \"Показати інформацію про використання\"\n\"subShowInfoDesc\" = \"Залишок трафіку та дата відображатимуться в клієнтських програмах.\"\n\"subURI\" = \"URI зворотного проксі\"\n\"subURIDesc\" = \"URI до URL-адреси підписки для використання за проксі.\"\n\"externalTrafficInformEnable\" = \"Інформація про зовнішній трафік\"\n\"externalTrafficInformEnableDesc\" = \"Інформувати зовнішній API про кожне оновлення трафіку.\"\n\"externalTrafficInformURI\" = \"Інформаційний URI зовнішнього трафіку\"\n\"externalTrafficInformURIDesc\" = \"Оновлення трафіку надсилаються на цей URI.\"\n\"fragment\" = \"Фрагментація\"\n\"fragmentDesc\" = \"Увімкнути фрагментацію для пакету привітання TLS\"\n\"fragmentSett\" = \"Параметри фрагментації\"\n\"noisesDesc\" = \"Увімкнути Noises.\"\n\"noisesSett\" = \"Налаштування Noises\"\n\"mux\" = \"Mux\"\n\"muxDesc\" = \"Передавати кілька незалежних потоків даних у межах встановленого потоку даних.\"\n\"muxSett\" = \"Налаштування Mux\"\n\"direct\" = \"Пряме підключення\"\n\"directDesc\" = \"Безпосередньо встановлює з’єднання з доменами або діапазонами IP певної країни.\"\n\"notifications\" = \"Сповіщення\"\n\"certs\" = \"Сертифікати\"\n\"externalTraffic\" = \"Зовнішній трафік\"\n\"dateAndTime\" = \"Дата та час\"\n\"proxyAndServer\" = \"Проксі та сервер\"\n\"intervals\" = \"Інтервали\"\n\"information\" = \"Інформація\"\n\"language\" = \"Мова\"\n\"telegramBotLanguage\" = \"Мова Telegram-бота\"\n\n[pages.xray]\n\"title\" = \"Xray конфігурації\"\n\"save\" = \"Зберегти\"\n\"restart\" = \"Перезапустити Xray\"\n\"restartSuccess\" = \"Xray успішно перезапущено\"\n\"stopSuccess\" = \"Xray успішно зупинено\"\n\"restartError\" = \"Виникла помилка під час перезапуску Xray.\"\n\"stopError\" = \"Виникла помилка під час зупинки Xray.\"\n\"basicTemplate\" = \"Базовий шаблон\"\n\"advancedTemplate\" = \"Додатково\"\n\"generalConfigs\" = \"Загальні конфігурації\"\n\"generalConfigsDesc\" = \"Ці параметри визначатимуть загальні налаштування.\"\n\"logConfigs\" = \"Журнал\"\n\"logConfigsDesc\" = \"Журнали можуть вплинути на ефективність вашого сервера. Рекомендується вмикати його з розумом лише у випадку ваших потреб\"\n\"blockConfigsDesc\" = \"Ці параметри блокуватимуть трафік на основі конкретних запитуваних протоколів і веб-сайтів.\"\n\"basicRouting\" = \"Основна Маршрутизація\"\n\"blockConnectionsConfigsDesc\" = \"Ці параметри блокуватимуть трафік на основі запитаних країн.\"\n\"directConnectionsConfigsDesc\" = \"Пряме з'єднання гарантує, що певний трафік не буде маршрутизовано через інший сервер.\"\n\"blockips\" = \"Блокувати IP\"\n\"blockdomains\" = \"Блокувати домени\"\n\"directips\" = \"Прямі IP\"\n\"directdomains\" = \"Прямі домени\"\n\"ipv4Routing\" = \"Маршрутизація IPv4\"\n\"ipv4RoutingDesc\" = \"Ці параметри спрямовуватимуть трафік на основі певного призначення через IPv4.\"\n\"warpRouting\" = \"WARP Маршрутизація\"\n\"warpRoutingDesc\" = \"Ці параметри маршрутизуватимуть трафік на основі певного пункту призначення через WARP.\"\n\"Template\" = \"Шаблон розширеної конфігурації Xray\"\n\"TemplateDesc\" = \"Остаточний конфігураційний файл Xray буде створено на основі цього шаблону.\"\n\"FreedomStrategy\" = \"Стратегія протоколу свободи\"\n\"FreedomStrategyDesc\" = \"Установити стратегію виведення для мережі в протоколі свободи.\"\n\"RoutingStrategy\" = \"Загальна стратегія маршрутизації\"\n\"RoutingStrategyDesc\" = \"Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів.\"\n\"outboundTestUrl\" = \"URL тесту outbound\"\n\"outboundTestUrlDesc\" = \"URL для перевірки з'єднання outbound\"\n\"Torrent\" = \"Блокувати протокол BitTorrent\"\n\"Inbounds\" = \"Вхідні\"\n\"InboundsDesc\" = \"Прийняття певних клієнтів.\"\n\"Outbounds\" = \"Вихід\"\n\"Balancers\" = \"Балансери\"\n\"OutboundsDesc\" = \"Встановити шлях вихідного трафіку.\"\n\"Routings\" = \"Правила маршрутизації\"\n\"RoutingsDesc\" = \"Пріоритет кожного правила важливий!\"\n\"completeTemplate\" = \"Усі\"\n\"logLevel\" = \"Рівень журналу\"\n\"logLevelDesc\" = \"Рівень журналу для журналів помилок із зазначенням інформації, яку потрібно записати.\"\n\"accessLog\" = \"Журнал доступу\"\n\"accessLogDesc\" = \"Шлях до файлу журналу доступу. Спеціальне значення 'none' вимикає журнали доступу\"\n\"errorLog\" = \"Журнал помилок\"\n\"errorLogDesc\" = \"Шлях до файлу журналу помилок. Спеціальне значення 'none' вимикає журнали помилок\"\n\"dnsLog\" = \"Журнал DNS\"\n\"dnsLogDesc\" = \"Чи включити журнали запитів DNS\"\n\"maskAddress\" = \"Маскувати Адресу\"\n\"maskAddressDesc\" = \"Маска IP-адреси, при активації автоматично замінює IP-адресу, яка з'являється у журналі.\"\n\"statistics\" = \"Статистика\"\n\"statsInboundUplink\" = \"Статистика вхідного аплінку\"\n\"statsInboundUplinkDesc\" = \"Увімкнення збору статистики для вхідного трафіку всіх вхідних проксі.\"\n\"statsInboundDownlink\" = \"Статистика вхідного даунлінку\"\n\"statsInboundDownlinkDesc\" = \"Увімкнення збору статистики для вихідного трафіку всіх вхідних проксі.\"\n\"statsOutboundUplink\" = \"Статистика вихідного аплінку\"\n\"statsOutboundUplinkDesc\" = \"Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі.\"\n\"statsOutboundDownlink\" = \"Статистика вихідного даунлінку\"\n\"statsOutboundDownlinkDesc\" = \"Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі.\"\n\n[pages.xray.rules]\n\"first\" = \"Перший\"\n\"last\" = \"Останній\"\n\"up\" = \"Вгору\"\n\"down\" = \"Вниз\"\n\"source\" = \"Джерело\"\n\"dest\" = \"Пункт призначення\"\n\"inbound\" = \"Вхідний\"\n\"outbound\" = \"Вихідний\"\n\"balancer\" = \"Балансувальник\"\n\"info\" = \"Інформація\"\n\"add\" = \"Додати правило\"\n\"edit\" = \"Редагувати правило\"\n\"useComma\" = \"Елементи, розділені комами\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"Додати вихідний\"\n\"addReverse\" = \"Додати реверс\"\n\"editOutbound\" = \"Редагувати вихідні\"\n\"editReverse\" = \"Редагувати реверс\"\n\"tag\" = \"Тег\"\n\"tagDesc\" = \"Унікальний тег\"\n\"address\" = \"Адреса\"\n\"reverse\" = \"Зворотний\"\n\"domain\" = \"Домен\"\n\"type\" = \"Тип\"\n\"bridge\" = \"Міст\"\n\"portal\" = \"Портал\"\n\"link\" = \"Посилання\"\n\"intercon\" = \"Взаємозв'язок\"\n\"settings\" = \"Налаштування\"\n\"accountInfo\" = \"Інформація про обліковий запис\"\n\"outboundStatus\" = \"Статус виходу\"\n\"sendThrough\" = \"Надіслати через\"\n\"test\" = \"Тест\"\n\"testResult\" = \"Результат тесту\"\n\"testing\" = \"Тестування з'єднання...\"\n\"testSuccess\" = \"Тест успішний\"\n\"testFailed\" = \"Тест не пройдено\"\n\"testError\" = \"Не вдалося протестувати вихідне з'єднання\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"Додати балансир\"\n\"editBalancer\" = \"Редагувати балансир\"\n\"balancerStrategy\" = \"Стратегія\"\n\"balancerSelectors\" = \"Селектори\"\n\"tag\" = \"Тег\"\n\"tagDesc\" = \"Унікальний тег\"\n\"balancerDesc\" = \"Неможливо використовувати balancerTag і outboundTag одночасно. Якщо використовувати одночасно, працюватиме лише outboundTag.\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"Приватний ключ\"\n\"publicKey\" = \"Публічний ключ\"\n\"allowedIPs\" = \"Дозволені IP-адреси\"\n\"endpoint\" = \"Кінцева точка\"\n\"psk\" = \"Спільний ключ\"\n\"domainStrategy\" = \"Стратегія домену\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"Назва інтерфейсу TUN. Значення за замовчуванням - 'xray0'\"\n\"mtuDesc\" = \"Максимальна одиниця передачі. Максимальний розмір пакетів даних. Значення за замовчуванням - 1500\"\n\"userLevel\" = \"Рівень користувача\"\n\"userLevelDesc\" = \"Всі з'єднання, встановлені через цей вхід, використовуватимуть цей рівень користувача. Значення за замовчуванням - 0\"\n\n[pages.xray.dns]\n\"enable\" = \"Увімкнути DNS\"\n\"enableDesc\" = \"Увімкнути вбудований DNS-сервер\"\n\"tag\" = \"Мітка вхідного DNS\"\n\"tagDesc\" = \"Ця мітка буде доступна як вхідна мітка в правилах маршрутизації.\"\n\"clientIp\" = \"IP клієнта\"\n\"clientIpDesc\" = \"Використовується для повідомлення серверу про вказане місцезнаходження IP під час DNS-запитів\"\n\"disableCache\" = \"Вимкнути кеш\"\n\"disableCacheDesc\" = \"Вимкнути кешування DNS\"\n\"disableFallback\" = \"Вимкнути резервний DNS\"\n\"disableFallbackDesc\" = \"Вимкнути резервні DNS-запити\"\n\"disableFallbackIfMatch\" = \"Вимкнути резервний DNS при збігу\"\n\"disableFallbackIfMatchDesc\" = \"Вимкнути резервні DNS-запити при збігу списку доменів DNS-сервера\"\n\"enableParallelQuery\" = \"Увімкнути паралельні запити\"\n\"enableParallelQueryDesc\" = \"Увімкнути паралельні DNS-запити до кількох серверів для швидшого вирішення\"\n\"strategy\" = \"Стратегія запиту\"\n\"strategyDesc\" = \"Загальна стратегія вирішення доменних імен\"\n\"add\" = \"Додати сервер\"\n\"edit\" = \"Редагувати сервер\"\n\"domains\" = \"Домени\"\n\"expectIPs\" = \"Очікувані IP\"\n\"unexpectIPs\" = \"Неочікувані IP\"\n\"useSystemHosts\" = \"Використовувати системні Hosts\"\n\"useSystemHostsDesc\" = \"Використовувати файл hosts з встановленої системи\"\n\"usePreset\" = \"Використати шаблон\"\n\"dnsPresetTitle\" = \"Шаблони DNS\"\n\"dnsPresetFamily\" = \"Сімейний\"\n\n[pages.xray.fakedns]\n\"add\" = \"Додати підроблений DNS\"\n\"edit\" = \"Редагувати підроблений DNS\"\n\"ipPool\" = \"Підмережа IP-пулу\"\n\"poolSize\" = \"Розмір пулу\"\n\n[pages.settings.security]\n\"admin\" = \"Облікові дані адміністратора\"\n\"twoFactor\" = \"Двофакторна аутентифікація\"\n\"twoFactorEnable\" = \"Увімкнути 2FA\"\n\"twoFactorEnableDesc\" = \"Додає додатковий рівень аутентифікації для підвищення безпеки.\"\n\"twoFactorModalSetTitle\" = \"Увімкнути двофакторну аутентифікацію\"\n\"twoFactorModalDeleteTitle\" = \"Вимкнути двофакторну аутентифікацію\"\n\"twoFactorModalSteps\" = \"Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:\"\n\"twoFactorModalFirstStep\" = \"1. Відскануйте цей QR-код у програмі для аутентифікації або скопіюйте токен біля QR-коду та вставте його в програму\"\n\"twoFactorModalSecondStep\" = \"2. Введіть код з програми\"\n\"twoFactorModalRemoveStep\" = \"Введіть код з програми, щоб вимкнути двофакторну аутентифікацію.\"\n\"twoFactorModalChangeCredentialsTitle\" = \"Змінити облікові дані\"\n\"twoFactorModalChangeCredentialsStep\" = \"Введіть код з додатку, щоб змінити облікові дані адміністратора.\"\n\"twoFactorModalSetSuccess\" = \"Двофакторна аутентифікація була успішно встановлена\"\n\"twoFactorModalDeleteSuccess\" = \"Двофакторна аутентифікація була успішно видалена\"\n\"twoFactorModalError\" = \"Невірний код\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"Параметри було змінено.\"\n\"getSettings\" = \"Виникла помилка під час отримання параметрів.\"\n\"modifyUserError\" = \"Виникла помилка під час зміни облікових даних адміністратора.\"\n\"modifyUser\" = \"Ви успішно змінили облікові дані адміністратора.\"\n\"originalUserPassIncorrect\" = \"Поточне ім'я користувача або пароль недійсні\"\n\"userPassMustBeNotEmpty\" = \"Нове ім'я користувача та пароль порожні\"\n\"getOutboundTrafficError\" = \"Помилка отримання вихідного трафіку\"\n\"resetOutboundTrafficError\" = \"Помилка скидання вихідного трафіку\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ Клавіатуру закрито!\"\n\"noResult\" = \"❗ Немає результату!\"\n\"noQuery\" = \"❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!\"\n\"wentWrong\" = \"❌ Щось пішло не так!\"\n\"noIpRecord\" = \"❗ Немає запису IP!\"\n\"noInbounds\" = \"❗ Вхідні не знайдені!\"\n\"unlimited\" = \"♾ Необмежено (Скинути)\"\n\"add\" = \"Додати\"\n\"month\" = \"Місяць\"\n\"months\" = \"Місяці\"\n\"day\" = \"День\"\n\"days\" = \"Дні\"\n\"hours\" = \"Години\"\n\"minutes\" = \"Хвилини\"\n\"unknown\" = \"Невідомо\"\n\"inbounds\" = \"Вхідні\"\n\"clients\" = \"Клієнти\"\n\"offline\" = \"🔴 Офлайн\"\n\"online\" = \"🟢 Онлайн\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ Невідома команда.\"\n\"pleaseChoose\" = \"👇 Будь ласка, виберіть:\\r\\n\"\n\"help\" = \"🤖 Ласкаво просимо до цього бота! Він розроблений, щоб надавати певні дані з веб-панелі та дозволяє вносити зміни за потреби.\\r\\n\\r\\n\"\n\"start\" = \"👋 Привіт <i>{{ .Firstname }}</i>.\\r\\n\"\n\"welcome\" = \"🤖 Ласкаво просимо до <b>{{ .Hostname }}</b> бота керування.\\r\\n\"\n\"status\" = \"✅ Бот в порядку!\"\n\"usage\" = \"❗ Введіть текст для пошуку!\"\n\"getID\" = \"🆔 Ваш ідентифікатор: <code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"Для перезапуску Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\nДля пошуку електронної пошти клієнта:\\r\\n<code>/usage [Електронна пошта]</code>\\r\\n\\r\\nДля пошуку вхідних (зі статистикою клієнта):\\r\\n<code>/inbound [Примітка]</code>\\r\\n\\r\\nID чату Telegram:\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"Для пошуку статистики використовуйте наступну команду:\\r\\n<code>/usage [Електронна пошта]</code>\\r\\n\\r\\nID чату Telegram:\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ Операція успішна!\"\n\"restartFailed\" = \"❗ Помилка в операції.\\r\\n\\r\\n<code>Помилка: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core не запущений.\"\n\"startDesc\" = \"Показати головне меню\"\n\"helpDesc\" = \"Довідка по боту\"\n\"statusDesc\" = \"Перевірити статус бота\"\n\"idDesc\" = \"Показати ваш Telegram ID\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 Навантаження ЦП  {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%\"\n\"selectUserFailed\" = \"❌ Помилка під час вибору користувача!\"\n\"userSaved\" = \"✅ Користувача Telegram збережено.\"\n\"loginSuccess\" = \"✅ Успішно ввійшли в панель\\r\\n\"\n\"loginFailed\" = \"❗️ Помилка входу в панель.\\r\\n\"\n\"2faFailed\" = \"Помилка 2FA\"\n\"report\" = \"🕰 Заплановані звіти: {{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ Дата й час: {{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 Хост: {{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 3X-UI Версія: {{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Xray Версія: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IP-адреси:\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ Час роботи: {{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 Завантаження системи: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 RAM: {{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP: {{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP: {{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 Трафік: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Статус: {{ .State }}\\r\\n\"\n\"username\" = \"👤 Ім'я користувача: {{ .Username }}\\r\\n\"\n\"password\" = \"👤 Пароль: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ Час: {{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 Inbound: {{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 Порт: {{ .Port }}\\r\\n\"\n\"expire\" = \"📅 Дата закінчення: {{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 Термін дії: {{ .Time }}\\r\\n\"\n\"active\" = \"💡 Активний: {{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 Увімкнено: {{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 Стан підключення: {{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 Був(ла) онлайн: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 Електронна пошта: {{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 Upload: ↑{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 Download: ↓{{ .Download }}\\r\\n\"\n\"total\" = \"📊 Всього: ↑↓{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 Користувач Telegram: {{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 Вичерпано {{ .Type }}:\\r\\n\"\n\"exhaustedCount\" = \"🚨 Вичерпано кількість {{ .Type }} count:\\r\\n\"\n\"onlinesCount\" = \"🌐 Онлайн-клієнти: {{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 Вимкнено: {{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 Скоро вичерпається: {{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 Час резервного копіювання: {{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 Оновлено: {{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ Так\"\n\"no\" = \"❌ Ні\"\n\"received_id\" = \"🔑📥 ID оновлено.\"\n\"received_password\" = \"🔑📥 Пароль оновлено.\"\n\"received_email\" = \"📧📥 Електронна пошта оновлена.\"\n\"received_comment\" = \"💬📥 Коментар оновлено.\"\n\"id_prompt\" = \"🔑 Стандартний ID: {{ .ClientId }}\\n\\nВведіть ваш ID.\"\n\"pass_prompt\" = \"🔑 Стандартний пароль: {{ .ClientPassword }}\\n\\nВведіть ваш пароль.\"\n\"email_prompt\" = \"📧 Стандартний email: {{ .ClientEmail }}\\n\\nВведіть ваш email.\"\n\"comment_prompt\" = \"💬 Стандартний коментар: {{ .ClientComment }}\\n\\nВведіть ваш коментар.\"\n\"inbound_client_data_id\" = \"🔄 Вхід: {{ .InboundRemark }}\\n\\n🔑 ID: {{ .ClientId }}\\n📧 Електронна пошта: {{ .ClientEmail }}\\n📊 Трафік: {{ .ClientTraffic }}\\n📅 Дата завершення: {{ .ClientExp }}\\n🌐 Обмеження IP: {{ .IpLimit }}\\n💬 Коментар: {{ .ClientComment }}\\n\\nТепер ви можете додати клієнта до вхідного з'єднання!\"\n\"inbound_client_data_pass\" = \"🔄 Вхід: {{ .InboundRemark }}\\n\\n🔑 Пароль: {{ .ClientPass }}\\n📧 Електронна пошта: {{ .ClientEmail }}\\n📊 Трафік: {{ .ClientTraffic }}\\n📅 Дата завершення: {{ .ClientExp }}\\n🌐 Обмеження IP: {{ .IpLimit }}\\n💬 Коментар: {{ .ClientComment }}\\n\\nТепер ви можете додати клієнта до вхідного з'єднання!\"\n\"cancel\" = \"❌ Процес скасовано! \\n\\nВи можете знову розпочати, використовуючи /start у будь-який час. 🔄\"\n\"error_add_client\" = \"⚠️ Помилка:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"Гаразд, залишу значення за замовчуванням. 😊\"\n\"incorrect_input\" = \"Ваш ввід невірний.\\nФрази повинні бути без пробілів.\\nПравильний приклад: aaaaaa\\nНеправильний приклад: aaa aaa 🚫\"\n\"AreYouSure\" = \"Ви впевнені? 🤔\"\n\"SuccessResetTraffic\" = \"📧 Електронна пошта: {{ .ClientEmail }}\\n🏁 Результат: ✅ Успішно\"\n\"FailedResetTraffic\" = \"📧 Електронна пошта: {{ .ClientEmail }}\\n🏁 Результат: ❌ Невдача \\n\\n🛠️ Помилка: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 Процес скидання трафіку завершено для всіх клієнтів.\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ Закрити клавіатуру\"\n\"cancel\" = \"❌ Скасувати\"\n\"cancelReset\" = \"❌ Скасувати скидання\"\n\"cancelIpLimit\" = \"❌ Скасувати обмеження IP\"\n\"confirmResetTraffic\" = \"✅ Підтвердити скидання трафіку?\"\n\"confirmClearIps\" = \"✅ Підтвердити очищення IP-адрес?\"\n\"confirmRemoveTGUser\" = \"✅ Підтвердити видалення користувача Telegram?\"\n\"confirmToggle\" = \"✅ Підтвердити ввімкнути/вимкнути користувача?\"\n\"dbBackup\" = \"Отримати резервну копію БД\"\n\"serverUsage\" = \"Використання сервера\"\n\"getInbounds\" = \"Отримати вхідні\"\n\"depleteSoon\" = \"Скоро вичерпати\"\n\"clientUsage\" = \"Отримати використання\"\n\"onlines\" = \"Онлайн-клієнти\"\n\"commands\" = \"Команди\"\n\"refresh\" = \"🔄 Оновити\"\n\"clearIPs\" = \"❌ Очистити IP-адреси\"\n\"removeTGUser\" = \"❌ Видалити користувача Telegram\"\n\"selectTGUser\" = \"👤 Виберіть користувача Telegram\"\n\"selectOneTGUser\" = \"👤 Виберіть користувача Telegram:\"\n\"resetTraffic\" = \"📈 Скинути трафік\"\n\"resetExpire\" = \"📅 Змінити термін дії\"\n\"ipLog\" = \"🔢 IP журнал\"\n\"ipLimit\" = \"🔢 IP Ліміт\"\n\"setTGUser\" = \"👤 Встановити користувача Telegram\"\n\"toggle\" = \"🔘 Увімкнути / Вимкнути\"\n\"custom\" = \"🔢 Custom\"\n\"confirmNumber\" = \"✅ Підтвердити: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ Підтвердити додавання: {{ .Num }}\"\n\"limitTraffic\" = \"🚧 Ліміт трафіку\"\n\"getBanLogs\" = \"Отримати журнали заборон\"\n\"allClients\" = \"Всі Клієнти\"\n\"addClient\" = \"Додати клієнта\"\n\"submitDisable\" = \"Надіслати як вимкнено ☑️\"\n\"submitEnable\" = \"Надіслати як увімкнено ✅\"\n\"use_default\" = \"🏷️ Використати типове\"\n\"change_id\" = \"⚙️🔑 ID\"\n\"change_password\" = \"⚙️🔑 Пароль\"\n\"change_email\" = \"⚙️📧 Електронна пошта\"\n\"change_comment\" = \"⚙️💬 Коментар\"\n\"ResetAllTraffics\" = \"Скинути весь трафік\"\n\"SortedTrafficUsageReport\" = \"Відсортований звіт про використання трафіку\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ Операція успішна!\"\n\"errorOperation\" = \"❗ Помилка в роботі.\"\n\"getInboundsFailed\" = \"❌ Не вдалося отримати вхідні повідомлення.\"\n\"getClientsFailed\" = \"❌ Не вдалося отримати клієнтів.\"\n\"canceled\" = \"❌ {{ .Email }}: Операцію скасовано.\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}: Клієнт успішно оновлено.\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}: IP-адреси успішно оновлено.\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}: Користувач Telegram клієнта успішно оновлено.\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}: Трафік скинуто успішно.\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: Ліміт трафіку успішно збережено.\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}: Успішно скинуто дні закінчення терміну дії.\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}: IP обмеження {{ .Count }} успішно збережено.\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}: IP успішно очищено.\"\n\"getIpLog\" = \"✅ {{ .Email }}: Отримати IP-журнал.\"\n\"getUserInfo\" = \"✅ {{ .Email }}: Отримати інформацію про користувача Telegram.\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}: Користувача Telegram видалено успішно.\"\n\"enableSuccess\" = \"✅ {{ .Email }}: Увімкнути успішно.\"\n\"disableSuccess\" = \"✅ {{ .Email }}: Успішно вимкнено.\"\n\"askToAddUserId\" = \"Вашу конфігурацію не знайдено!\\r\\nБудь ласка, попросіть свого адміністратора використовувати ваш ідентифікатор Telegram у вашій конфігурації.\\r\\n\\r\\nВаш ідентифікатор користувача: <code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"Виберіть клієнта для Вхідного {{ .Inbound }}\"\n\"chooseInbound\" = \"Виберіть Вхідний\"\n"
  },
  {
    "path": "web/translation/translate.vi_VN.toml",
    "content": "\"username\" = \"Tên người dùng\"\r\n\"password\" = \"Mật khẩu\"\r\n\"login\" = \"Đăng nhập\"\r\n\"confirm\" = \"Xác nhận\"\r\n\"cancel\" = \"Hủy bỏ\"\r\n\"close\" = \"Đóng\"\r\n\"create\" = \"Tạo\"\r\n\"update\" = \"Cập nhật\"\r\n\"copy\" = \"Sao chép\"\r\n\"copied\" = \"Đã sao chép\"\r\n\"download\" = \"Tải xuống\"\r\n\"remark\" = \"Ghi chú\"\r\n\"enable\" = \"Kích hoạt\"\r\n\"protocol\" = \"Giao thức\"\r\n\"search\" = \"Tìm kiếm\"\r\n\"filter\" = \"Bộ lọc\"\r\n\"loading\" = \"Đang tải\"\r\n\"second\" = \"Giây\"\r\n\"minute\" = \"Phút\"\r\n\"hour\" = \"Giờ\"\r\n\"day\" = \"Ngày\"\r\n\"check\" = \"Kiểm tra\"\r\n\"indefinite\" = \"Không xác định\"\r\n\"unlimited\" = \"Không giới hạn\"\r\n\"none\" = \"None\"\r\n\"qrCode\" = \"Mã QR\"\r\n\"info\" = \"Thông tin thêm\"\r\n\"edit\" = \"Chỉnh sửa\"\r\n\"delete\" = \"Xóa\"\r\n\"reset\" = \"Đặt lại\"\r\n\"noData\" = \"Không có dữ liệu.\"\r\n\"copySuccess\" = \"Đã sao chép thành công\"\r\n\"sure\" = \"Chắc chắn\"\r\n\"encryption\" = \"Mã hóa\"\r\n\"useIPv4ForHost\" = \"Sử dụng IPv4 cho máy chủ\"\r\n\"transmission\" = \"Truyền tải\"\r\n\"host\" = \"Máy chủ\"\r\n\"path\" = \"Đường dẫn\"\r\n\"camouflage\" = \"Ngụy trang\"\r\n\"status\" = \"Trạng thái\"\r\n\"enabled\" = \"Đã kích hoạt\"\r\n\"disabled\" = \"Đã tắt\"\r\n\"depleted\" = \"Depleted\"\r\n\"depletingSoon\" = \"Depleting...\"\r\n\"offline\" = \"Ngoại tuyến\"\r\n\"online\" = \"Trực tuyến\"\r\n\"domainName\" = \"Tên miền\"\r\n\"monitor\" = \"Listening IP\"\r\n\"certificate\" = \"Chứng chỉ số\"\r\n\"fail\" = \"Thất bại\"\r\n\"comment\" = \"Bình luận\"\r\n\"success\" = \"Thành công\"\r\n\"lastOnline\" = \"Lần online gần nhất\"\r\n\"getVersion\" = \"Lấy phiên bản\"\r\n\"install\" = \"Cài đặt\"\r\n\"clients\" = \"Các khách hàng\"\r\n\"usage\" = \"Sử dụng\"\r\n\"twoFactorCode\" = \"Mã\"\r\n\"remained\" = \"Còn lại\"\r\n\"security\" = \"Bảo vệ\"\r\n\"secAlertTitle\" = \"Cảnh báo an ninh-Tiếng Việt by Ohoang7\"\r\n\"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\"\r\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.\"\r\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.\"\r\n\"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ể.\"\r\n\"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.\"\r\n\"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.\"\r\n\"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.\"\r\n\"emptyDnsDesc\" = \"Không có máy chủ DNS nào được thêm.\"\r\n\"emptyFakeDnsDesc\" = \"Không có máy chủ Fake DNS nào được thêm.\"\r\n\"emptyBalancersDesc\" = \"Không có bộ cân bằng tải nào được thêm.\"\r\n\"emptyReverseDesc\" = \"Không có proxy ngược nào được thêm.\"\r\n\"somethingWentWrong\" = \"Đã xảy ra lỗi\"\r\n\r\n[subscription]\r\n\"title\" = \"Thông tin đăng ký\"\r\n\"subId\" = \"ID đăng ký\"\r\n\"status\" = \"Trạng thái\"\r\n\"downloaded\" = \"Đã tải xuống\"\r\n\"uploaded\" = \"Đã tải lên\"\r\n\"expiry\" = \"Hết hạn\"\r\n\"totalQuota\" = \"Tổng hạn mức\"\r\n\"individualLinks\" = \"Liên kết riêng lẻ\"\r\n\"active\" = \"Hoạt động\"\r\n\"inactive\" = \"Không hoạt động\"\r\n\"unlimited\" = \"Không giới hạn\"\r\n\"noExpiry\" = \"Không hết hạn\"\r\n\r\n[menu]\r\n\"theme\" = \"Chủ đề\"\r\n\"dark\" = \"Tối\"\r\n\"ultraDark\" = \"Siêu tối\"\r\n\"dashboard\" = \"Trạng thái hệ thống\"\r\n\"inbounds\" = \"Đầu vào khách hàng\"\r\n\"settings\" = \"Cài đặt bảng điều khiển\"\r\n\"logout\" = \"Đăng xuất\"\r\n\"xray\" = \"Cài đặt Xray\"\r\n\"link\" = \"Quản lý\"\r\n\r\n[pages.login]\r\n\"hello\" = \"Xin chào\"\r\n\"title\" = \"Chào mừng\"\r\n\"loginAgain\" = \"Thời hạn đăng nhập đã hết. Vui lòng đăng nhập lại.\"\r\n\r\n[pages.login.toasts]\r\n\"invalidFormData\" = \"Dạng dữ liệu nhập không hợp lệ.\"\r\n\"emptyUsername\" = \"Vui lòng nhập tên người dùng.\"\r\n\"emptyPassword\" = \"Vui lòng nhập mật khẩu.\"\r\n\"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ệ.\"\r\n\"successLogin\" = \"Bạn đã đăng nhập vào tài khoản thành công.\"\r\n\r\n[pages.index]\r\n\"title\" = \"Trạng thái hệ thống\"\r\n\"cpu\" = \"CPU\"\r\n\"logicalProcessors\" = \"Bộ xử lý logic\"\r\n\"frequency\" = \"Tần số\"\r\n\"swap\" = \"Swap\"\r\n\"storage\" = \"Lưu trữ\"\r\n\"memory\" = \"RAM\"\r\n\"threads\" = \"Luồng\"\r\n\"xrayStatus\" = \"Xray\"\r\n\"stopXray\" = \"Dừng lại\"\r\n\"restartXray\" = \"Khởi động lại\"\r\n\"xraySwitch\" = \"Phiên bản\"\r\n\"xraySwitchClick\" = \"Chọn phiên bản mà bạn muốn chuyển đổi sang.\"\r\n\"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.\"\r\n\"xrayStatusUnknown\" = \"Không xác định\"\r\n\"xrayStatusRunning\" = \"Đang chạy\"\r\n\"xrayStatusStop\" = \"Dừng\"\r\n\"xrayStatusError\" = \"Lỗi\"\r\n\"xrayErrorPopoverTitle\" = \"Đã xảy ra lỗi khi chạy Xray\"\r\n\"operationHours\" = \"Thời gian hoạt động\"\r\n\"systemLoad\" = \"Tải hệ thống\"\r\n\"systemLoadDesc\" = \"trung bình tải hệ thống trong 1, 5 và 15 phút qua\"\r\n\"connectionCount\" = \"Số lượng kết nối\"\r\n\"ipAddresses\" = \"Địa chỉ IP\"\r\n\"toggleIpVisibility\" = \"Chuyển đổi hiển thị IP\"\r\n\"overallSpeed\" = \"Tốc độ tổng thể\"\r\n\"upload\" = \"Tải lên\"\r\n\"download\" = \"Tải xuống\"\r\n\"totalData\" = \"Tổng dữ liệu\"\r\n\"sent\" = \"Đã gửi\"\r\n\"received\" = \"Đã nhận\"\r\n\"documentation\" = \"Tài liệu\"\r\n\"xraySwitchVersionDialog\" = \"Bạn có chắc chắn muốn thay đổi phiên bản Xray không?\"\r\n\"xraySwitchVersionDialogDesc\" = \"Hành động này sẽ thay đổi phiên bản Xray thành #version#.\"\r\n\"xraySwitchVersionPopover\" = \"Xray đã được cập nhật thành công\"\r\n\"geofileUpdateDialog\" = \"Bạn có chắc chắn muốn cập nhật geofile không?\"\r\n\"geofileUpdateDialogDesc\" = \"Hành động này sẽ cập nhật tệp #filename#.\"\r\n\"geofilesUpdateDialogDesc\" = \"Thao tác này sẽ cập nhật tất cả các tập tin.\"\r\n\"geofilesUpdateAll\" = \"Cập nhật tất cả\"\r\n\"geofileUpdatePopover\" = \"Geofile đã được cập nhật thành công\"\r\n\"dontRefresh\" = \"Đang tiến hành cài đặt, vui lòng không làm mới trang này.\"\r\n\"logs\" = \"Nhật ký\"\r\n\"config\" = \"Cấu hình\"\r\n\"backup\" = \"Sao lưu\"\r\n\"backupTitle\" = \"Sao lưu & Khôi phục Cơ sở dữ liệu\"\r\n\"exportDatabase\" = \"Sao lưu\"\r\n\"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ị.\"\r\n\"importDatabase\" = \"Khôi phục\"\r\n\"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.\"\r\n\"importDatabaseSuccess\" = \"Đã nhập cơ sở dữ liệu thành công\"\r\n\"importDatabaseError\" = \"Lỗi xảy ra khi nhập cơ sở dữ liệu\"\r\n\"readDatabaseError\" = \"Lỗi xảy ra khi đọc cơ sở dữ liệu\"\r\n\"getDatabaseError\" = \"Lỗi xảy ra khi truy xuất cơ sở dữ liệu\"\r\n\"getConfigError\" = \"Lỗi xảy ra khi truy xuất tệp cấu hình\"\r\n\r\n[pages.inbounds]\r\n\"allTimeTraffic\" = \"Tổng Lưu Lượng\"\r\n\"allTimeTrafficUsage\" = \"Tổng mức sử dụng mọi lúc\"\r\n\"title\" = \"Điểm vào (Inbounds)\"\r\n\"totalDownUp\" = \"Tổng tải lên/tải xuống\"\r\n\"totalUsage\" = \"Tổng sử dụng\"\r\n\"inboundCount\" = \"Số lượng điểm vào\"\r\n\"operate\" = \"Thao tác\"\r\n\"enable\" = \"Kích hoạt\"\r\n\"remark\" = \"Chú thích\"\r\n\"protocol\" = \"Giao thức\"\r\n\"port\" = \"Cổng\"\r\n\"portMap\" = \"Cổng tạo\"\r\n\"traffic\" = \"Lưu lượng\"\r\n\"details\" = \"Chi tiết\"\r\n\"transportConfig\" = \"Giao vận\"\r\n\"expireDate\" = \"Ngày hết hạn\"\r\n\"createdAt\" = \"Tạo lúc\"\r\n\"updatedAt\" = \"Cập nhật\"\r\n\"resetTraffic\" = \"Đặt lại lưu lượng\"\r\n\"addInbound\" = \"Thêm điểm vào\"\r\n\"generalActions\" = \"Hành động chung\"\r\n\"autoRefresh\" = \"Tự động làm mới\"\r\n\"autoRefreshInterval\" = \"Khoảng thời gian\"\r\n\"modifyInbound\" = \"Chỉnh sửa điểm vào (Inbound)\"\r\n\"deleteInbound\" = \"Xóa điểm vào (Inbound)\"\r\n\"deleteInboundContent\" = \"Xác nhận xóa điểm vào? (Inbound)\"\r\n\"deleteClient\" = \"Xóa người dùng\"\r\n\"deleteClientContent\" = \"Bạn có chắc chắn muốn xóa người dùng không?\"\r\n\"resetTrafficContent\" = \"Xác nhận đặt lại lưu lượng?\"\r\n\"copyLink\" = \"Sao chép liên kết\"\r\n\"address\" = \"Địa chỉ\"\r\n\"network\" = \"Mạng\"\r\n\"destinationPort\" = \"Cổng đích\"\r\n\"targetAddress\" = \"Địa chỉ mục tiêu\"\r\n\"monitorDesc\" = \"Mặc định để trống\"\r\n\"meansNoLimit\" = \"= Không giới hạn (đơn vị: GB)\"\r\n\"totalFlow\" = \"Tổng lưu lượng\"\r\n\"leaveBlankToNeverExpire\" = \"Để trống để không bao giờ hết hạn\"\r\n\"noRecommendKeepDefault\" = \"Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định\"\r\n\"certificatePath\" = \"Đường dẫn tập\"\r\n\"certificateContent\" = \"Nội dung tập\"\r\n\"publicKey\" = \"Khóa công khai\"\r\n\"privatekey\" = \"Khóa cá nhân\"\r\n\"clickOnQRcode\" = \"Nhấn vào Mã QR để sao chép\"\r\n\"client\" = \"Người dùng\"\r\n\"export\" = \"Xuất liên kết\"\r\n\"clone\" = \"Sao chép\"\r\n\"cloneInbound\" = \"Sao chép điểm vào (Inbound)\"\r\n\"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.\"\r\n\"cloneInboundOk\" = \"Sao chép\"\r\n\"resetAllTraffic\" = \"Đặt lại lưu lượng cho tất cả điểm vào\"\r\n\"resetAllTrafficTitle\" = \"Đặt lại lưu lượng cho tất cả điểm vào\"\r\n\"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?\"\r\n\"resetInboundClientTraffics\" = \"Đặt lại lưu lượng toàn bộ người dùng của điểm vào\"\r\n\"resetInboundClientTrafficTitle\" = \"Đặt lại lưu lượng cho toàn bộ người dùng của điểm vào\"\r\n\"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?\"\r\n\"resetAllClientTraffics\" = \"Đặt lại lưu lượng cho toàn bộ người dùng\"\r\n\"resetAllClientTrafficTitle\" = \"Đặt lại lưu lượng cho toàn bộ người dùng\"\r\n\"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?\"\r\n\"delDepletedClients\" = \"Xóa các người dùng đã cạn kiệt\"\r\n\"delDepletedClientsTitle\" = \"Xóa các người dùng đã cạn kiệt\"\r\n\"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?\"\r\n\"email\" = \"Email\"\r\n\"emailDesc\" = \"Vui lòng cung cấp một địa chỉ email duy nhất.\"\r\n\"IPLimit\" = \"Giới hạn IP\"\r\n\"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).\"\r\n\"IPLimitlog\" = \"Lịch sử IP\"\r\n\"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ử).\"\r\n\"IPLimitlogclear\" = \"Xóa Lịch sử\"\r\n\"setDefaultCert\" = \"Đặt chứng chỉ từ bảng điều khiển\"\r\n\"telegramDesc\" = \"Vui lòng cung cấp ID Trò chuyện Telegram. (sử dụng lệnh '/id' trong bot) hoặc (@userinfobot)\"\r\n\"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\"\r\n\"info\" = \"Thông tin\"\r\n\"same\" = \"Giống nhau\"\r\n\"inboundData\" = \"Dữ liệu gửi đến\"\r\n\"exportInbound\" = \"Xuất nhập khẩu\"\r\n\"import\" = \"Nhập\"\r\n\"importInbound\" = \"Nhập inbound\"\r\n\"periodicTrafficResetTitle\" = \"Đặt lại lưu lượng\"\r\n\"periodicTrafficResetDesc\" = \"Tự động đặt lại bộ đếm lưu lượng theo khoảng thời gian xác định\"\r\n\"lastReset\" = \"Đặt lại lần cuối\"\r\n\r\n[pages.client]\r\n\"add\" = \"Thêm người dùng\"\r\n\"edit\" = \"Chỉnh sửa người dùng\"\r\n\"submitAdd\" = \"Thêm\"\r\n\"submitEdit\" = \"Lưu thay đổi\"\r\n\"clientCount\" = \"Số lượng người dùng\"\r\n\"bulk\" = \"Thêm hàng loạt\"\r\n\"method\" = \"Phương pháp\"\r\n\"first\" = \"Đầu tiên\"\r\n\"last\" = \"Cuối cùng\"\r\n\"prefix\" = \"Tiền tố\"\r\n\"postfix\" = \"Hậu tố\"\r\n\"delayedStart\" = \"Bắt đầu ở Lần Đầu\"\r\n\"expireDays\" = \"Khoảng thời gian\"\r\n\"days\" = \"ngày\"\r\n\"renew\" = \"Tự động gia hạn\"\r\n\"renewDesc\" = \"Tự động gia hạn sau khi hết hạn. (0 = tắt)(đơn vị: ngày)\"\r\n\r\n[pages.inbounds.periodicTrafficReset]\r\n\"never\" = \"Không bao giờ\"\r\n\"daily\" = \"Hàng ngày\"\r\n\"weekly\" = \"Hàng tuần\"\r\n\"monthly\" = \"Hàng tháng\"\r\n\r\n[pages.inbounds.toasts]\r\n\"obtain\" = \"Nhận\"\r\n\"updateSuccess\" = \"Cập nhật thành công\"\r\n\"logCleanSuccess\" = \"Đã xóa nhật ký\"\r\n\"inboundsUpdateSuccess\" = \"Đã cập nhật thành công các kết nối inbound\"\r\n\"inboundUpdateSuccess\" = \"Đã cập nhật thành công kết nối inbound\"\r\n\"inboundCreateSuccess\" = \"Đã tạo thành công kết nối inbound\"\r\n\"inboundDeleteSuccess\" = \"Đã xóa thành công kết nối inbound\"\r\n\"inboundClientAddSuccess\" = \"Đã thêm client inbound\"\r\n\"inboundClientDeleteSuccess\" = \"Đã xóa client inbound\"\r\n\"inboundClientUpdateSuccess\" = \"Đã cập nhật client inbound\"\r\n\"delDepletedClientsSuccess\" = \"Đã xóa tất cả client hết hạn\"\r\n\"resetAllClientTrafficSuccess\" = \"Đã đặt lại toàn bộ lưu lượng client\"\r\n\"resetAllTrafficSuccess\" = \"Đã đặt lại toàn bộ lưu lượng\"\r\n\"resetInboundClientTrafficSuccess\" = \"Đã đặt lại lưu lượng\"\r\n\"trafficGetError\" = \"Lỗi khi lấy thông tin lưu lượng\"\r\n\"getNewX25519CertError\" = \"Lỗi khi lấy chứng chỉ X25519.\"\r\n\"getNewmldsa65Error\" = \"Lỗi khi lấy chứng chỉ mldsa65.\"\r\n\"getNewVlessEncError\" = \"Lỗi khi lấy chứng chỉ VlessEnc.\"\r\n\r\n[pages.inbounds.stream.general]\r\n\"request\" = \"Lời yêu cầu\"\r\n\"response\" = \"Phản ứng\"\r\n\"name\" = \"Tên\"\r\n\"value\" = \"Giá trị\"\r\n\r\n[pages.inbounds.stream.tcp]\r\n\"version\" = \"Phiên bản\"\r\n\"method\" = \"Phương pháp\"\r\n\"path\" = \"Đường dẫn\"\r\n\"status\" = \"Trạng thái\"\r\n\"statusDescription\" = \"Tình trạng Mô tả\"\r\n\"requestHeader\" = \"Header yêu cầu\"\r\n\"responseHeader\" = \"Header phản hồi\"\r\n\r\n[pages.settings]\r\n\"title\" = \"Cài đặt\"\r\n\"save\" = \"Lưu\"\r\n\"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.\"\r\n\"restartPanel\" = \"Khởi động lại bảng điều khiển\"\r\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ủ.\"\r\n\"restartPanelSuccess\" = \"Đã khởi động lại bảng điều khiển thành công\"\r\n\"actions\" = \"Hành động\"\r\n\"resetDefaultConfig\" = \"Đặt lại cấu hình mặc định\"\r\n\"panelSettings\" = \"Bảng điều khiển\"\r\n\"securitySettings\" = \"Bảo mật\"\r\n\"TGBotSettings\" = \"Bot Telegram\"\r\n\"panelListeningIP\" = \"IP Nghe của bảng điều khiển\"\r\n\"panelListeningIPDesc\" = \"Mặc định để trống để nghe tất cả các IP.\"\r\n\"panelListeningDomain\" = \"Tên miền của nghe bảng điều khiển\"\r\n\"panelListeningDomainDesc\" = \"Mặc định để trống để nghe tất cả các tên miền và IP\"\r\n\"panelPort\" = \"Cổng bảng điều khiển\"\r\n\"panelPortDesc\" = \"Cổng được sử dụng để kết nối với bảng điều khiển này\"\r\n\"publicKeyPath\" = \"Đường dẫn file chứng chỉ bảng điều khiển\"\r\n\"publicKeyPathDesc\" = \"Điền vào đường dẫn đầy đủ (bắt đầu từ '/')\"\r\n\"privateKeyPath\" = \"Đường dẫn file khóa của chứng chỉ bảng điều khiển\"\r\n\"privateKeyPathDesc\" = \"Điền vào đường dẫn đầy đủ (bắt đầu từ '/')\"\r\n\"panelUrlPath\" = \"Đường dẫn gốc URL bảng điều khiển\"\r\n\"panelUrlPathDesc\" = \"Phải bắt đầu và kết thúc bằng '/'\"\r\n\"pageSize\" = \"Kích thước phân trang\"\r\n\"pageSizeDesc\" = \"Xác định kích thước trang cho bảng gửi đến. Đặt 0 để tắt\"\r\n\"remarkModel\" = \"Ghi chú mô hình và ký tự phân tách\"\r\n\"datepicker\" = \"Kiểu lịch\"\r\n\"datepickerPlaceholder\" = \"Chọn ngày\"\r\n\"datepickerDescription\" = \"Tác vụ chạy theo lịch trình sẽ chạy theo kiểu lịch này.\"\r\n\"sampleRemark\" = \"Nhận xét mẫu\"\r\n\"oldUsername\" = \"Tên người dùng hiện tại\"\r\n\"currentPassword\" = \"Mật khẩu hiện tại\"\r\n\"newUsername\" = \"Tên người dùng mới\"\r\n\"newPassword\" = \"Mật khẩu mới\"\r\n\"telegramBotEnable\" = \"Bật Bot Telegram\"\r\n\"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\"\r\n\"telegramToken\" = \"Token Telegram\"\r\n\"telegramTokenDesc\" = \"Bạn phải nhận token từ quản lý bot Telegram @botfather\"\r\n\"telegramProxy\" = \"Socks5 Proxy\"\r\n\"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.\"\r\n\"telegramAPIServer\" = \"Telegram API Server\"\r\n\"telegramAPIServerDesc\" = \"Máy chủ API Telegram để sử dụng. Để trống để sử dụng máy chủ mặc định.\"\r\n\"telegramChatId\" = \"Chat ID Telegram của quản trị viên\"\r\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.\"\r\n\"telegramNotifyTime\" = \"Thời gian thông báo của bot Telegram\"\r\n\"telegramNotifyTimeDesc\" = \"Sử dụng định dạng thời gian Crontab.\"\r\n\"tgNotifyBackup\" = \"Sao lưu Cơ sở dữ liệu\"\r\n\"tgNotifyBackupDesc\" = \"Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo.\"\r\n\"tgNotifyLogin\" = \"Thông báo Đăng nhập\"\r\n\"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.\"\r\n\"sessionMaxAge\" = \"Thời gian tối đa của phiên\"\r\n\"sessionMaxAgeDesc\" = \"Thời gian của phiên đăng nhập (đơn vị: phút)\"\r\n\"expireTimeDiff\" = \"Ngưỡng hết hạn cho thông báo\"\r\n\"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)\"\r\n\"trafficDiff\" = \"Ngưỡng lưu lượng cho thông báo\"\r\n\"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)\"\r\n\"tgNotifyCpu\" = \"Ngưỡng cảnh báo tỷ lệ CPU\"\r\n\"tgNotifyCpuDesc\" = \"Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)\"\r\n\"timeZone\" = \"Múi giờ\"\r\n\"timeZoneDesc\" = \"Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này.\"\r\n\"subSettings\" = \"Gói đăng ký\"\r\n\"subEnable\" = \"Bật dịch vụ\"\r\n\"subEnableDesc\" = \"Tính năng gói đăng ký với cấu hình riêng\"\r\n\"subJsonEnable\" = \"Bật/Tắt điểm cuối đăng ký JSON độc lập.\"\r\n\"subTitle\" = \"Tiêu đề Đăng ký\"\r\n\"subTitleDesc\" = \"Tiêu đề hiển thị trong ứng dụng VPN\"\r\n\"subSupportUrl\" = \"URL Hỗ trợ\"\r\n\"subSupportUrlDesc\" = \"Liên kết hỗ trợ kỹ thuật hiển thị trong ứng dụng VPN\"\r\n\"subProfileUrl\" = \"URL Hồ sơ\"\r\n\"subProfileUrlDesc\" = \"Liên kết đến trang web của bạn hiển thị trong ứng dụng VPN\"\r\n\"subAnnounce\" = \"Thông báo\"\r\n\"subAnnounceDesc\" = \"Văn bản thông báo hiển thị trong ứng dụng VPN\"\r\n\"subEnableRouting\" = \"Bật định tuyến\"\r\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)\"\r\n\"subRoutingRules\" = \"Quy tắc định tuyến\"\r\n\"subRoutingRulesDesc\" = \"Quy tắc định tuyến toàn cầu cho client VPN. (Chỉ dành cho Happ)\"\r\n\"subListen\" = \"Listening IP\"\r\n\"subListenDesc\" = \"Mặc định để trống để nghe tất cả các IP\"\r\n\"subPort\" = \"Cổng gói đăng ký\"\r\n\"subPortDesc\" = \"Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ\"\r\n\"subCertPath\" = \"Đường dẫn file chứng chỉ gói đăng ký\"\r\n\"subCertPathDesc\" = \"Điền vào đường dẫn đầy đủ (bắt đầu với '/')\"\r\n\"subKeyPath\" = \"Đường dẫn file khóa của chứng chỉ gói đăng ký\"\r\n\"subKeyPathDesc\" = \"Điền vào đường dẫn đầy đủ (bắt đầu với '/')\"\r\n\"subPath\" = \"Đường dẫn gốc URL gói đăng ký\"\r\n\"subPathDesc\" = \"Phải bắt đầu và kết thúc bằng '/'\"\r\n\"subDomain\" = \"Tên miền con\"\r\n\"subDomainDesc\" = \"Mặc định để trống để nghe tất cả các tên miền và IP\"\r\n\"subUpdates\" = \"Khoảng thời gian cập nhật gói đăng ký\"\r\n\"subUpdatesDesc\" = \"Số giờ giữa các cập nhật trong ứng dụng khách\"\r\n\"subEncrypt\" = \"Mã hóa cấu hình\"\r\n\"subEncryptDesc\" = \"Mã hóa các cấu hình được trả về trong gói đăng ký\"\r\n\"subShowInfo\" = \"Hiển thị thông tin sử dụng\"\r\n\"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\"\r\n\"subURI\" = \"URI proxy trung gian\"\r\n\"subURIDesc\" = \"Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian\"\r\n\"externalTrafficInformEnable\" = \"Thông báo giao thông bên ngoài\"\r\n\"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.\"\r\n\"externalTrafficInformURI\" = \"URI thông báo lưu lượng truy cập bên ngoài\"\r\n\"externalTrafficInformURIDesc\" = \"Cập nhật lưu lượng truy cập được gửi tới URI này.\"\r\n\"fragment\" = \"Sự phân mảnh\"\r\n\"fragmentDesc\" = \"Kích hoạt phân mảnh cho gói TLS hello\"\r\n\"fragmentSett\" = \"Cài đặt phân mảnh\"\r\n\"noisesDesc\" = \"Bật Noises.\"\r\n\"noisesSett\" = \"Cài đặt Noises\"\r\n\"mux\" = \"Mux\"\r\n\"muxDesc\" = \"Truyền nhiều luồng dữ liệu độc lập trong luồng dữ liệu đã thiết lập.\"\r\n\"muxSett\" = \"Mux Cài đặt\"\r\n\"direct\" = \"Kết nối trực tiếp\"\r\n\"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ể.\"\r\n\"notifications\" = \"Thông báo\"\r\n\"certs\" = \"Chứng chỉ\"\r\n\"externalTraffic\" = \"Lưu lượng bên ngoài\"\r\n\"dateAndTime\" = \"Ngày và giờ\"\r\n\"proxyAndServer\" = \"Proxy và máy chủ\"\r\n\"intervals\" = \"Khoảng thời gian\"\r\n\"information\" = \"Thông tin\"\r\n\"language\" = \"Ngôn ngữ\"\r\n\"telegramBotLanguage\" = \"Ngôn ngữ của Bot Telegram\"\r\n\r\n[pages.xray]\r\n\"title\" = \"Cài đặt Xray\"\r\n\"save\" = \"Lưu cài đặt\"\r\n\"restart\" = \"Khởi động lại Xray\"\r\n\"restartSuccess\" = \"Đã khởi động lại Xray thành công\"\r\n\"stopSuccess\" = \"Xray đã được dừng thành công\"\r\n\"restartError\" = \"Đã xảy ra lỗi khi khởi động lại Xray.\"\r\n\"stopError\" = \"Đã xảy ra lỗi khi dừng Xray.\"\r\n\"basicTemplate\" = \"Mẫu Cơ bản\"\r\n\"advancedTemplate\" = \"Mẫu Nâng cao\"\r\n\"generalConfigs\" = \"Cấu hình Chung\"\r\n\"generalConfigsDesc\" = \"Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát.\"\r\n\"logConfigs\" = \"Nhật ký\"\r\n\"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\"\r\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ể.\"\r\n\"basicRouting\" = \"Định tuyến Cơ bản\"\r\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ể.\"\r\n\"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.\"\r\n\"blockips\" = \"Chặn IP\"\r\n\"blockdomains\" = \"Chặn Tên Miền\"\r\n\"directips\" = \"IP Trực Tiếp\"\r\n\"directdomains\" = \"Tên Miền Trực Tiếp\"\r\n\"ipv4Routing\" = \"Định tuyến IPv4\"\r\n\"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.\"\r\n\"warpRouting\" = \"Định tuyến WARP\"\r\n\"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.\"\r\n\"Template\" = \"Mẫu Cấu hình Xray\"\r\n\"TemplateDesc\" = \"Tạo tệp cấu hình Xray cuối cùng dựa trên mẫu này.\"\r\n\"FreedomStrategy\" = \"Cấu hình Chiến lược cho Giao thức Freedom\"\r\n\"FreedomStrategyDesc\" = \"Đặt chiến lược đầu ra của mạng trong Giao thức Freedom.\"\r\n\"RoutingStrategy\" = \"Cấu hình Chiến lược Định tuyến Tên miền\"\r\n\"RoutingStrategyDesc\" = \"Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS.\"\r\n\"outboundTestUrl\" = \"URL kiểm tra outbound\"\r\n\"outboundTestUrlDesc\" = \"URL dùng khi kiểm tra kết nối outbound\"\r\n\"Torrent\" = \"Cấu hình sử dụng BitTorrent\"\r\n\"Inbounds\" = \"Đầu vào\"\r\n\"InboundsDesc\" = \"Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể.\"\r\n\"Outbounds\" = \"Đầu ra\"\r\n\"Balancers\" = \"Cân bằng\"\r\n\"OutboundsDesc\" = \"Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này.\"\r\n\"Routings\" = \"Quy tắc định tuyến\"\r\n\"RoutingsDesc\" = \"Mức độ ưu tiên của mỗi quy tắc đều quan trọng!\"\r\n\"completeTemplate\" = \"All\"\r\n\"logLevel\" = \"Mức đăng nhập\"\r\n\"logLevelDesc\" = \"Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại.\"\r\n\"accessLog\" = \"Nhật ký truy cập\"\r\n\"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'\"\r\n\"errorLog\" = \"Nhật ký lỗi\"\r\n\"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'\"\r\n\"dnsLog\" = \"Nhật ký DNS\"\r\n\"dnsLogDesc\" = \"Có bật nhật ký truy vấn DNS không\"\r\n\"maskAddress\" = \"Ẩn Địa Chỉ\"\r\n\"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ý.\"\r\n\"statistics\" = \"Thống kê\"\r\n\"statsInboundUplink\" = \"Thống kê tải lên đầu vào\"\r\n\"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.\"\r\n\"statsInboundDownlink\" = \"Thống kê tải xuống đầu vào\"\r\n\"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.\"\r\n\"statsOutboundUplink\" = \"Thống kê tải lên đầu ra\"\r\n\"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.\"\r\n\"statsOutboundDownlink\" = \"Thống kê tải xuống đầu ra\"\r\n\"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.\"\r\n\r\n[pages.xray.rules]\r\n\"first\" = \"Đầu tiên\"\r\n\"last\" = \"Cuối cùng\"\r\n\"up\" = \"Lên\"\r\n\"down\" = \"Xuống\"\r\n\"source\" = \"Nguồn\"\r\n\"dest\" = \"Đích\"\r\n\"inbound\" = \"Vào\"\r\n\"outbound\" = \"Ra\"\r\n\"balancer\" = \"Cân bằng\"\r\n\"info\" = \"Thông tin\"\r\n\"add\" = \"Thêm quy tắc\"\r\n\"edit\" = \"Chỉnh sửa quy tắc\"\r\n\"useComma\" = \"Các mục được phân tách bằng dấu phẩy\"\r\n\r\n[pages.xray.outbound]\r\n\"addOutbound\" = \"Thêm thư đi\"\r\n\"addReverse\" = \"Thêm đảo ngược\"\r\n\"editOutbound\" = \"Chỉnh sửa gửi đi\"\r\n\"editReverse\" = \"Chỉnh sửa ngược lại\"\r\n\"tag\" = \"Thẻ\"\r\n\"tagDesc\" = \"thẻ duy nhất\"\r\n\"address\" = \"Địa chỉ\"\r\n\"reverse\" = \"Đảo ngược\"\r\n\"domain\" = \"Miền\"\r\n\"type\" = \"Loại\"\r\n\"bridge\" = \"Cầu\"\r\n\"portal\" = \"Cổng thông tin\"\r\n\"link\" = \"Liên kết\"\r\n\"intercon\" = \"Kết nối\"\r\n\"settings\" = \"cài đặt\"\r\n\"accountInfo\" = \"Thông tin tài khoản\"\r\n\"outboundStatus\" = \"Trạng thái đầu ra\"\r\n\"sendThrough\" = \"Gửi qua\"\r\n\"test\" = \"Kiểm tra\"\r\n\"testResult\" = \"Kết quả kiểm tra\"\r\n\"testing\" = \"Đang kiểm tra kết nối...\"\r\n\"testSuccess\" = \"Kiểm tra thành công\"\r\n\"testFailed\" = \"Kiểm tra thất bại\"\r\n\"testError\" = \"Không thể kiểm tra đầu ra\"\r\n\r\n[pages.xray.balancer]\r\n\"addBalancer\" = \"Thêm cân bằng\"\r\n\"editBalancer\" = \"Chỉnh sửa cân bằng\"\r\n\"balancerStrategy\" = \"Chiến lược\"\r\n\"balancerSelectors\" = \"Bộ chọn\"\r\n\"tag\" = \"Thẻ\"\r\n\"tagDesc\" = \"thẻ duy nhất\"\r\n\"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.\"\r\n\r\n[pages.xray.wireguard]\r\n\"secretKey\" = \"Khoá bí mật\"\r\n\"publicKey\" = \"Khóa công khai\"\r\n\"allowedIPs\" = \"IP được phép\"\r\n\"endpoint\" = \"Điểm cuối\"\r\n\"psk\" = \"Khóa chia sẻ\"\r\n\"domainStrategy\" = \"Chiến lược tên miền\"\r\n\r\n[pages.xray.tun]\r\n\"nameDesc\" = \"Tên của giao diện TUN. Giá trị mặc định là 'xray0'\"\r\n\"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\"\r\n\"userLevel\" = \"Mức Người Dùng\"\r\n\"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\"\r\n\r\n[pages.xray.dns]\r\n\"enable\" = \"Kích hoạt DNS\"\r\n\"enableDesc\" = \"Kích hoạt máy chủ DNS tích hợp\"\r\n\"tag\" = \"Thẻ gửi đến DNS\"\r\n\"tagDesc\" = \"Thẻ này sẽ có sẵn dưới dạng thẻ Gửi đến trong quy tắc định tuyến.\"\r\n\"clientIp\" = \"IP khách hàng\"\r\n\"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\"\r\n\"disableCache\" = \"Tắt bộ nhớ đệm\"\r\n\"disableCacheDesc\" = \"Tắt bộ nhớ đệm DNS\"\r\n\"disableFallback\" = \"Tắt Fallback\"\r\n\"disableFallbackDesc\" = \"Tắt các truy vấn DNS Fallback\"\r\n\"disableFallbackIfMatch\" = \"Tắt Fallback Nếu Khớp\"\r\n\"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\"\r\n\"enableParallelQuery\" = \"Bật Truy vấn Song song\"\r\n\"enableParallelQueryDesc\" = \"Bật truy vấn DNS song song đến nhiều máy chủ để phân giải nhanh hơn\"\r\n\"strategy\" = \"Chiến lược truy vấn\"\r\n\"strategyDesc\" = \"Chiến lược tổng thể để phân giải tên miền\"\r\n\"add\" = \"Thêm máy chủ\"\r\n\"edit\" = \"Chỉnh sửa máy chủ\"\r\n\"domains\" = \"Tên miền\"\r\n\"expectIPs\" = \"Các IP Dự Kiến\"\r\n\"unexpectIPs\" = \"IP không mong muốn\"\r\n\"useSystemHosts\" = \"Sử dụng Hosts hệ thống\"\r\n\"useSystemHostsDesc\" = \"Sử dụng file hosts từ hệ thống đã cài đặt\"\r\n\"usePreset\" = \"Dùng mẫu\"\r\n\"dnsPresetTitle\" = \"Mẫu DNS\"\r\n\"dnsPresetFamily\" = \"Gia đình\"\r\n\r\n[pages.xray.fakedns]\r\n\"add\" = \"Thêm DNS giả\"\r\n\"edit\" = \"Chỉnh sửa DNS giả\"\r\n\"ipPool\" = \"Mạng con nhóm IP\"\r\n\"poolSize\" = \"Kích thước bể bơi\"\r\n\r\n[pages.settings.security]\r\n\"admin\" = \"Thông tin đăng nhập quản trị viên\"\r\n\"twoFactor\" = \"Xác thực hai yếu tố\"\r\n\"twoFactorEnable\" = \"Bật 2FA\"\r\n\"twoFactorEnableDesc\" = \"Thêm một lớp bảo mật bổ sung để tăng cường an toàn.\"\r\n\"twoFactorModalSetTitle\" = \"Bật xác thực hai yếu tố\"\r\n\"twoFactorModalDeleteTitle\" = \"Tắt xác thực hai yếu tố\"\r\n\"twoFactorModalSteps\" = \"Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:\"\r\n\"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\"\r\n\"twoFactorModalSecondStep\" = \"2. Nhập mã từ ứng dụng\"\r\n\"twoFactorModalRemoveStep\" = \"Nhập mã từ ứng dụng để xóa xác thực hai yếu tố.\"\r\n\"twoFactorModalChangeCredentialsTitle\" = \"Thay đổi thông tin xác thực\"\r\n\"twoFactorModalChangeCredentialsStep\" = \"Nhập mã từ ứng dụng để thay đổi thông tin xác thực quản trị viên.\"\r\n\"twoFactorModalSetSuccess\" = \"Xác thực hai yếu tố đã được thiết lập thành công\"\r\n\"twoFactorModalDeleteSuccess\" = \"Xác thực hai yếu tố đã được xóa thành công\"\r\n\"twoFactorModalError\" = \"Mã sai\"\r\n\r\n[pages.settings.toasts]\r\n\"modifySettings\" = \"Các tham số đã được thay đổi.\"\r\n\"getSettings\" = \"Lỗi xảy ra khi truy xuất tham số.\"\r\n\"modifyUserError\" = \"Đã xảy ra lỗi khi thay đổi thông tin đăng nhập quản trị viên.\"\r\n\"modifyUser\" = \"Bạn đã thay đổi thông tin đăng nhập quản trị viên thành công.\"\r\n\"originalUserPassIncorrect\" = \"Tên người dùng hoặc mật khẩu gốc không đúng\"\r\n\"userPassMustBeNotEmpty\" = \"Tên người dùng mới và mật khẩu mới không thể để trống\"\r\n\"getOutboundTrafficError\" = \"Lỗi khi lấy lưu lượng truy cập đi\"\r\n\"resetOutboundTrafficError\" = \"Lỗi khi đặt lại lưu lượng truy cập đi\"\r\n\r\n[tgbot]\r\n\"keyboardClosed\" = \"❌ Bàn phím đã đóng!\"\r\n\"noResult\" = \"❗ Không có kết quả!\"\r\n\"noQuery\" = \"❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!\"\r\n\"wentWrong\" = \"❌ Đã xảy ra lỗi!\"\r\n\"noIpRecord\" = \"❗ Không có bản ghi IP!\"\r\n\"noInbounds\" = \"❗ Không tìm thấy inbound!\"\r\n\"unlimited\" = \"♾ Không giới hạn (Đặt lại)\"\r\n\"add\" = \"Thêm\"\r\n\"month\" = \"Tháng\"\r\n\"months\" = \"Tháng\"\r\n\"day\" = \"Ngày\"\r\n\"days\" = \"Ngày\"\r\n\"hours\" = \"Giờ\"\r\n\"minutes\" = \"Phút\"\r\n\"unknown\" = \"Không xác định\"\r\n\"inbounds\" = \"Inbound\"\r\n\"clients\" = \"Client\"\r\n\"offline\" = \"🔴 Ngoại tuyến\"\r\n\"online\" = \"🟢 Trực tuyến\"\r\n\r\n[tgbot.commands]\r\n\"unknown\" = \"❗ Lệnh không rõ\"\r\n\"pleaseChoose\" = \"👇 Vui lòng chọn:\\r\\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\"\r\n\"start\" = \"👋 Xin chào <i>{{ .Firstname }}</i>.\\r\\n\"\r\n\"welcome\" = \"🤖 Chào mừng đến với bot quản lý của <b>{{ .Hostname }}</b>.\\r\\n\"\r\n\"status\" = \"✅ Bot hoạt động bình thường!\"\r\n\"usage\" = \"❗ Vui lòng cung cấp văn bản để tìm kiếm!\"\r\n\"getID\" = \"🆔 ID của bạn: <code>{{ .ID }}</code>\"\r\n\"helpAdminCommands\" = \"Để khởi động lại Xray Core:\\r\\n<code>/restart</code>\\r\\n\\r\\nĐể tìm kiếm email của khách hàng:\\r\\n<code>/usage [Email]</code>\\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<code>/inbound [Ghi chú]</code>\\r\\n\\r\\nID Trò chuyện Telegram:\\r\\n<code>/id</code>\"\r\n\"helpClientCommands\" = \"Để tìm kiếm thống kê, sử dụng lệnh sau:\\r\\n<code>/usage [Email]</code>\\r\\n\\r\\nID Trò chuyện Telegram:\\r\\n<code>/id</code>\"\r\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\r\n\"restartSuccess\" = \"✅ Hoạt động thành công!\"\r\n\"restartFailed\" = \"❗ Lỗi trong quá trình hoạt động.\\r\\n\\r\\n<code>Lỗi: {{ .Error }}</code>.\"\r\n\"xrayNotRunning\" = \"❗ Xray Core không chạy.\"\r\n\"startDesc\" = \"Hiển thị menu chính\"\r\n\"helpDesc\" = \"Trợ giúp bot\"\r\n\"statusDesc\" = \"Kiểm tra trạng thái bot\"\r\n\"idDesc\" = \"Hiển thị ID Telegram của bạn\"\r\n\r\n[tgbot.messages]\r\n\"cpuThreshold\" = \"🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%\"\r\n\"selectUserFailed\" = \"❌ Lỗi khi chọn người dùng!\"\r\n\"userSaved\" = \"✅ Người dùng Telegram đã được lưu.\"\r\n\"loginSuccess\" = \"✅ Đăng nhập thành công vào bảng điều khiển.\\r\\n\"\r\n\"loginFailed\" = \"❗️ Đăng nhập vào bảng điều khiển thất bại.\\r\\n\"\r\n\"2faFailed\" = \"Lỗi 2FA\"\r\n\"report\" = \"🕰 Báo cáo định kỳ: {{ .RunTime }}\\r\\n\"\r\n\"datetime\" = \"⏰ Ngày-Giờ: {{ .DateTime }}\\r\\n\"\r\n\"hostname\" = \"💻 Tên máy chủ: {{ .Hostname }}\\r\\n\"\r\n\"version\" = \"🚀 Phiên bản X-UI: {{ .Version }}\\r\\n\"\r\n\"xrayVersion\" = \"📡 Phiên bản Xray: {{ .XrayVersion }}\\r\\n\"\r\n\"ipv6\" = \"🌐 IPv6: {{ .IPv6 }}\\r\\n\"\r\n\"ipv4\" = \"🌐 IPv4: {{ .IPv4 }}\\r\\n\"\r\n\"ip\" = \"🌐 IP: {{ .IP }}\\r\\n\"\r\n\"ips\" = \"🔢 Các IP:\\r\\n{{ .IPs }}\\r\\n\"\r\n\"serverUpTime\" = \"⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\\r\\n\"\r\n\"serverLoad\" = \"📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\r\n\"serverMemory\" = \"📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\\r\\n\"\r\n\"tcpCount\" = \"🔹 Số lượng kết nối TCP: {{ .Count }}\\r\\n\"\r\n\"udpCount\" = \"🔸 Số lượng kết nối UDP: {{ .Count }}\\r\\n\"\r\n\"traffic\" = \"🚦 Lưu lượng: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\r\n\"xrayStatus\" = \"ℹ️ Trạng thái Xray: {{ .State }}\\r\\n\"\r\n\"username\" = \"👤 Tên người dùng: {{ .Username }}\\r\\n\"\r\n\"password\" = \"👤 Mật khẩu: {{ .Password }}\\r\\n\"\r\n\"time\" = \"⏰ Thời gian: {{ .Time }}\\r\\n\"\r\n\"inbound\" = \"📍 Inbound: {{ .Remark }}\\r\\n\"\r\n\"port\" = \"🔌 Cổng: {{ .Port }}\\r\\n\"\r\n\"expire\" = \"📅 Ngày hết hạn: {{ .Time }}\\r\\n\"\r\n\"expireIn\" = \"📅 Hết hạn sau: {{ .Time }}\\r\\n\"\r\n\"active\" = \"💡 Đang hoạt động: {{ .Enable }}\\r\\n\"\r\n\"enabled\" = \"🚨 Đã bật: {{ .Enable }}\\r\\n\"\r\n\"online\" = \"🌐 Trạng thái kết nối: {{ .Status }}\\r\\n\"\r\n\"lastOnline\" = \"🔙 Lần online gần nhất: {{ .Time }}\\r\\n\"\r\n\"email\" = \"📧 Email: {{ .Email }}\\r\\n\"\r\n\"upload\" = \"🔼 Tải lên: ↑{{ .Upload }}\\r\\n\"\r\n\"download\" = \"🔽 Tải xuống: ↓{{ .Download }}\\r\\n\"\r\n\"total\" = \"📊 Tổng cộng: ↑↓{{ .UpDown }} / {{ .Total }}\\r\\n\"\r\n\"TGUser\" = \"👤 Người dùng Telegram: {{ .TelegramID }}\\r\\n\"\r\n\"exhaustedMsg\" = \"🚨 Sự cạn kiệt {{ .Type }}:\\r\\n\"\r\n\"exhaustedCount\" = \"🚨 Số lần cạn kiệt {{ .Type }}:\\r\\n\"\r\n\"onlinesCount\" = \"🌐 Khách hàng trực tuyến: {{ .Count }}\\r\\n\"\r\n\"disabled\" = \"🛑 Vô hiệu hóa: {{ .Disabled }}\\r\\n\"\r\n\"depleteSoon\" = \"🔜 Sắp cạn kiệt: {{ .Deplete }}\\r\\n\\r\\n\"\r\n\"backupTime\" = \"🗄 Thời gian sao lưu: {{ .Time }}\\r\\n\"\r\n\"refreshedOn\" = \"\\r\\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\\r\\n\\r\\n\"\r\n\"yes\" = \"✅ Có\"\r\n\"no\" = \"❌ Không\"\r\n\"received_id\" = \"🔑📥 ID đã được cập nhật.\"\r\n\"received_password\" = \"🔑📥 Mật khẩu đã được cập nhật.\"\r\n\"received_email\" = \"📧📥 Email đã được cập nhật.\"\r\n\"received_comment\" = \"💬📥 Bình luận đã được cập nhật.\"\r\n\"id_prompt\" = \"🔑 ID mặc định: {{ .ClientId }}\\n\\nVui lòng nhập ID của bạn.\"\r\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.\"\r\n\"email_prompt\" = \"📧 Email mặc định: {{ .ClientEmail }}\\n\\nVui lòng nhập email của bạn.\"\r\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.\"\r\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!\"\r\n\"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!\"\r\n\"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. 🔄\"\r\n\"error_add_client\" = \"⚠️ Lỗi:\\n\\n {{ .error }}\"\r\n\"using_default_value\" = \"Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊\"\r\n\"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 🚫\"\r\n\"AreYouSure\" = \"Bạn có chắc không? 🤔\"\r\n\"SuccessResetTraffic\" = \"📧 Email: {{ .ClientEmail }}\\n🏁 Kết quả: ✅ Thành công\"\r\n\"FailedResetTraffic\" = \"📧 Email: {{ .ClientEmail }}\\n🏁 Kết quả: ❌ Thất bại \\n\\n🛠️ Lỗi: [ {{ .ErrorMessage }} ]\"\r\n\"FinishProcess\" = \"🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng.\"\r\n\r\n[tgbot.buttons]\r\n\"closeKeyboard\" = \"❌ Đóng Bàn Phím\"\r\n\"cancel\" = \"❌ Hủy\"\r\n\"cancelReset\" = \"❌ Hủy Đặt Lại\"\r\n\"cancelIpLimit\" = \"❌ Hủy Giới Hạn IP\"\r\n\"confirmResetTraffic\" = \"✅ Xác Nhận Đặt Lại Lưu Lượng?\"\r\n\"confirmClearIps\" = \"✅ Xác Nhận Xóa Các IP?\"\r\n\"confirmRemoveTGUser\" = \"✅ Xác Nhận Xóa Người Dùng Telegram?\"\r\n\"confirmToggle\" = \"✅ Xác nhận Bật/Tắt người dùng?\"\r\n\"dbBackup\" = \"Tải bản sao lưu cơ sở dữ liệu\"\r\n\"serverUsage\" = \"Sử Dụng Máy Chủ\"\r\n\"getInbounds\" = \"Lấy cổng vào\"\r\n\"depleteSoon\" = \"Depleted Soon\"\r\n\"clientUsage\" = \"Lấy Sử Dụng\"\r\n\"onlines\" = \"Khách hàng trực tuyến\"\r\n\"commands\" = \"Lệnh\"\r\n\"refresh\" = \"🔄 Cập Nhật\"\r\n\"clearIPs\" = \"❌ Xóa IP\"\r\n\"removeTGUser\" = \"❌ Xóa Người Dùng Telegram\"\r\n\"selectTGUser\" = \"👤 Chọn Người Dùng Telegram\"\r\n\"selectOneTGUser\" = \"👤 Chọn một người dùng telegram:\"\r\n\"resetTraffic\" = \"📈 Đặt Lại Lưu Lượng\"\r\n\"resetExpire\" = \"📅 Thay đổi ngày hết hạn\"\r\n\"ipLog\" = \"🔢 Nhật ký địa chỉ IP\"\r\n\"ipLimit\" = \"🔢 Giới Hạn địa chỉ IP\"\r\n\"setTGUser\" = \"👤 Đặt Người Dùng Telegram\"\r\n\"toggle\" = \"🔘 Bật / Tắt\"\r\n\"custom\" = \"🔢 Tùy chỉnh\"\r\n\"confirmNumber\" = \"✅ Xác nhận: {{ .Num }}\"\r\n\"confirmNumberAdd\" = \"✅ Xác nhận thêm: {{ .Num }}\"\r\n\"limitTraffic\" = \"🚧 Giới hạn lưu lượng\"\r\n\"getBanLogs\" = \"Cấm nhật ký\"\r\n\"allClients\" = \"Tất cả Khách hàng\"\r\n\"addClient\" = \"Thêm Khách Hàng\"\r\n\"submitDisable\" = \"Gửi Dưới Dạng Vô Hiệu ☑️\"\r\n\"submitEnable\" = \"Gửi Dưới Dạng Kích Hoạt ✅\"\r\n\"use_default\" = \"🏷️ Sử Dụng Mặc Định\"\r\n\"change_id\" = \"⚙️🔑 ID\"\r\n\"change_password\" = \"⚙️🔑 Mật Khẩu\"\r\n\"change_email\" = \"⚙️📧 Email\"\r\n\"change_comment\" = \"⚙️💬 Bình Luận\"\r\n\"ResetAllTraffics\" = \"Đặt lại tất cả lưu lượng\"\r\n\"SortedTrafficUsageReport\" = \"Báo cáo sử dụng lưu lượng đã sắp xếp\"\r\n\r\n[tgbot.answers]\r\n\"successfulOperation\" = \"✅ Thành công!\"\r\n\"errorOperation\" = \"❗ Lỗi Trong Quá Trình Thực Hiện.\"\r\n\"getInboundsFailed\" = \"❌ Không Thể Lấy Được Inbounds\"\r\n\"getClientsFailed\" = \"❌ Không thể lấy khách hàng.\"\r\n\"canceled\" = \"❌ {{ .Email }} : Thao Tác Đã Bị Hủy.\"\r\n\"clientRefreshSuccess\" = \"✅ {{ .Email }} : Cập Nhật Thành Công Cho Khách Hàng.\"\r\n\"IpRefreshSuccess\" = \"✅ {{ .Email }} : Cập Nhật Thành Công Cho IPs.\"\r\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }} : Cập Nhật Thành Công Cho Người Dùng Telegram.\"\r\n\"resetTrafficSuccess\" = \"✅ {{ .Email }} : Đặt Lại Lưu Lượng Thành Công.\"\r\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }} : Đã lưu thành công giới hạn lưu lượng.\"\r\n\"expireResetSuccess\" = \"✅ {{ .Email }} : Đặt Lại Ngày Hết Hạn Thành Công.\"\r\n\"resetIpSuccess\" = \"✅ {{ .Email }} : Giới Hạn IP {{ .Count }} Đã Được Lưu Thành Công.\"\r\n\"clearIpSuccess\" = \"✅ {{ .Email }} : IP Đã Được Xóa Thành Công.\"\r\n\"getIpLog\" = \"✅ {{ .Email }} : Lấy nhật ký IP Thành Công.\"\r\n\"getUserInfo\" = \"✅ {{ .Email }} : Lấy Thông Tin Người Dùng Telegram Thành Công.\"\r\n\"removedTGUserSuccess\" = \"✅ {{ .Email }} : Người Dùng Telegram Đã Được Xóa Thành Công.\"\r\n\"enableSuccess\" = \"✅ {{ .Email }} : Đã Bật Thành Công.\"\r\n\"disableSuccess\" = \"✅ {{ .Email }} : Đã Tắt Thành Công.\"\r\n\"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: <code>{{ .TgUserID }}</code>\"\r\n\"chooseClient\" = \"Chọn một Khách hàng cho Inbound {{ .Inbound }}\"\r\n\"chooseInbound\" = \"Chọn một Inbound\"\r\n"
  },
  {
    "path": "web/translation/translate.zh_CN.toml",
    "content": "\"username\" = \"用户名\"\n\"password\" = \"密码\"\n\"login\" = \"登录\"\n\"confirm\" = \"确定\"\n\"cancel\" = \"取消\"\n\"close\" = \"关闭\"\n\"create\" = \"创建\"\n\"update\" = \"更新\"\n\"copy\" = \"复制\"\n\"copied\" = \"已复制\"\n\"download\" = \"下载\"\n\"remark\" = \"备注\"\n\"enable\" = \"启用\"\n\"protocol\" = \"协议\"\n\"search\" = \"搜索\"\n\"filter\" = \"筛选\"\n\"loading\" = \"加载中...\"\n\"second\" = \"秒\"\n\"minute\" = \"分钟\"\n\"hour\" = \"小时\"\n\"day\" = \"天\"\n\"check\" = \"查看\"\n\"indefinite\" = \"无限期\"\n\"unlimited\" = \"无限制\"\n\"none\" = \"无\"\n\"qrCode\" = \"二维码\"\n\"info\" = \"更多信息\"\n\"edit\" = \"编辑\"\n\"delete\" = \"删除\"\n\"reset\" = \"重置\"\n\"noData\" = \"无数据。\"\n\"copySuccess\" = \"复制成功\"\n\"sure\" = \"确定\"\n\"encryption\" = \"加密\"\n\"useIPv4ForHost\" = \"使用 IPv4 连接主机\"\n\"transmission\" = \"传输\"\n\"host\" = \"主机\"\n\"path\" = \"路径\"\n\"camouflage\" = \"伪装\"\n\"status\" = \"状态\"\n\"enabled\" = \"开启\"\n\"disabled\" = \"关闭\"\n\"depleted\" = \"耗尽\"\n\"depletingSoon\" = \"即将耗尽\"\n\"offline\" = \"离线\"\n\"online\" = \"在线\"\n\"domainName\" = \"域名\"\n\"monitor\" = \"监听\"\n\"certificate\" = \"数字证书\"\n\"fail\" = \"失败\"\n\"comment\" = \"评论\"\n\"success\" = \"成功\"\n\"lastOnline\" = \"上次在线\"\n\"getVersion\" = \"获取版本\"\n\"install\" = \"安装\"\n\"clients\" = \"客户端\"\n\"usage\" = \"使用情况\"\n\"twoFactorCode\" = \"代码\"\n\"remained\" = \"剩余\"\n\"security\" = \"安全\"\n\"secAlertTitle\" = \"安全警报\"\n\"secAlertSsl\" = \"此连接不安全。在激活 TLS 进行数据保护之前，请勿输入敏感信息。\"\n\"secAlertConf\" = \"某些设置易受攻击。建议加强安全协议以防止潜在漏洞。\"\n\"secAlertSSL\" = \"面板缺少安全连接。请安装 TLS 证书以保护数据安全。\"\n\"secAlertPanelPort\" = \"面板默认端口存在安全风险。请配置随机端口或特定端口。\"\n\"secAlertPanelURI\" = \"面板默认 URI 路径不安全。请配置复杂的 URI 路径。\"\n\"secAlertSubURI\" = \"订阅默认 URI 路径不安全。请配置复杂的 URI 路径。\"\n\"secAlertSubJsonURI\" = \"订阅 JSON 默认 URI 路径不安全。请配置复杂的 URI 路径。\"\n\"emptyDnsDesc\" = \"未添加DNS服务器。\"\n\"emptyFakeDnsDesc\" = \"未添加Fake DNS服务器。\"\n\"emptyBalancersDesc\" = \"未添加负载均衡器。\"\n\"emptyReverseDesc\" = \"未添加反向代理。\"\n\"somethingWentWrong\" = \"出了点问题\"\n\n[subscription]\n\"title\" = \"订阅信息\"\n\"subId\" = \"订阅 ID\"\n\"status\" = \"状态\"\n\"downloaded\" = \"已下载\"\n\"uploaded\" = \"已上传\"\n\"expiry\" = \"到期\"\n\"totalQuota\" = \"总配额\"\n\"individualLinks\" = \"单独链接\"\n\"active\" = \"启用\"\n\"inactive\" = \"停用\"\n\"unlimited\" = \"无限制\"\n\"noExpiry\" = \"无到期\"\n\n[menu]\n\"theme\" = \"主题\"\n\"dark\" = \"暗色\"\n\"ultraDark\" = \"超暗色\"\n\"dashboard\" = \"系统状态\"\n\"inbounds\" = \"入站列表\"\n\"settings\" = \"面板设置\"\n\"xray\" = \"Xray 设置\"\n\"logout\" = \"退出登录\"\n\"link\" = \"管理\"\n\n[pages.login]\n\"hello\" = \"你好\"\n\"title\" = \"欢迎\"\n\"loginAgain\" = \"登录时效已过，请重新登录\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"数据格式错误\"\n\"emptyUsername\" = \"请输入用户名\"\n\"emptyPassword\" = \"请输入密码\"\n\"wrongUsernameOrPassword\" = \"用户名、密码或双重验证码无效。\"\n\"successLogin\" = \"您已成功登录您的账户。\"\n\n[pages.index]\n\"title\" = \"系统状态\"\n\"cpu\" = \"CPU\"\n\"logicalProcessors\" = \"逻辑处理器\"\n\"frequency\" = \"频率\"\n\"swap\" = \"交换分区\"\n\"storage\" = \"存储\"\n\"memory\" = \"内存\"\n\"threads\" = \"线程\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"停止\"\n\"restartXray\" = \"重启\"\n\"xraySwitch\" = \"版本\"\n\"xraySwitchClick\" = \"选择你要切换到的版本\"\n\"xraySwitchClickDesk\" = \"请谨慎选择，因为较旧版本可能与当前配置不兼容\"\n\"xrayStatusUnknown\" = \"未知\"\n\"xrayStatusRunning\" = \"运行中\"\n\"xrayStatusStop\" = \"停止\"\n\"xrayStatusError\" = \"错误\"\n\"xrayErrorPopoverTitle\" = \"运行Xray时发生错误\"\n\"operationHours\" = \"系统正常运行时间\"\n\"systemLoad\" = \"系统负载\"\n\"systemLoadDesc\" = \"过去 1、5 和 15 分钟的系统平均负载\"\n\"connectionCount\" = \"连接数\"\n\"ipAddresses\" = \"IP地址\"\n\"toggleIpVisibility\" = \"切换IP可见性\"\n\"overallSpeed\" = \"整体速度\"\n\"upload\" = \"上传\"\n\"download\" = \"下载\"\n\"totalData\" = \"总数据\"\n\"sent\" = \"已发送\"\n\"received\" = \"已接收\"\n\"documentation\" = \"文档\"\n\"xraySwitchVersionDialog\" = \"您确定要更改Xray版本吗？\"\n\"xraySwitchVersionDialogDesc\" = \"这将把Xray版本更改为#version#。\"\n\"xraySwitchVersionPopover\" = \"Xray 更新成功\"\n\"geofileUpdateDialog\" = \"您确定要更新地理文件吗？\"\n\"geofileUpdateDialogDesc\" = \"这将更新 #filename# 文件。\"\n\"geofilesUpdateDialogDesc\" = \"这将更新所有文件。\"\n\"geofilesUpdateAll\" = \"全部更新\"\n\"geofileUpdatePopover\" = \"地理文件更新成功\"\n\"dontRefresh\" = \"安装中，请勿刷新此页面\"\n\"logs\" = \"日志\"\n\"config\" = \"配置\"\n\"backup\" = \"备份\"\n\"backupTitle\" = \"备份和恢复数据库\"\n\"exportDatabase\" = \"备份\"\n\"exportDatabaseDesc\" = \"点击下载包含当前数据库备份的 .db 文件到您的设备。\"\n\"importDatabase\" = \"恢复\"\n\"importDatabaseDesc\" = \"点击选择并上传设备中的 .db 文件以从备份恢复数据库。\"\n\"importDatabaseSuccess\" = \"数据库导入成功\"\n\"importDatabaseError\" = \"导入数据库时出错\"\n\"readDatabaseError\" = \"读取数据库时出错\"\n\"getDatabaseError\" = \"检索数据库时出错\"\n\"getConfigError\" = \"检索配置文件时出错\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"累计总流量\"\n\"allTimeTrafficUsage\" = \"所有时间总使用量\"\n\"title\" = \"入站列表\"\n\"totalDownUp\" = \"总上传 / 下载\"\n\"totalUsage\" = \"总用量\"\n\"inboundCount\" = \"入站数量\"\n\"operate\" = \"菜单\"\n\"enable\" = \"启用\"\n\"remark\" = \"备注\"\n\"protocol\" = \"协议\"\n\"port\" = \"端口\"\n\"portMap\" = \"端口映射\"\n\"traffic\" = \"流量\"\n\"details\" = \"详细信息\"\n\"transportConfig\" = \"传输配置\"\n\"expireDate\" = \"到期时间\"\n\"createdAt\" = \"创建时间\"\n\"updatedAt\" = \"更新时间\"\n\"resetTraffic\" = \"重置流量\"\n\"addInbound\" = \"添加入站\"\n\"generalActions\" = \"通用操作\"\n\"autoRefresh\" = \"自动刷新\"\n\"autoRefreshInterval\" = \"间隔\"\n\"modifyInbound\" = \"修改入站\"\n\"deleteInbound\" = \"删除入站\"\n\"deleteInboundContent\" = \"确定要删除入站吗？\"\n\"deleteClient\" = \"删除客户端\"\n\"deleteClientContent\" = \"确定要删除客户端吗？\"\n\"resetTrafficContent\" = \"确定要重置流量吗？\"\n\"copyLink\" = \"复制链接\"\n\"address\" = \"地址\"\n\"network\" = \"网络\"\n\"destinationPort\" = \"目标端口\"\n\"targetAddress\" = \"目标地址\"\n\"monitorDesc\" = \"留空表示监听所有 IP\"\n\"meansNoLimit\" = \"= 无限制（单位：GB)\"\n\"totalFlow\" = \"总流量\"\n\"leaveBlankToNeverExpire\" = \"留空表示永不过期\"\n\"noRecommendKeepDefault\" = \"建议保留默认值\"\n\"certificatePath\" = \"文件路径\"\n\"certificateContent\" = \"文件内容\"\n\"publicKey\" = \"公钥\"\n\"privatekey\" = \"私钥\"\n\"clickOnQRcode\" = \"点击二维码复制\"\n\"client\" = \"客户\"\n\"export\" = \"导出链接\"\n\"clone\" = \"克隆\"\n\"cloneInbound\" = \"克隆\"\n\"cloneInboundContent\" = \"此入站规则除端口（Port）、监听 IP（Listening IP）和客户端（Clients）以外的所有配置都将应用于克隆\"\n\"cloneInboundOk\" = \"创建克隆\"\n\"resetAllTraffic\" = \"重置所有入站流量\"\n\"resetAllTrafficTitle\" = \"重置所有入站流量\"\n\"resetAllTrafficContent\" = \"确定要重置所有入站流量吗？\"\n\"resetInboundClientTraffics\" = \"重置客户端流量\"\n\"resetInboundClientTrafficTitle\" = \"重置所有客户端流量\"\n\"resetInboundClientTrafficContent\" = \"确定要重置此入站客户端的所有流量吗？\"\n\"resetAllClientTraffics\" = \"重置所有客户端流量\"\n\"resetAllClientTrafficTitle\" = \"重置所有客户端流量\"\n\"resetAllClientTrafficContent\" = \"确定要重置所有客户端的所有流量吗？\"\n\"delDepletedClients\" = \"删除流量耗尽的客户端\"\n\"delDepletedClientsTitle\" = \"删除流量耗尽的客户端\"\n\"delDepletedClientsContent\" = \"确定要删除所有流量耗尽的客户端吗？\"\n\"email\" = \"电子邮件\"\n\"emailDesc\" = \"电子邮件必须完全唯一\"\n\"IPLimit\" = \"IP 限制\"\n\"IPLimitDesc\" = \"如果数量超过设置值，则禁用入站流量。（0 = 禁用）\"\n\"IPLimitlog\" = \"IP 日志\"\n\"IPLimitlogDesc\" = \"IP 历史日志（要启用被禁用的入站流量，请清除日志）\"\n\"IPLimitlogclear\" = \"清除日志\"\n\"setDefaultCert\" = \"从面板设置证书\"\n\"telegramDesc\" = \"请提供Telegram聊天ID。（在机器人中使用'/id'命令）或（@userinfobot\"\n\"subscriptionDesc\" = \"要找到你的订阅 URL，请导航到“详细信息”。此外，你可以为多个客户端使用相同的名称。\"\n\"info\" = \"信息\"\n\"same\" = \"相同\"\n\"inboundData\" = \"入站数据\"\n\"exportInbound\" = \"导出入站规则\"\n\"import\" = \"导入\"\n\"importInbound\" = \"导入入站规则\"\n\"periodicTrafficResetTitle\" = \"流量重置\"\n\"periodicTrafficResetDesc\" = \"按指定间隔自动重置流量计数器\"\n\"lastReset\" = \"上次重置\"\n\n[pages.client]\n\"add\" = \"添加客户端\"\n\"edit\" = \"编辑客户端\"\n\"submitAdd\" = \"添加客户端\"\n\"submitEdit\" = \"保存修改\"\n\"clientCount\" = \"客户端数量\"\n\"bulk\" = \"批量创建\"\n\"method\" = \"方法\"\n\"first\" = \"置顶\"\n\"last\" = \"置底\"\n\"prefix\" = \"前缀\"\n\"postfix\" = \"后缀\"\n\"delayedStart\" = \"首次使用后开始\"\n\"expireDays\" = \"期间\"\n\"days\" = \"天\"\n\"renew\" = \"自动续订\"\n\"renewDesc\" = \"到期后自动续订。(0 = 禁用)(单位: 天)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"从不\"\n\"daily\" = \"每日\"\n\"weekly\" = \"每周\"\n\"monthly\" = \"每月\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"获取\"\n\"updateSuccess\" = \"更新成功\"\n\"logCleanSuccess\" = \"日志已清除\"\n\"inboundsUpdateSuccess\" = \"入站连接已成功更新\"\n\"inboundUpdateSuccess\" = \"入站连接已成功更新\"\n\"inboundCreateSuccess\" = \"入站连接已成功创建\"\n\"inboundDeleteSuccess\" = \"入站连接已成功删除\"\n\"inboundClientAddSuccess\" = \"已添加入站客户端\"\n\"inboundClientDeleteSuccess\" = \"入站客户端已删除\"\n\"inboundClientUpdateSuccess\" = \"入站客户端已更新\"\n\"delDepletedClientsSuccess\" = \"所有耗尽客户端已删除\"\n\"resetAllClientTrafficSuccess\" = \"客户端所有流量已重置\"\n\"resetAllTrafficSuccess\" = \"所有流量已重置\"\n\"resetInboundClientTrafficSuccess\" = \"流量已重置\"\n\"trafficGetError\" = \"获取流量数据时出错\"\n\"getNewX25519CertError\" = \"获取X25519证书时出错。\"\n\"getNewmldsa65Error\" = \"获取mldsa65证书时出错。\"\n\"getNewVlessEncError\" = \"获取VlessEnc证书时出错。\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"请求\"\n\"response\" = \"响应\"\n\"name\" = \"名称\"\n\"value\" = \"值\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"版本\"\n\"method\" = \"方法\"\n\"path\" = \"路径\"\n\"status\" = \"状态\"\n\"statusDescription\" = \"状态说明\"\n\"requestHeader\" = \"请求头\"\n\"responseHeader\" = \"响应头\"\n\n[pages.settings]\n\"title\" = \"面板设置\"\n\"save\" = \"保存\"\n\"infoDesc\" = \"此处的所有更改都需要保存并重启面板才能生效\"\n\"restartPanel\" = \"重启面板\"\n\"restartPanelDesc\" = \"确定要重启面板吗？若重启后无法访问面板，请前往服务器查看面板日志信息\"\n\"restartPanelSuccess\" = \"面板已成功重启\"\n\"actions\" = \"操作\"\n\"resetDefaultConfig\" = \"重置为默认配置\"\n\"panelSettings\" = \"常规\"\n\"securitySettings\" = \"安全设定\"\n\"TGBotSettings\" = \"Telegram 机器人配置\"\n\"panelListeningIP\" = \"面板监听 IP\"\n\"panelListeningIPDesc\" = \"默认留空监听所有 IP\"\n\"panelListeningDomain\" = \"面板监听域名\"\n\"panelListeningDomainDesc\" = \"默认情况下留空以监视所有域名和 IP 地址\"\n\"panelPort\" = \"面板监听端口\"\n\"panelPortDesc\" = \"重启面板生效\"\n\"publicKeyPath\" = \"面板证书公钥文件路径\"\n\"publicKeyPathDesc\" = \"填写一个 '/' 开头的绝对路径\"\n\"privateKeyPath\" = \"面板证书密钥文件路径\"\n\"privateKeyPathDesc\" = \"填写一个 '/' 开头的绝对路径\"\n\"panelUrlPath\" = \"面板 url 根路径\"\n\"panelUrlPathDesc\" = \"必须以 '/' 开头，以 '/' 结尾\"\n\"pageSize\" = \"分页大小\"\n\"pageSizeDesc\" = \"定义入站表的页面大小。设置 0 表示禁用\"\n\"remarkModel\" = \"备注模型和分隔符\"\n\"datepicker\" = \"日期选择器\"\n\"datepickerPlaceholder\" = \"选择日期\"\n\"datepickerDescription\" = \"选择器日历类型指定到期日期\"\n\"sampleRemark\" = \"备注示例\"\n\"oldUsername\" = \"原用户名\"\n\"currentPassword\" = \"原密码\"\n\"newUsername\" = \"新用户名\"\n\"newPassword\" = \"新密码\"\n\"telegramBotEnable\" = \"启用 Telegram 机器人\"\n\"telegramBotEnableDesc\" = \"启用 Telegram 机器人功能\"\n\"telegramToken\" = \"Telegram 机器人令牌（token）\"\n\"telegramTokenDesc\" = \"从 '@BotFather' 获取的 Telegram 机器人令牌\"\n\"telegramProxy\" = \"SOCKS5 Proxy\"\n\"telegramProxyDesc\" = \"启用 SOCKS5 代理连接到 Telegram（根据指南调整设置）\"\n\"telegramAPIServer\" = \"Telegram API Server\"\n\"telegramAPIServerDesc\" = \"要使用的 Telegram API 服务器。留空以使用默认服务器。\"\n\"telegramChatId\" = \"管理员聊天 ID\"\n\"telegramChatIdDesc\" = \"Telegram 管理员聊天 ID (多个以逗号分隔)（可通过 @userinfobot 获取，或在机器人中使用 '/id' 命令获取）\"\n\"telegramNotifyTime\" = \"通知时间\"\n\"telegramNotifyTimeDesc\" = \"设置周期性的 Telegram 机器人通知时间（使用 crontab 时间格式）\"\n\"tgNotifyBackup\" = \"数据库备份\"\n\"tgNotifyBackupDesc\" = \"发送带有报告的数据库备份文件\"\n\"tgNotifyLogin\" = \"登录通知\"\n\"tgNotifyLoginDesc\" = \"当有人试图登录你的面板时显示用户名、IP 地址和时间\"\n\"sessionMaxAge\" = \"会话时长\"\n\"sessionMaxAgeDesc\" = \"保持登录状态的时长（单位：分钟）\"\n\"expireTimeDiff\" = \"到期通知阈值\"\n\"expireTimeDiffDesc\" = \"达到此阈值时，将收到有关到期时间的通知（单位：天）\"\n\"trafficDiff\" = \"流量耗尽阈值\"\n\"trafficDiffDesc\" = \"达到此阈值时，将收到有关流量耗尽的通知（单位：GB）\"\n\"tgNotifyCpu\" = \"CPU 负载通知阈值\"\n\"tgNotifyCpuDesc\" = \"CPU 负载超过此阈值时，将收到通知（单位：%）\"\n\"timeZone\" = \"时区\"\n\"timeZoneDesc\" = \"定时任务将按照该时区的时间运行\"\n\"subSettings\" = \"订阅设置\"\n\"subEnable\" = \"启用订阅服务\"\n\"subEnableDesc\" = \"启用订阅服务功能\"\n\"subJsonEnable\" = \"单独启用/禁用 JSON 订阅端点。\"\n\"subTitle\" = \"订阅标题\"\n\"subTitleDesc\" = \"在VPN客户端中显示的标题\"\n\"subSupportUrl\" = \"支持链接\"\n\"subSupportUrlDesc\" = \"VPN 客户端中显示的技术支持链接\"\n\"subProfileUrl\" = \"个人资料链接\"\n\"subProfileUrlDesc\" = \"VPN 客户端中显示的网站链接\"\n\"subAnnounce\" = \"公告\"\n\"subAnnounceDesc\" = \"VPN 客户端中显示的公告文本\"\n\"subEnableRouting\" = \"启用路由\"\n\"subEnableRoutingDesc\" = \"在 VPN 客户端中启用路由的全局设置。（僅限 Happ）\"\n\"subRoutingRules\" = \"路由規則\"\n\"subRoutingRulesDesc\" = \"VPN 用戶端的全域路由規則。（僅限 Happ）\"\n\"subListen\" = \"监听 IP\"\n\"subListenDesc\" = \"订阅服务监听的 IP 地址（留空表示监听所有 IP）\"\n\"subPort\" = \"监听端口\"\n\"subPortDesc\" = \"订阅服务监听的端口号（必须是未使用的端口）\"\n\"subCertPath\" = \"公钥路径\"\n\"subCertPathDesc\" = \"订阅服务使用的公钥文件路径（以 '/' 开头）\"\n\"subKeyPath\" = \"私钥路径\"\n\"subKeyPathDesc\" = \"订阅服务使用的私钥文件路径（以 '/' 开头）\"\n\"subPath\" = \"URI 路径\"\n\"subPathDesc\" = \"订阅服务使用的 URI 路径（以 '/' 开头，以 '/' 结尾）\"\n\"subDomain\" = \"监听域名\"\n\"subDomainDesc\" = \"订阅服务监听的域名（留空表示监听所有域名和 IP）\"\n\"subUpdates\" = \"更新间隔\"\n\"subUpdatesDesc\" = \"客户端应用中订阅 URL 的更新间隔（单位：小时）\"\n\"subEncrypt\" = \"编码\"\n\"subEncryptDesc\" = \"订阅服务返回的内容将采用 Base64 编码\"\n\"subShowInfo\" = \"显示使用信息\"\n\"subShowInfoDesc\" = \"客户端应用中将显示剩余流量和日期信息\"\n\"subURI\" = \"反向代理 URI\"\n\"subURIDesc\" = \"用于代理后面的订阅 URL 的 URI 路径\"\n\"externalTrafficInformEnable\" = \"外部交通通知\"\n\"externalTrafficInformEnableDesc\" = \"每次流量更新时通知外部 API\"\n\"externalTrafficInformURI\" = \"外部流量通知 URI\"\n\"externalTrafficInformURIDesc\" = \"流量更新将发送到此 URI\"\n\"fragment\" = \"分片\"\n\"fragmentDesc\" = \"启用 TLS hello 数据包分片\"\n\"fragmentSett\" = \"设置\"\n\"noisesDesc\" = \"启用 Noises.\"\n\"noisesSett\" = \"Noises 设置\"\n\"mux\" = \"多路复用器\"\n\"muxDesc\" = \"在已建立的数据流内传输多个独立的数据流\"\n\"muxSett\" = \"复用器设置\"\n\"direct\" = \"直接连接\"\n\"directDesc\" = \"直接与特定国家的域或IP范围建立连接\"\n\"notifications\" = \"通知\"\n\"certs\" = \"证书\"\n\"externalTraffic\" = \"外部流量\"\n\"dateAndTime\" = \"日期和时间\"\n\"proxyAndServer\" = \"代理和服务器\"\n\"intervals\" = \"间隔\"\n\"information\" = \"信息\"\n\"language\" = \"语言\"\n\"telegramBotLanguage\" = \"Telegram 机器人语言\"\n\n[pages.xray]\n\"title\" = \"Xray 配置\"\n\"save\" = \"保存\"\n\"restart\" = \"重新启动 Xray\"\n\"restartSuccess\" = \"Xray 已成功重新启动\"\n\"stopSuccess\" = \"Xray 已成功停止\"\n\"restartError\" = \"重启Xray时发生错误。\"\n\"stopError\" = \"停止Xray时发生错误。\"\n\"basicTemplate\" = \"基础配置\"\n\"advancedTemplate\" = \"高级配置\"\n\"generalConfigs\" = \"常规配置\"\n\"generalConfigsDesc\" = \"这些选项将决定常规配置\"\n\"logConfigs\" = \"日志\"\n\"logConfigsDesc\" = \"日志可能会影响服务器的性能，建议仅在需要时启用\"\n\"blockConfigsDesc\" = \"这些选项将阻止用户连接到特定协议和网站\"\n\"basicRouting\" = \"基本路由\"\n\"blockConnectionsConfigsDesc\" = \"这些选项将根据特定的请求国家阻止流量。\"\n\"directConnectionsConfigsDesc\" = \"直接连接确保特定的流量不会通过其他服务器路由。\"\n\"blockips\" = \"阻止IP\"\n\"blockdomains\" = \"阻止域名\"\n\"directips\" = \"直接IP\"\n\"directdomains\" = \"直接域名\"\n\"ipv4Routing\" = \"IPv4 路由\"\n\"ipv4RoutingDesc\" = \"此选项将仅通过 IPv4 路由到目标域\"\n\"warpRouting\" = \"WARP 路由\"\n\"warpRoutingDesc\" = \"注意：在使用这些选项之前，请按照面板 GitHub 上的步骤在你的服务器上以 socks5 代理模式安装 WARP。WARP 将通过 Cloudflare 服务器将流量路由到网站。\"\n\"Template\" = \"高级 Xray 配置模板\"\n\"TemplateDesc\" = \"最终的 Xray 配置文件将基于此模板生成\"\n\"FreedomStrategy\" = \"Freedom 协议策略\"\n\"FreedomStrategyDesc\" = \"设置 Freedom 协议中网络的输出策略\"\n\"RoutingStrategy\" = \"配置路由域策略\"\n\"RoutingStrategyDesc\" = \"设置 DNS 解析的整体路由策略\"\n\"outboundTestUrl\" = \"出站测试 URL\"\n\"outboundTestUrlDesc\" = \"测试出站连接时使用的 URL\"\n\"Torrent\" = \"屏蔽 BitTorrent 协议\"\n\"Inbounds\" = \"入站规则\"\n\"InboundsDesc\" = \"接受来自特定客户端的流量\"\n\"Outbounds\" = \"出站规则\"\n\"Balancers\" = \"负载均衡\"\n\"OutboundsDesc\" = \"设置出站流量传出方式\"\n\"Routings\" = \"路由规则\"\n\"RoutingsDesc\" = \"每条规则的优先级都很重要\"\n\"completeTemplate\" = \"全部\"\n\"logLevel\" = \"日志级别\"\n\"logLevelDesc\" = \"错误日志的日志级别，用于指示需要记录的信息\"\n\"accessLog\" = \"访问日志\"\n\"accessLogDesc\" = \"访问日志的文件路径。特殊值 'none' 禁用访问日志\"\n\"errorLog\" = \"错误日志\"\n\"errorLogDesc\" = \"错误日志的文件路径。特殊值 'none' 禁用错误日志\"\n\"dnsLog\" = \"DNS 日志\"\n\"dnsLogDesc\" = \"是否启用 DNS 查询日志\"\n\"maskAddress\" = \"隐藏地址\"\n\"maskAddressDesc\" = \"IP 地址掩码，启用时会自动替换日志中出现的 IP 地址。\"\n\"statistics\" = \"统计\"\n\"statsInboundUplink\" = \"入站上传统计\"\n\"statsInboundUplinkDesc\" = \"启用所有入站代理的上行流量统计收集。\"\n\"statsInboundDownlink\" = \"入站下载统计\"\n\"statsInboundDownlinkDesc\" = \"启用所有入站代理的下行流量统计收集。\"\n\"statsOutboundUplink\" = \"出站上传统计\"\n\"statsOutboundUplinkDesc\" = \"启用所有出站代理的上行流量统计收集。\"\n\"statsOutboundDownlink\" = \"出站下载统计\"\n\"statsOutboundDownlinkDesc\" = \"启用所有出站代理的下行流量统计收集。\"\n\n[pages.xray.rules]\n\"first\" = \"置顶\"\n\"last\" = \"置底\"\n\"up\" = \"向上\"\n\"down\" = \"向下\"\n\"source\" = \"来源\"\n\"dest\" = \"目的地址\"\n\"inbound\" = \"入站\"\n\"outbound\" = \"出站\"\n\"balancer\" = \"负载均衡\"\n\"info\" = \"信息\"\n\"add\" = \"添加规则\"\n\"edit\" = \"编辑规则\"\n\"useComma\" = \"逗号分隔的项目\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"添加出站\"\n\"addReverse\" = \"添加反向\"\n\"editOutbound\" = \"编辑出站\"\n\"editReverse\" = \"编辑反向\"\n\"tag\" = \"标签\"\n\"tagDesc\" = \"唯一标签\"\n\"address\" = \"地址\"\n\"reverse\" = \"反向\"\n\"domain\" = \"域名\"\n\"type\" = \"类型\"\n\"bridge\" = \"Bridge\"\n\"portal\" = \"Portal\"\n\"link\" = \"链接\"\n\"intercon\" = \"互连\"\n\"settings\" = \"设置\"\n\"accountInfo\" = \"帐户信息\"\n\"outboundStatus\" = \"出站状态\"\n\"sendThrough\" = \"发送通过\"\n\"test\" = \"测试\"\n\"testResult\" = \"测试结果\"\n\"testing\" = \"正在测试连接...\"\n\"testSuccess\" = \"测试成功\"\n\"testFailed\" = \"测试失败\"\n\"testError\" = \"测试出站失败\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"添加负载均衡\"\n\"editBalancer\" = \"编辑负载均衡\"\n\"balancerStrategy\" = \"策略\"\n\"balancerSelectors\" = \"选择器\"\n\"tag\" = \"标签\"\n\"tagDesc\" = \"唯一标签\"\n\"balancerDesc\" = \"无法同时使用 balancerTag 和 outboundTag。如果同时使用，则只有 outboundTag 会生效。\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"密钥\"\n\"publicKey\" = \"公钥\"\n\"allowedIPs\" = \"允许的 IP\"\n\"endpoint\" = \"端点\"\n\"psk\" = \"共享密钥\"\n\"domainStrategy\" = \"域策略\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"TUN 接口的名称。默认值为 'xray0'\"\n\"mtuDesc\" = \"最大传输单元。数据包的最大大小。默认值为 1500\"\n\"userLevel\" = \"用户级别\"\n\"userLevelDesc\" = \"通过此入站的所有连接都将使用此用户级别。默认值为 0\"\n\n[pages.xray.dns]\n\"enable\" = \"启用 DNS\"\n\"enableDesc\" = \"启用内置 DNS 服务器\"\n\"tag\" = \"DNS 入站标签\"\n\"tagDesc\" = \"此标签将在路由规则中可用作入站标签\"\n\"clientIp\" = \"客户端IP\"\n\"clientIpDesc\" = \"用于在DNS查询期间通知服务器指定的IP位置\"\n\"disableCache\" = \"禁用缓存\"\n\"disableCacheDesc\" = \"禁用DNS缓存\"\n\"disableFallback\" = \"禁用回退\"\n\"disableFallbackDesc\" = \"禁用回退DNS查询\"\n\"disableFallbackIfMatch\" = \"匹配时禁用回退\"\n\"disableFallbackIfMatchDesc\" = \"当DNS服务器的匹配域名列表命中时，禁用回退DNS查询\"\n\"enableParallelQuery\" = \"启用并行查询\"\n\"enableParallelQueryDesc\" = \"启用并行DNS查询到多个服务器以实现更快的解析\"\n\"strategy\" = \"查询策略\"\n\"strategyDesc\" = \"解析域名的总体策略\"\n\"add\" = \"添加服务器\"\n\"edit\" = \"编辑服务器\"\n\"domains\" = \"域\"\n\"expectIPs\" = \"预期 IP\"\n\"unexpectIPs\" = \"意外IP\"\n\"useSystemHosts\" = \"使用系统Hosts\"\n\"useSystemHostsDesc\" = \"使用已安装系统的hosts文件\"\n\"usePreset\" = \"使用模板\"\n\"dnsPresetTitle\" = \"DNS模板\"\n\"dnsPresetFamily\" = \"家庭\"\n\n[pages.xray.fakedns]\n\"add\" = \"添加假 DNS\"\n\"edit\" = \"编辑假 DNS\"\n\"ipPool\" = \"IP 池子网\"\n\"poolSize\" = \"池大小\"\n\n[pages.settings.security]\n\"admin\" = \"管理员凭据\"\n\"twoFactor\" = \"双重验证\"\n\"twoFactorEnable\" = \"启用2FA\"\n\"twoFactorEnableDesc\" = \"增加额外的验证层以提高安全性。\"\n\"twoFactorModalSetTitle\" = \"启用双重认证\"\n\"twoFactorModalDeleteTitle\" = \"停用双重认证\"\n\"twoFactorModalSteps\" = \"要设定双重认证，请执行以下步骤：\"\n\"twoFactorModalFirstStep\" = \"1. 在认证应用程序中扫描此QR码，或复制QR码附近的令牌并粘贴到应用程序中\"\n\"twoFactorModalSecondStep\" = \"2. 输入应用程序中的验证码\"\n\"twoFactorModalRemoveStep\" = \"输入应用程序中的验证码以移除双重认证。\"\n\"twoFactorModalChangeCredentialsTitle\" = \"更改凭据\"\n\"twoFactorModalChangeCredentialsStep\" = \"输入应用程序中的代码以更改管理员凭据。\"\n\"twoFactorModalSetSuccess\" = \"双因素认证已成功建立\"\n\"twoFactorModalDeleteSuccess\" = \"双因素认证已成功删除\"\n\"twoFactorModalError\" = \"验证码错误\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"参数已更改。\"\n\"getSettings\" = \"获取参数时发生错误\"\n\"modifyUserError\" = \"更改管理员凭据时发生错误。\"\n\"modifyUser\" = \"您已成功更改管理员凭据。\"\n\"originalUserPassIncorrect\" = \"原用户名或原密码错误\"\n\"userPassMustBeNotEmpty\" = \"新用户名和新密码不能为空\"\n\"getOutboundTrafficError\" = \"获取出站流量错误\"\n\"resetOutboundTrafficError\" = \"重置出站流量错误\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ 自定义键盘已关闭！\"\n\"noResult\" = \"❗ 没有结果！\"\n\"noQuery\" = \"❌ 未找到查询！请再次使用该命令！\"\n\"wentWrong\" = \"❌ 出了点问题！\"\n\"noIpRecord\" = \"❗ 没有IP记录！\"\n\"noInbounds\" = \"❗ 未找到入站！\"\n\"unlimited\" = \"♾ 无限（重置）\"\n\"add\" = \"添加\"\n\"month\" = \"月\"\n\"months\" = \"月\"\n\"day\" = \"天\"\n\"days\" = \"天\"\n\"hours\" = \"小时\"\n\"minutes\" = \"分钟\"\n\"unknown\" = \"未知\"\n\"inbounds\" = \"入站\"\n\"clients\" = \"客户端\"\n\"offline\" = \"🔴 离线\"\n\"online\" = \"🟢 在线\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ 未知命令\"\n\"pleaseChoose\" = \"👇 请选择：\\r\\n\"\n\"help\" = \"🤖 欢迎使用本机器人！它旨在为您提供来自服务器的特定数据，并允许您进行必要的修改。\\r\\n\\r\\n\"\n\"start\" = \"👋 你好，<i>{{ .Firstname }}</i>。\\r\\n\"\n\"welcome\" = \"🤖 欢迎来到 <b>{{ .Hostname }}</b> 管理机器人。\\r\\n\"\n\"status\" = \"✅ 机器人正常运行！\"\n\"usage\" = \"❗ 请输入要搜索的文本！\"\n\"getID\" = \"🆔 您的 ID 为：<code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"要重新启动 Xray Core：\\r\\n<code>/restart</code>\\r\\n\\r\\n要搜索客户电子邮件：\\r\\n<code>/usage [电子邮件]</code>\\r\\n\\r\\n要搜索入站（带有客户统计数据）：\\r\\n<code>/inbound [备注]</code>\\r\\n\\r\\nTelegram聊天ID：\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"要搜索统计数据，请使用以下命令：\\r\\n<code>/usage [电子邮件]</code>\\r\\n\\r\\nTelegram聊天ID：\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ 操作成功!\"\n\"restartFailed\" = \"❗ 操作错误。\\r\\n\\r\\n<code>错误: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core 未运行。\"\n\"startDesc\" = \"显示主菜单\"\n\"helpDesc\" = \"机器人帮助\"\n\"statusDesc\" = \"检查机器人状态\"\n\"idDesc\" = \"显示您的 Telegram ID\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 CPU 使用率为 {{ .Percent }}%，超过阈值 {{ .Threshold }}%\"\n\"selectUserFailed\" = \"❌ 用户选择错误！\"\n\"userSaved\" = \"✅ 电报用户已保存。\"\n\"loginSuccess\" = \"✅ 成功登录到面板。\\r\\n\"\n\"loginFailed\" = \"❗️ 面板登录失败。\\r\\n\"\n\"2faFailed\" = \"2FA 失败\"\n\"report\" = \"🕰 定时报告：{{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ 日期时间：{{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 主机名：{{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 X-UI 版本：{{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Xray 版本: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6：{{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4：{{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP：{{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IP 地址：\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ 服务器运行时间：{{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 服务器负载：{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 服务器内存：{{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP 连接数：{{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP 连接数：{{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 流量：{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Xray 状态：{{ .State }}\\r\\n\"\n\"username\" = \"👤 用户名：{{ .Username }}\\r\\n\"\n\"password\" = \"👤 密码: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ 时间：{{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 入站：{{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 端口：{{ .Port }}\\r\\n\"\n\"expire\" = \"📅 过期日期：{{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 剩余时间：{{ .Time }}\\r\\n\"\n\"active\" = \"💡 激活：{{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 已启用：{{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 连接状态：{{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 上次在线: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 邮箱：{{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 上传↑：{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 下载↓：{{ .Download }}\\r\\n\"\n\"total\" = \"📊 总计：{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 电报用户：{{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 耗尽的 {{ .Type }}：\\r\\n\"\n\"exhaustedCount\" = \"🚨 耗尽的 {{ .Type }} 数量：\\r\\n\"\n\"onlinesCount\" = \"🌐 在线客户：{{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 禁用：{{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 即将耗尽：{{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 备份时间：{{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 刷新时间：{{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ 是的\"\n\"no\" = \"❌ 没有\"\n\"received_id\" = \"🔑📥 ID 已更新。\"\n\"received_password\" = \"🔑📥 密码已更新。\"\n\"received_email\" = \"📧📥 邮箱已更新。\"\n\"received_comment\" = \"💬📥 评论已更新。\"\n\"id_prompt\" = \"🔑 默认 ID: {{ .ClientId }}\\n\\n请输入您的 ID。\"\n\"pass_prompt\" = \"🔑 默认密码: {{ .ClientPassword }}\\n\\n请输入您的密码。\"\n\"email_prompt\" = \"📧 默认邮箱: {{ .ClientEmail }}\\n\\n请输入您的邮箱。\"\n\"comment_prompt\" = \"💬 默认评论: {{ .ClientComment }}\\n\\n请输入您的评论。\"\n\"inbound_client_data_id\" = \"🔄 入站: {{ .InboundRemark }}\\n\\n🔑 ID: {{ .ClientId }}\\n📧 邮箱: {{ .ClientEmail }}\\n📊 流量: {{ .ClientTraffic }}\\n📅 到期日期: {{ .ClientExp }}\\n🌐 IP 限制: {{ .IpLimit }}\\n💬 备注: {{ .ClientComment }}\\n\\n你现在可以将客户添加到入站了！\"\n\"inbound_client_data_pass\" = \"🔄 入站: {{ .InboundRemark }}\\n\\n🔑 密码: {{ .ClientPass }}\\n📧 邮箱: {{ .ClientEmail }}\\n📊 流量: {{ .ClientTraffic }}\\n📅 到期日期: {{ .ClientExp }}\\n🌐 IP 限制: {{ .IpLimit }}\\n💬 备注: {{ .ClientComment }}\\n\\n你现在可以将客户添加到入站了！\"\n\"cancel\" = \"❌ 进程已取消！\\n\\n您可以随时使用 /start 重新开始。 🔄\"\n\"error_add_client\" = \"⚠️ 错误:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"好的，我会使用默认值。 😊\"\n\"incorrect_input\" = \"您的输入无效。\\n短语应连续输入，不能有空格。\\n正确示例: aaaaaa\\n错误示例: aaa aaa 🚫\"\n\"AreYouSure\" = \"你确定吗？🤔\"\n\"SuccessResetTraffic\" = \"📧 邮箱: {{ .ClientEmail }}\\n🏁 结果: ✅ 成功\"\n\"FailedResetTraffic\" = \"📧 邮箱: {{ .ClientEmail }}\\n🏁 结果: ❌ 失败 \\n\\n🛠️ 错误: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 所有客户的流量重置已完成。\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ 关闭键盘\"\n\"cancel\" = \"❌ 取消\"\n\"cancelReset\" = \"❌ 取消重置\"\n\"cancelIpLimit\" = \"❌ 取消 IP 限制\"\n\"confirmResetTraffic\" = \"✅ 确认重置流量？\"\n\"confirmClearIps\" = \"✅ 确认清除 IP？\"\n\"confirmRemoveTGUser\" = \"✅ 确认移除 Telegram 用户？\"\n\"confirmToggle\" = \"✅ 确认启用/禁用用户？\"\n\"dbBackup\" = \"获取数据库备份\"\n\"serverUsage\" = \"服务器使用情况\"\n\"getInbounds\" = \"获取入站信息\"\n\"depleteSoon\" = \"即将耗尽\"\n\"clientUsage\" = \"获取使用情况\"\n\"onlines\" = \"在线客户端\"\n\"commands\" = \"命令\"\n\"refresh\" = \"🔄 刷新\"\n\"clearIPs\" = \"❌ 清除 IP\"\n\"removeTGUser\" = \"❌ 移除 Telegram 用户\"\n\"selectTGUser\" = \"👤 选择 Telegram 用户\"\n\"selectOneTGUser\" = \"👤 选择一个 Telegram 用户：\"\n\"resetTraffic\" = \"📈 重置流量\"\n\"resetExpire\" = \"📅 更改到期日期\"\n\"ipLog\" = \"🔢 IP 日志\"\n\"ipLimit\" = \"🔢 IP 限制\"\n\"setTGUser\" = \"👤 设置 Telegram 用户\"\n\"toggle\" = \"🔘 启用/禁用\"\n\"custom\" = \"🔢 风俗\"\n\"confirmNumber\" = \"✅ 确认: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ 确认添加：{{ .Num }}\"\n\"limitTraffic\" = \"🚧 流量限制\"\n\"getBanLogs\" = \"禁止日志\"\n\"allClients\" = \"所有客户\"\n\"addClient\" = \"添加客户\"\n\"submitDisable\" = \"提交为禁用 ☑️\"\n\"submitEnable\" = \"提交为启用 ✅\"\n\"use_default\" = \"🏷️ 使用默认\"\n\"change_id\" = \"⚙️🔑 ID\"\n\"change_password\" = \"⚙️🔑 密码\"\n\"change_email\" = \"⚙️📧 邮箱\"\n\"change_comment\" = \"⚙️💬 评论\"\n\"ResetAllTraffics\" = \"重置所有流量\"\n\"SortedTrafficUsageReport\" = \"排序的流量使用报告\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ 成功！\"\n\"errorOperation\" = \"❗ 操作错误。\"\n\"getInboundsFailed\" = \"❌ 获取入站信息失败。\"\n\"getClientsFailed\" = \"❌ 获取客户失败。\"\n\"canceled\" = \"❌ {{ .Email }}：操作已取消。\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}：客户端刷新成功。\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}：IP 刷新成功。\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}：客户端的 Telegram 用户刷新成功。\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}：流量已重置成功。\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: 流量限制保存成功。\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}：过期天数已重置成功。\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}：成功保存 IP 限制数量为 {{ .Count }}。\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}：IP 已成功清除。\"\n\"getIpLog\" = \"✅ {{ .Email }}：获取 IP 日志。\"\n\"getUserInfo\" = \"✅ {{ .Email }}：获取 Telegram 用户信息。\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}：Telegram 用户已成功移除。\"\n\"enableSuccess\" = \"✅ {{ .Email }}：已成功启用。\"\n\"disableSuccess\" = \"✅ {{ .Email }}：已成功禁用。\"\n\"askToAddUserId\" = \"未找到您的配置！\\r\\n请向管理员询问，在您的配置中使用您的 Telegram 用户 ChatID。\\r\\n\\r\\n您的用户 ChatID：<code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"为入站 {{ .Inbound }} 选择一个客户\"\n\"chooseInbound\" = \"选择一个入站\"\n"
  },
  {
    "path": "web/translation/translate.zh_TW.toml",
    "content": "\"username\" = \"使用者名稱\"\n\"password\" = \"密碼\"\n\"login\" = \"登入\"\n\"confirm\" = \"確定\"\n\"cancel\" = \"取消\"\n\"close\" = \"關閉\"\n\"create\" = \"建立\"\n\"update\" = \"更新\"\n\"copy\" = \"複製\"\n\"copied\" = \"已複製\"\n\"download\" = \"下載\"\n\"remark\" = \"備註\"\n\"enable\" = \"啟用\"\n\"protocol\" = \"協議\"\n\"search\" = \"搜尋\"\n\"filter\" = \"篩選\"\n\"loading\" = \"載入中...\"\n\"second\" = \"秒\"\n\"minute\" = \"分鐘\"\n\"hour\" = \"小時\"\n\"day\" = \"天\"\n\"check\" = \"檢視\"\n\"indefinite\" = \"無限期\"\n\"unlimited\" = \"無限制\"\n\"none\" = \"無\"\n\"qrCode\" = \"二維碼\"\n\"info\" = \"更多資訊\"\n\"edit\" = \"編輯\"\n\"delete\" = \"刪除\"\n\"reset\" = \"重置\"\n\"noData\" = \"無數據。\"\n\"copySuccess\" = \"複製成功\"\n\"sure\" = \"確定\"\n\"encryption\" = \"加密\"\n\"useIPv4ForHost\" = \"使用 IPv4 連接主機\"\n\"transmission\" = \"傳輸\"\n\"host\" = \"主機\"\n\"path\" = \"路徑\"\n\"camouflage\" = \"偽裝\"\n\"status\" = \"狀態\"\n\"enabled\" = \"開啟\"\n\"disabled\" = \"關閉\"\n\"depleted\" = \"耗盡\"\n\"depletingSoon\" = \"即將耗盡\"\n\"offline\" = \"離線\"\n\"online\" = \"線上\"\n\"domainName\" = \"域名\"\n\"monitor\" = \"監聽\"\n\"certificate\" = \"憑證\"\n\"fail\" = \"失敗\"\n\"comment\" = \"評論\"\n\"success\" = \"成功\"\n\"lastOnline\" = \"上次上線\"\n\"getVersion\" = \"獲取版本\"\n\"install\" = \"安裝\"\n\"clients\" = \"客戶端\"\n\"usage\" = \"使用情況\"\n\"twoFactorCode\" = \"代碼\"\n\"remained\" = \"剩餘\"\n\"security\" = \"安全\"\n\"secAlertTitle\" = \"安全警報\"\n\"secAlertSsl\" = \"此連線不安全。在啟用 TLS 進行資料保護之前，請勿輸入敏感資訊。\"\n\"secAlertConf\" = \"某些設定易受攻擊。建議加強安全協議以防止潛在漏洞。\"\n\"secAlertSSL\" = \"面板缺少安全連線。請安裝 TLS 證書以保護資料安全。\"\n\"secAlertPanelPort\" = \"面板預設埠存在安全風險。請配置隨機埠或特定埠。\"\n\"secAlertPanelURI\" = \"面板預設 URI 路徑不安全。請配置複雜的 URI 路徑。\"\n\"secAlertSubURI\" = \"訂閱預設 URI 路徑不安全。請配置複雜的 URI 路徑。\"\n\"secAlertSubJsonURI\" = \"訂閱 JSON 預設 URI 路徑不安全。請配置複雜的 URI 路徑。\"\n\"emptyDnsDesc\" = \"未添加DNS伺服器。\"\n\"emptyFakeDnsDesc\" = \"未添加Fake DNS伺服器。\"\n\"emptyBalancersDesc\" = \"未添加負載平衡器。\"\n\"emptyReverseDesc\" = \"未添加反向代理。\"\n\"somethingWentWrong\" = \"發生錯誤\"\n\n[subscription]\n\"title\" = \"訂閱資訊\"\n\"subId\" = \"訂閱 ID\"\n\"status\" = \"狀態\"\n\"downloaded\" = \"已下載\"\n\"uploaded\" = \"已上傳\"\n\"expiry\" = \"到期\"\n\"totalQuota\" = \"總配額\"\n\"individualLinks\" = \"個別連結\"\n\"active\" = \"啟用\"\n\"inactive\" = \"停用\"\n\"unlimited\" = \"無限制\"\n\"noExpiry\" = \"無到期\"\n\n[menu]\n\"theme\" = \"主題\"\n\"dark\" = \"深色\"\n\"ultraDark\" = \"超深色\"\n\"dashboard\" = \"系統狀態\"\n\"inbounds\" = \"入站列表\"\n\"settings\" = \"面板設定\"\n\"xray\" = \"Xray 設定\"\n\"logout\" = \"退出登入\"\n\"link\" = \"管理\"\n\n[pages.login]\n\"hello\" = \"你好\"\n\"title\" = \"歡迎\"\n\"loginAgain\" = \"登入時效已過，請重新登入\"\n\n[pages.login.toasts]\n\"invalidFormData\" = \"資料格式錯誤\"\n\"emptyUsername\" = \"請輸入使用者名稱\"\n\"emptyPassword\" = \"請輸入密碼\"\n\"wrongUsernameOrPassword\" = \"用戶名、密碼或雙重驗證碼無效。\"\n\"successLogin\" = \"您已成功登入您的帳戶。\"\n\n[pages.index]\n\"title\" = \"系統狀態\"\n\"cpu\" = \"CPU\"\n\"logicalProcessors\" = \"邏輯處理器\"\n\"frequency\" = \"頻率\"\n\"swap\" = \"交換空間\"\n\"storage\" = \"儲存\"\n\"memory\" = \"記憶體\"\n\"threads\" = \"執行緒\"\n\"xrayStatus\" = \"Xray\"\n\"stopXray\" = \"停止\"\n\"restartXray\" = \"重啟\"\n\"xraySwitch\" = \"版本\"\n\"xraySwitchClick\" = \"選擇你要切換到的版本\"\n\"xraySwitchClickDesk\" = \"請謹慎選擇，因為較舊版本可能與當前配置不相容\"\n\"xrayStatusUnknown\" = \"未知\"\n\"xrayStatusRunning\" = \"運行中\"\n\"xrayStatusStop\" = \"停止\"\n\"xrayStatusError\" = \"錯誤\"\n\"xrayErrorPopoverTitle\" = \"執行Xray時發生錯誤\"\n\"operationHours\" = \"系統正常執行時間\"\n\"systemLoad\" = \"系統負載\"\n\"systemLoadDesc\" = \"過去 1、5 和 15 分鐘的系統平均負載\"\n\"connectionCount\" = \"連線數\"\n\"ipAddresses\" = \"IP地址\"\n\"toggleIpVisibility\" = \"切換IP可見性\"\n\"overallSpeed\" = \"整體速度\"\n\"upload\" = \"上傳\"\n\"download\" = \"下載\"\n\"totalData\" = \"總數據\"\n\"sent\" = \"已發送\"\n\"received\" = \"已接收\"\n\"documentation\" = \"文件\"\n\"xraySwitchVersionDialog\" = \"您確定要變更Xray版本嗎？\"\n\"xraySwitchVersionDialogDesc\" = \"這將會把Xray版本變更為#version#。\"\n\"xraySwitchVersionPopover\" = \"Xray 更新成功\"\n\"geofileUpdateDialog\" = \"您確定要更新地理檔案嗎？\"\n\"geofileUpdateDialogDesc\" = \"這將更新 #filename# 檔案。\"\n\"geofilesUpdateDialogDesc\" = \"這將更新所有文件。\"\n\"geofilesUpdateAll\" = \"全部更新\"\n\"geofileUpdatePopover\" = \"地理檔案更新成功\"\n\"dontRefresh\" = \"安裝中，請勿重新整理此頁面\"\n\"logs\" = \"日誌\"\n\"config\" = \"配置\"\n\"backup\" = \"備份和恢復\"\n\"backupTitle\" = \"備份和恢復資料庫\"\n\"exportDatabase\" = \"備份\"\n\"exportDatabaseDesc\" = \"點擊下載包含當前資料庫備份的 .db 文件到您的設備。\"\n\"importDatabase\" = \"恢復\"\n\"importDatabaseDesc\" = \"點擊選擇並上傳設備中的 .db 文件以從備份恢復資料庫。\"\n\"importDatabaseSuccess\" = \"資料庫匯入成功\"\n\"importDatabaseError\" = \"匯入資料庫時發生錯誤\"\n\"readDatabaseError\" = \"讀取資料庫時發生錯誤\"\n\"getDatabaseError\" = \"檢索資料庫時發生錯誤\"\n\"getConfigError\" = \"檢索設定檔時發生錯誤\"\n\n[pages.inbounds]\n\"allTimeTraffic\" = \"累計總流量\"\n\"allTimeTrafficUsage\" = \"所有时间总使用量\"\n\"title\" = \"入站列表\"\n\"totalDownUp\" = \"總上傳 / 下載\"\n\"totalUsage\" = \"總用量\"\n\"inboundCount\" = \"入站數量\"\n\"operate\" = \"選單\"\n\"enable\" = \"啟用\"\n\"remark\" = \"備註\"\n\"protocol\" = \"協議\"\n\"port\" = \"埠\"\n\"portMap\" = \"埠映射\"\n\"traffic\" = \"流量\"\n\"details\" = \"詳細資訊\"\n\"transportConfig\" = \"傳輸配置\"\n\"expireDate\" = \"到期時間\"\n\"createdAt\" = \"建立時間\"\n\"updatedAt\" = \"更新時間\"\n\"resetTraffic\" = \"重置流量\"\n\"addInbound\" = \"新增入站\"\n\"generalActions\" = \"通用操作\"\n\"autoRefresh\" = \"自動刷新\"\n\"autoRefreshInterval\" = \"間隔\"\n\"modifyInbound\" = \"修改入站\"\n\"deleteInbound\" = \"刪除入站\"\n\"deleteInboundContent\" = \"確定要刪除入站嗎？\"\n\"deleteClient\" = \"刪除客戶端\"\n\"deleteClientContent\" = \"確定要刪除客戶端嗎？\"\n\"resetTrafficContent\" = \"確定要重置流量嗎？\"\n\"copyLink\" = \"複製連結\"\n\"address\" = \"地址\"\n\"network\" = \"網路\"\n\"destinationPort\" = \"目標埠\"\n\"targetAddress\" = \"目標地址\"\n\"monitorDesc\" = \"留空表示監聽所有 IP\"\n\"meansNoLimit\" = \"= 無限制（單位：GB)\"\n\"totalFlow\" = \"總流量\"\n\"leaveBlankToNeverExpire\" = \"留空表示永不過期\"\n\"noRecommendKeepDefault\" = \"建議保留預設值\"\n\"certificatePath\" = \"檔案路徑\"\n\"certificateContent\" = \"檔案內容\"\n\"publicKey\" = \"公鑰\"\n\"privatekey\" = \"私鑰\"\n\"clickOnQRcode\" = \"點選二維碼複製\"\n\"client\" = \"客戶\"\n\"export\" = \"匯出連結\"\n\"clone\" = \"複製\"\n\"cloneInbound\" = \"複製\"\n\"cloneInboundContent\" = \"此入站規則除埠（Port）、監聽 IP（Listening IP）和客戶端（Clients）以外的所有配置都將應用於克隆\"\n\"cloneInboundOk\" = \"建立克隆\"\n\"resetAllTraffic\" = \"重置所有入站流量\"\n\"resetAllTrafficTitle\" = \"重置所有入站流量\"\n\"resetAllTrafficContent\" = \"確定要重置所有入站流量嗎？\"\n\"resetInboundClientTraffics\" = \"重置客戶端流量\"\n\"resetInboundClientTrafficTitle\" = \"重置所有客戶端流量\"\n\"resetInboundClientTrafficContent\" = \"確定要重置此入站客戶端的所有流量嗎？\"\n\"resetAllClientTraffics\" = \"重置所有客戶端流量\"\n\"resetAllClientTrafficTitle\" = \"重置所有客戶端流量\"\n\"resetAllClientTrafficContent\" = \"確定要重置所有客戶端的所有流量嗎？\"\n\"delDepletedClients\" = \"刪除流量耗盡的客戶端\"\n\"delDepletedClientsTitle\" = \"刪除流量耗盡的客戶端\"\n\"delDepletedClientsContent\" = \"確定要刪除所有流量耗盡的客戶端嗎？\"\n\"email\" = \"電子郵件\"\n\"emailDesc\" = \"電子郵件必須完全唯一\"\n\"IPLimit\" = \"IP 限制\"\n\"IPLimitDesc\" = \"如果數量超過設定值，則禁用入站流量。（0 = 禁用）\"\n\"IPLimitlog\" = \"IP 日誌\"\n\"IPLimitlogDesc\" = \"IP 歷史日誌（要啟用被禁用的入站流量，請清除日誌）\"\n\"IPLimitlogclear\" = \"清除日誌\"\n\"setDefaultCert\" = \"從面板設定證書\"\n\"telegramDesc\" = \"請提供Telegram聊天ID。（在機器人中使用'/id'命令）或（@userinfobot\"\n\"subscriptionDesc\" = \"要找到你的訂閱 URL，請導航到“詳細資訊”。此外，你可以為多個客戶端使用相同的名稱。\"\n\"info\" = \"資訊\"\n\"same\" = \"相同\"\n\"inboundData\" = \"入站資料\"\n\"exportInbound\" = \"匯出入站規則\"\n\"import\" = \"匯入\"\n\"importInbound\" = \"匯入入站規則\"\n\"periodicTrafficResetTitle\" = \"流量重置\"\n\"periodicTrafficResetDesc\" = \"按指定間隔自動重置流量計數器\"\n\"lastReset\" = \"上次重置\"\n\n[pages.client]\n\"add\" = \"新增客戶端\"\n\"edit\" = \"編輯客戶端\"\n\"submitAdd\" = \"新增客戶端\"\n\"submitEdit\" = \"儲存修改\"\n\"clientCount\" = \"客戶端數量\"\n\"bulk\" = \"批量建立\"\n\"method\" = \"方法\"\n\"first\" = \"置頂\"\n\"last\" = \"置底\"\n\"prefix\" = \"字首\"\n\"postfix\" = \"字尾\"\n\"delayedStart\" = \"首次使用後開始\"\n\"expireDays\" = \"期間\"\n\"days\" = \"天\"\n\"renew\" = \"自動續訂\"\n\"renewDesc\" = \"到期後自動續訂。(0 = 禁用)(單位: 天)\"\n\n[pages.inbounds.periodicTrafficReset]\n\"never\" = \"從不\"\n\"daily\" = \"每日\"\n\"weekly\" = \"每週\"\n\"monthly\" = \"每月\"\n\n[pages.inbounds.toasts]\n\"obtain\" = \"獲取\"\n\"updateSuccess\" = \"更新成功\"\n\"logCleanSuccess\" = \"日誌已清除\"\n\"inboundsUpdateSuccess\" = \"入站連接已成功更新\"\n\"inboundUpdateSuccess\" = \"入站連接已成功更新\"\n\"inboundCreateSuccess\" = \"入站連接已成功建立\"\n\"inboundDeleteSuccess\" = \"入站連接已成功刪除\"\n\"inboundClientAddSuccess\" = \"已新增入站客戶端\"\n\"inboundClientDeleteSuccess\" = \"入站客戶端已刪除\"\n\"inboundClientUpdateSuccess\" = \"入站客戶端已更新\"\n\"delDepletedClientsSuccess\" = \"所有耗盡客戶端已刪除\"\n\"resetAllClientTrafficSuccess\" = \"客戶端所有流量已重置\"\n\"resetAllTrafficSuccess\" = \"所有流量已重置\"\n\"resetInboundClientTrafficSuccess\" = \"流量已重置\"\n\"trafficGetError\" = \"取得流量資料時發生錯誤\"\n\"getNewX25519CertError\" = \"取得X25519憑證時發生錯誤。\"\n\"getNewmldsa65Error\" = \"取得mldsa65憑證時發生錯誤。\"\n\"getNewVlessEncError\" = \"取得VlessEnc憑證時發生錯誤。\"\n\n[pages.inbounds.stream.general]\n\"request\" = \"請求\"\n\"response\" = \"響應\"\n\"name\" = \"名稱\"\n\"value\" = \"值\"\n\n[pages.inbounds.stream.tcp]\n\"version\" = \"版本\"\n\"method\" = \"方法\"\n\"path\" = \"路徑\"\n\"status\" = \"狀態\"\n\"statusDescription\" = \"狀態說明\"\n\"requestHeader\" = \"請求頭\"\n\"responseHeader\" = \"響應頭\"\n\n[pages.settings]\n\"title\" = \"面板設定\"\n\"save\" = \"儲存\"\n\"infoDesc\" = \"此處的所有更改都需要儲存並重啟面板才能生效\"\n\"restartPanel\" = \"重啟面板\"\n\"restartPanelDesc\" = \"確定要重啟面板嗎？若重啟後無法訪問面板，請前往伺服器檢視面板日誌資訊\"\n\"restartPanelSuccess\" = \"面板已成功重新啟動\"\n\"actions\" = \"操作\"\n\"resetDefaultConfig\" = \"重置為預設配置\"\n\"panelSettings\" = \"常規\"\n\"securitySettings\" = \"安全設定\"\n\"TGBotSettings\" = \"Telegram 機器人配置\"\n\"panelListeningIP\" = \"面板監聽 IP\"\n\"panelListeningIPDesc\" = \"預設留空監聽所有 IP\"\n\"panelListeningDomain\" = \"面板監聽域名\"\n\"panelListeningDomainDesc\" = \"預設情況下留空以監視所有域名和 IP 地址\"\n\"panelPort\" = \"面板監聽埠\"\n\"panelPortDesc\" = \"重啟面板生效\"\n\"publicKeyPath\" = \"面板證書公鑰檔案路徑\"\n\"publicKeyPathDesc\" = \"填寫一個 '/' 開頭的絕對路徑\"\n\"privateKeyPath\" = \"面板證書金鑰檔案路徑\"\n\"privateKeyPathDesc\" = \"填寫一個 '/' 開頭的絕對路徑\"\n\"panelUrlPath\" = \"面板 url 根路徑\"\n\"panelUrlPathDesc\" = \"必須以 '/' 開頭，以 '/' 結尾\"\n\"pageSize\" = \"分頁大小\"\n\"pageSizeDesc\" = \"定義入站表的頁面大小。設定 0 表示禁用\"\n\"remarkModel\" = \"備註模型和分隔符\"\n\"datepicker\" = \"日期選擇器\"\n\"datepickerPlaceholder\" = \"選擇日期\"\n\"datepickerDescription\" = \"選擇器日曆類型指定到期日期\"\n\"sampleRemark\" = \"備註示例\"\n\"oldUsername\" = \"原使用者名稱\"\n\"currentPassword\" = \"原密碼\"\n\"newUsername\" = \"新使用者名稱\"\n\"newPassword\" = \"新密碼\"\n\"telegramBotEnable\" = \"啟用 Telegram 機器人\"\n\"telegramBotEnableDesc\" = \"啟用 Telegram 機器人功能\"\n\"telegramToken\" = \"Telegram 機器人令牌（token）\"\n\"telegramTokenDesc\" = \"從 '@BotFather' 獲取的 Telegram 機器人令牌\"\n\"telegramProxy\" = \"SOCKS5 Proxy\"\n\"telegramProxyDesc\" = \"啟用 SOCKS5 代理連線到 Telegram（根據指南調整設定）\"\n\"telegramAPIServer\" = \"Telegram API Server\"\n\"telegramAPIServerDesc\" = \"要使用的 Telegram API 伺服器。留空以使用預設伺服器。\"\n\"telegramChatId\" = \"管理員聊天 ID\"\n\"telegramChatIdDesc\" = \"Telegram 管理員聊天 ID (多個以逗號分隔)（可通過 @userinfobot 獲取，或在機器人中使用 '/id' 命令獲取）\"\n\"telegramNotifyTime\" = \"通知時間\"\n\"telegramNotifyTimeDesc\" = \"設定週期性的 Telegram 機器人通知時間（使用 crontab 時間格式）\"\n\"tgNotifyBackup\" = \"資料庫備份\"\n\"tgNotifyBackupDesc\" = \"傳送帶有報告的資料庫備份檔案\"\n\"tgNotifyLogin\" = \"登入通知\"\n\"tgNotifyLoginDesc\" = \"當有人試圖登入你的面板時顯示使用者名稱、IP 地址和時間\"\n\"sessionMaxAge\" = \"會話時長\"\n\"sessionMaxAgeDesc\" = \"保持登入狀態的時長（單位：分鐘）\"\n\"expireTimeDiff\" = \"到期通知閾值\"\n\"expireTimeDiffDesc\" = \"達到此閾值時，將收到有關到期時間的通知（單位：天）\"\n\"trafficDiff\" = \"流量耗盡閾值\"\n\"trafficDiffDesc\" = \"達到此閾值時，將收到有關流量耗盡的通知（單位：GB）\"\n\"tgNotifyCpu\" = \"CPU 負載通知閾值\"\n\"tgNotifyCpuDesc\" = \"CPU 負載超過此閾值時，將收到通知（單位：%）\"\n\"timeZone\" = \"時區\"\n\"timeZoneDesc\" = \"定時任務將按照該時區的時間執行\"\n\"subSettings\" = \"訂閱設定\"\n\"subEnable\" = \"啟用訂閱服務\"\n\"subEnableDesc\" = \"啟用訂閱服務功能\"\n\"subJsonEnable\" = \"獨立啟用/停用 JSON 訂閱端點。\"\n\"subTitle\" = \"訂閱標題\"\n\"subTitleDesc\" = \"在VPN客戶端中顯示的標題\"\n\"subSupportUrl\" = \"支援連結\"\n\"subSupportUrlDesc\" = \"VPN 用戶端中顯示的技術支援連結\"\n\"subProfileUrl\" = \"個人資料連結\"\n\"subProfileUrlDesc\" = \"VPN 用戶端中顯示的網站連結\"\n\"subAnnounce\" = \"公告\"\n\"subAnnounceDesc\" = \"VPN 用戶端中顯示的公告文字\"\n\"subEnableRouting\" = \"啟用路由\"\n\"subEnableRoutingDesc\" = \"在 VPN 用戶端中啟用路由的全域設定。（僅限 Happ）\"\n\"subRoutingRules\" = \"路由規則\"\n\"subRoutingRulesDesc\" = \"VPN 用戶端的全域路由規則。（僅限 Happ）\"\n\"subListen\" = \"監聽 IP\"\n\"subListenDesc\" = \"訂閱服務監聽的 IP 地址（留空表示監聽所有 IP）\"\n\"subPort\" = \"監聽埠\"\n\"subPortDesc\" = \"訂閱服務監聽的埠號（必須是未使用的埠）\"\n\"subCertPath\" = \"公鑰路徑\"\n\"subCertPathDesc\" = \"訂閱服務使用的公鑰檔案路徑（以 '/' 開頭）\"\n\"subKeyPath\" = \"私鑰路徑\"\n\"subKeyPathDesc\" = \"訂閱服務使用的私鑰檔案路徑（以 '/' 開頭）\"\n\"subPath\" = \"URI 路徑\"\n\"subPathDesc\" = \"訂閱服務使用的 URI 路徑（以 '/' 開頭，以 '/' 結尾）\"\n\"subDomain\" = \"監聽域名\"\n\"subDomainDesc\" = \"訂閱服務監聽的域名（留空表示監聽所有域名和 IP）\"\n\"subUpdates\" = \"更新間隔\"\n\"subUpdatesDesc\" = \"客戶端應用中訂閱 URL 的更新間隔（單位：小時）\"\n\"subEncrypt\" = \"編碼\"\n\"subEncryptDesc\" = \"訂閱服務返回的內容將採用 Base64 編碼\"\n\"subShowInfo\" = \"顯示使用資訊\"\n\"subShowInfoDesc\" = \"客戶端應用中將顯示剩餘流量和日期資訊\"\n\"subURI\" = \"反向代理 URI\"\n\"subURIDesc\" = \"用於代理後面的訂閱 URL 的 URI 路徑\"\n\"externalTrafficInformEnable\" = \"外部交通通知\"\n\"externalTrafficInformEnableDesc\" = \"每次流量更新時通知外部 API\"\n\"externalTrafficInformURI\" = \"外部流量通知 URI\"\n\"externalTrafficInformURIDesc\" = \"流量更新將會傳送到此 URI\"\n\"fragment\" = \"分片\"\n\"fragmentDesc\" = \"啟用 TLS hello 資料包分片\"\n\"fragmentSett\" = \"設定\"\n\"noisesDesc\" = \"啟用 Noises.\"\n\"noisesSett\" = \"Noises 設定\"\n\"mux\" = \"多路複用器\"\n\"muxDesc\" = \"在已建立的資料流內傳輸多個獨立的資料流\"\n\"muxSett\" = \"複用器設定\"\n\"direct\" = \"直接連線\"\n\"directDesc\" = \"直接與特定國家的域或IP範圍建立連線\"\n\"notifications\" = \"通知\"\n\"certs\" = \"證書\"\n\"externalTraffic\" = \"外部流量\"\n\"dateAndTime\" = \"日期和時間\"\n\"proxyAndServer\" = \"代理和伺服器\"\n\"intervals\" = \"間隔\"\n\"information\" = \"資訊\"\n\"language\" = \"語言\"\n\"telegramBotLanguage\" = \"Telegram 機器人語言\"\n\n[pages.xray]\n\"title\" = \"Xray 配置\"\n\"save\" = \"儲存\"\n\"restart\" = \"重新啟動 Xray\"\n\"restartSuccess\" = \"Xray 已成功重新啟動\"\n\"stopSuccess\" = \"Xray 已成功停止\"\n\"restartError\" = \"重新啟動Xray時發生錯誤。\"\n\"stopError\" = \"停止Xray時發生錯誤。\"\n\"basicTemplate\" = \"基礎配置\"\n\"advancedTemplate\" = \"高階配置\"\n\"generalConfigs\" = \"常規配置\"\n\"generalConfigsDesc\" = \"這些選項將決定常規配置\"\n\"logConfigs\" = \"日誌\"\n\"logConfigsDesc\" = \"日誌可能會影響伺服器的效能，建議僅在需要時啟用\"\n\"blockConfigsDesc\" = \"這些選項將阻止使用者連線到特定協議和網站\"\n\"basicRouting\" = \"基本路由\"\n\"blockConnectionsConfigsDesc\" = \"這些選項將根據特定的請求國家阻止流量。\"\n\"directConnectionsConfigsDesc\" = \"直接連線確保特定的流量不會通過其他伺服器路由。\"\n\"blockips\" = \"阻止IP\"\n\"blockdomains\" = \"阻止域名\"\n\"directips\" = \"直接IP\"\n\"directdomains\" = \"直接域名\"\n\"ipv4Routing\" = \"IPv4 路由\"\n\"ipv4RoutingDesc\" = \"此選項將僅通過 IPv4 路由到目標域\"\n\"warpRouting\" = \"WARP 路由\"\n\"warpRoutingDesc\" = \"注意：在使用這些選項之前，請按照面板 GitHub 上的步驟在你的伺服器上以 socks5 代理模式安裝 WARP。WARP 將通過 Cloudflare 伺服器將流量路由到網站。\"\n\"Template\" = \"高階 Xray 配置模板\"\n\"TemplateDesc\" = \"最終的 Xray 配置檔案將基於此模板生成\"\n\"FreedomStrategy\" = \"Freedom 協議策略\"\n\"FreedomStrategyDesc\" = \"設定 Freedom 協議中網路的輸出策略\"\n\"RoutingStrategy\" = \"配置路由域策略\"\n\"RoutingStrategyDesc\" = \"設定 DNS 解析的整體路由策略\"\n\"outboundTestUrl\" = \"出站測試 URL\"\n\"outboundTestUrlDesc\" = \"測試出站連線時使用的 URL\"\n\"Torrent\" = \"遮蔽 BitTorrent 協議\"\n\"Inbounds\" = \"入站規則\"\n\"InboundsDesc\" = \"接受來自特定客戶端的流量\"\n\"Outbounds\" = \"出站規則\"\n\"Balancers\" = \"負載均衡\"\n\"OutboundsDesc\" = \"設定出站流量傳出方式\"\n\"Routings\" = \"路由規則\"\n\"RoutingsDesc\" = \"每條規則的優先順序都很重要\"\n\"completeTemplate\" = \"全部\"\n\"logLevel\" = \"日誌級別\"\n\"logLevelDesc\" = \"錯誤日誌的日誌級別，用於指示需要記錄的資訊\"\n\"accessLog\" = \"訪問日誌\"\n\"accessLogDesc\" = \"訪問日誌的檔案路徑。特殊值 'none' 禁用訪問日誌\"\n\"errorLog\" = \"錯誤日誌\"\n\"errorLogDesc\" = \"錯誤日誌的檔案路徑。特殊值 'none' 禁用錯誤日誌\"\n\"dnsLog\" = \"DNS 日誌\"\n\"dnsLogDesc\" = \"是否啟用 DNS 查詢日誌\"\n\"maskAddress\" = \"隱藏地址\"\n\"maskAddressDesc\" = \"IP 地址掩碼，啟用時會自動替換日誌中出現的 IP 地址。\"\n\"statistics\" = \"統計\"\n\"statsInboundUplink\" = \"入站上傳統計\"\n\"statsInboundUplinkDesc\" = \"啟用所有入站代理的上行流量統計收集。\"\n\"statsInboundDownlink\" = \"入站下載統計\"\n\"statsInboundDownlinkDesc\" = \"啟用所有入站代理的下行流量統計收集。\"\n\"statsOutboundUplink\" = \"出站上傳統計\"\n\"statsOutboundUplinkDesc\" = \"啟用所有出站代理的上行流量統計收集。\"\n\"statsOutboundDownlink\" = \"出站下載統計\"\n\"statsOutboundDownlinkDesc\" = \"啟用所有出站代理的下行流量統計收集。\"\n\n[pages.xray.rules]\n\"first\" = \"置頂\"\n\"last\" = \"置底\"\n\"up\" = \"向上\"\n\"down\" = \"向下\"\n\"source\" = \"來源\"\n\"dest\" = \"目的地址\"\n\"inbound\" = \"入站\"\n\"outbound\" = \"出站\"\n\"balancer\" = \"負載均衡\"\n\"info\" = \"資訊\"\n\"add\" = \"新增規則\"\n\"edit\" = \"編輯規則\"\n\"useComma\" = \"逗號分隔的項目\"\n\n[pages.xray.outbound]\n\"addOutbound\" = \"新增出站\"\n\"addReverse\" = \"新增反向\"\n\"editOutbound\" = \"編輯出站\"\n\"editReverse\" = \"編輯反向\"\n\"tag\" = \"標籤\"\n\"tagDesc\" = \"唯一標籤\"\n\"address\" = \"地址\"\n\"reverse\" = \"反向\"\n\"domain\" = \"域名\"\n\"type\" = \"類型\"\n\"bridge\" = \"Bridge\"\n\"portal\" = \"Portal\"\n\"link\" = \"連結\"\n\"intercon\" = \"互連\"\n\"settings\" = \"設定\"\n\"accountInfo\" = \"帳戶資訊\"\n\"outboundStatus\" = \"出站狀態\"\n\"sendThrough\" = \"傳送通過\"\n\"test\" = \"測試\"\n\"testResult\" = \"測試結果\"\n\"testing\" = \"正在測試連接...\"\n\"testSuccess\" = \"測試成功\"\n\"testFailed\" = \"測試失敗\"\n\"testError\" = \"測試出站失敗\"\n\n[pages.xray.balancer]\n\"addBalancer\" = \"新增負載均衡\"\n\"editBalancer\" = \"編輯負載均衡\"\n\"balancerStrategy\" = \"策略\"\n\"balancerSelectors\" = \"選擇器\"\n\"tag\" = \"標籤\"\n\"tagDesc\" = \"唯一標籤\"\n\"balancerDesc\" = \"無法同時使用 balancerTag 和 outboundTag。如果同時使用，則只有 outboundTag 會生效。\"\n\n[pages.xray.wireguard]\n\"secretKey\" = \"金鑰\"\n\"publicKey\" = \"公鑰\"\n\"allowedIPs\" = \"允許的 IP\"\n\"endpoint\" = \"端點\"\n\"psk\" = \"共享金鑰\"\n\"domainStrategy\" = \"域策略\"\n\n[pages.xray.tun]\n\"nameDesc\" = \"TUN 介面的名稱。預設值為 'xray0'\"\n\"mtuDesc\" = \"最大傳輸單元。資料包的最大大小。預設值為 1500\"\n\"userLevel\" = \"用戶級別\"\n\"userLevelDesc\" = \"通過此入站的所有連接都將使用此用戶級別。預設值為 0\"\n\n[pages.xray.dns]\n\"enable\" = \"啟用 DNS\"\n\"enableDesc\" = \"啟用內建 DNS 伺服器\"\n\"tag\" = \"DNS 入站標籤\"\n\"tagDesc\" = \"此標籤將在路由規則中可用作入站標籤\"\n\"clientIp\" = \"客戶端IP\"\n\"clientIpDesc\" = \"用於在DNS查詢期間通知伺服器指定的IP位置\"\n\"disableCache\" = \"禁用快取\"\n\"disableCacheDesc\" = \"禁用DNS快取\"\n\"disableFallback\" = \"禁用回退\"\n\"disableFallbackDesc\" = \"禁用回退DNS查詢\"\n\"disableFallbackIfMatch\" = \"匹配時禁用回退\"\n\"disableFallbackIfMatchDesc\" = \"當DNS伺服器的匹配域名列表命中時，禁用回退DNS查詢\"\n\"enableParallelQuery\" = \"啟用並行查詢\"\n\"enableParallelQueryDesc\" = \"啟用並行DNS查詢到多個伺服器以實現更快的解析\"\n\"strategy\" = \"查詢策略\"\n\"strategyDesc\" = \"解析域名的總體策略\"\n\"add\" = \"新增伺服器\"\n\"edit\" = \"編輯伺服器\"\n\"domains\" = \"域\"\n\"expectIPs\" = \"預期 IP\"\n\"unexpectIPs\" = \"意外IP\"\n\"useSystemHosts\" = \"使用系統Hosts\"\n\"useSystemHostsDesc\" = \"使用已安裝系統的hosts檔案\"\n\"usePreset\" = \"使用範本\"\n\"dnsPresetTitle\" = \"DNS範本\"\n\"dnsPresetFamily\" = \"家庭\"\n\n[pages.xray.fakedns]\n\"add\" = \"新增假 DNS\"\n\"edit\" = \"編輯假 DNS\"\n\"ipPool\" = \"IP 池子網\"\n\"poolSize\" = \"池大小\"\n\n[pages.settings.security]\n\"admin\" = \"管理員憑證\"\n\"twoFactor\" = \"雙重驗證\"\n\"twoFactorEnable\" = \"啟用2FA\"\n\"twoFactorEnableDesc\" = \"增加額外的驗證層以提高安全性。\"\n\"twoFactorModalSetTitle\" = \"啟用雙重認證\"\n\"twoFactorModalDeleteTitle\" = \"停用雙重認證\"\n\"twoFactorModalSteps\" = \"要設定雙重認證，請執行以下步驟：\"\n\"twoFactorModalFirstStep\" = \"1. 在認證應用程式中掃描此QR碼，或複製QR碼附近的令牌並貼到應用程式中\"\n\"twoFactorModalSecondStep\" = \"2. 輸入應用程式中的驗證碼\"\n\"twoFactorModalRemoveStep\" = \"輸入應用程式中的驗證碼以移除雙重認證。\"\n\"twoFactorModalChangeCredentialsTitle\" = \"更改憑證\"\n\"twoFactorModalChangeCredentialsStep\" = \"輸入應用程式中的代碼以更改管理員憑證。\"\n\"twoFactorModalSetSuccess\" = \"雙重身份驗證已成功建立\"\n\"twoFactorModalDeleteSuccess\" = \"雙重身份驗證已成功刪除\"\n\"twoFactorModalError\" = \"驗證碼錯誤\"\n\n[pages.settings.toasts]\n\"modifySettings\" = \"參數已更改。\"\n\"getSettings\" = \"取得參數時發生錯誤\"\n\"modifyUserError\" = \"變更管理員憑證時發生錯誤。\"\n\"modifyUser\" = \"您已成功變更管理員憑證。\"\n\"originalUserPassIncorrect\" = \"原使用者名稱或原密碼錯誤\"\n\"userPassMustBeNotEmpty\" = \"新使用者名稱和新密碼不能為空\"\n\"getOutboundTrafficError\" = \"取得出站流量錯誤\"\n\"resetOutboundTrafficError\" = \"重設出站流量錯誤\"\n\n[tgbot]\n\"keyboardClosed\" = \"❌ 自定義鍵盤已關閉！\"\n\"noResult\" = \"❗ 沒有結果！\"\n\"noQuery\" = \"❌ 未找到查詢！請再次使用該命令！\"\n\"wentWrong\" = \"❌ 出了點問題！\"\n\"noIpRecord\" = \"❗ 沒有IP記錄！\"\n\"noInbounds\" = \"❗ 未找到入站！\"\n\"unlimited\" = \"♾ 無限（重置）\"\n\"add\" = \"添加\"\n\"month\" = \"月\"\n\"months\" = \"月\"\n\"day\" = \"天\"\n\"days\" = \"天\"\n\"hours\" = \"小時\"\n\"minutes\" = \"分鐘\"\n\"unknown\" = \"未知\"\n\"inbounds\" = \"入站\"\n\"clients\" = \"客戶端\"\n\"offline\" = \"🔴 離線\"\n\"online\" = \"🟢 在線\"\n\n[tgbot.commands]\n\"unknown\" = \"❗ 未知命令\"\n\"pleaseChoose\" = \"👇 請選擇：\\r\\n\"\n\"help\" = \"🤖 歡迎使用本機器人！它旨在為您提供來自伺服器的特定資料，並允許您進行必要的修改。\\r\\n\\r\\n\"\n\"start\" = \"👋 你好，<i>{{ .Firstname }}</i>。\\r\\n\"\n\"welcome\" = \"🤖 歡迎來到 <b>{{ .Hostname }}</b> 管理機器人。\\r\\n\"\n\"status\" = \"✅ 機器人正常執行！\"\n\"usage\" = \"❗ 請輸入要搜尋的文字！\"\n\"getID\" = \"🆔 您的 ID 為：<code>{{ .ID }}</code>\"\n\"helpAdminCommands\" = \"要重新啟動 Xray Core：\\r\\n<code>/restart</code>\\r\\n\\r\\n要搜尋客戶電子郵件：\\r\\n<code>/usage [電子郵件]</code>\\r\\n\\r\\n要搜尋入站（帶有客戶統計資料）：\\r\\n<code>/inbound [備註]</code>\\r\\n\\r\\nTelegram聊天ID：\\r\\n<code>/id</code>\"\n\"helpClientCommands\" = \"要搜尋統計資料，請使用以下命令：\\r\\n<code>/usage [電子郵件]</code>\\r\\n\\r\\nTelegram聊天ID：\\r\\n<code>/id</code>\"\n\"restartUsage\" = \"\\r\\n\\r\\n<code>/restart</code>\"\n\"restartSuccess\" = \"✅ 操作成功!\"\n\"restartFailed\" = \"❗ 操作錯誤。\\r\\n\\r\\n<code>錯誤: {{ .Error }}</code>.\"\n\"xrayNotRunning\" = \"❗ Xray Core 未運行。\"\n\"startDesc\" = \"顯示主選單\"\n\"helpDesc\" = \"機器人幫助\"\n\"statusDesc\" = \"檢查機器人狀態\"\n\"idDesc\" = \"顯示您的 Telegram ID\"\n\n[tgbot.messages]\n\"cpuThreshold\" = \"🔴 CPU 使用率為 {{ .Percent }}%，超過閾值 {{ .Threshold }}%\"\n\"selectUserFailed\" = \"❌ 使用者選擇錯誤！\"\n\"userSaved\" = \"✅ 電報使用者已儲存。\"\n\"loginSuccess\" = \"✅ 成功登入到面板。\\r\\n\"\n\"loginFailed\" = \"❗️ 面板登入失敗。\\r\\n\"\n\"2faFailed\" = \"2FA 失敗\"\n\"report\" = \"🕰 定時報告：{{ .RunTime }}\\r\\n\"\n\"datetime\" = \"⏰ 日期時間：{{ .DateTime }}\\r\\n\"\n\"hostname\" = \"💻 主機名：{{ .Hostname }}\\r\\n\"\n\"version\" = \"🚀 X-UI 版本：{{ .Version }}\\r\\n\"\n\"xrayVersion\" = \"📡 Xray 版本: {{ .XrayVersion }}\\r\\n\"\n\"ipv6\" = \"🌐 IPv6：{{ .IPv6 }}\\r\\n\"\n\"ipv4\" = \"🌐 IPv4：{{ .IPv4 }}\\r\\n\"\n\"ip\" = \"🌐 IP：{{ .IP }}\\r\\n\"\n\"ips\" = \"🔢 IP 地址：\\r\\n{{ .IPs }}\\r\\n\"\n\"serverUpTime\" = \"⏳ 伺服器執行時間：{{ .UpTime }} {{ .Unit }}\\r\\n\"\n\"serverLoad\" = \"📈 伺服器負載：{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\\r\\n\"\n\"serverMemory\" = \"📋 伺服器記憶體：{{ .Current }}/{{ .Total }}\\r\\n\"\n\"tcpCount\" = \"🔹 TCP 連線數：{{ .Count }}\\r\\n\"\n\"udpCount\" = \"🔸 UDP 連線數：{{ .Count }}\\r\\n\"\n\"traffic\" = \"🚦 流量：{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\\r\\n\"\n\"xrayStatus\" = \"ℹ️ Xray 狀態：{{ .State }}\\r\\n\"\n\"username\" = \"👤 使用者名稱：{{ .Username }}\\r\\n\"\n\"password\" = \"👤 密碼: {{ .Password }}\\r\\n\"\n\"time\" = \"⏰ 時間：{{ .Time }}\\r\\n\"\n\"inbound\" = \"📍 入站：{{ .Remark }}\\r\\n\"\n\"port\" = \"🔌 埠：{{ .Port }}\\r\\n\"\n\"expire\" = \"📅 過期日期：{{ .Time }}\\r\\n\"\n\"expireIn\" = \"📅 剩餘時間：{{ .Time }}\\r\\n\"\n\"active\" = \"💡 啟用：{{ .Enable }}\\r\\n\"\n\"enabled\" = \"🚨 已啟用：{{ .Enable }}\\r\\n\"\n\"online\" = \"🌐 連線狀態：{{ .Status }}\\r\\n\"\n\"lastOnline\" = \"🔙 上次上線: {{ .Time }}\\r\\n\"\n\"email\" = \"📧 郵箱：{{ .Email }}\\r\\n\"\n\"upload\" = \"🔼 上傳↑：{{ .Upload }}\\r\\n\"\n\"download\" = \"🔽 下載↓：{{ .Download }}\\r\\n\"\n\"total\" = \"📊 總計：{{ .UpDown }} / {{ .Total }}\\r\\n\"\n\"TGUser\" = \"👤 電報使用者：{{ .TelegramID }}\\r\\n\"\n\"exhaustedMsg\" = \"🚨 耗盡的 {{ .Type }}：\\r\\n\"\n\"exhaustedCount\" = \"🚨 耗盡的 {{ .Type }} 數量：\\r\\n\"\n\"onlinesCount\" = \"🌐 線上客戶：{{ .Count }}\\r\\n\"\n\"disabled\" = \"🛑 禁用：{{ .Disabled }}\\r\\n\"\n\"depleteSoon\" = \"🔜 即將耗盡：{{ .Deplete }}\\r\\n\\r\\n\"\n\"backupTime\" = \"🗄 備份時間：{{ .Time }}\\r\\n\"\n\"refreshedOn\" = \"\\r\\n📋🔄 重新整理時間：{{ .Time }}\\r\\n\\r\\n\"\n\"yes\" = \"✅ 是的\"\n\"no\" = \"❌ 沒有\"\n\"received_id\" = \"🔑📥 ID 已更新。\"\n\"received_password\" = \"🔑📥 密碼已更新。\"\n\"received_email\" = \"📧📥 電子郵件已更新。\"\n\"received_comment\" = \"💬📥 評論已更新。\"\n\"id_prompt\" = \"🔑 預設 ID: {{ .ClientId }}\\n\\n請輸入您的 ID。\"\n\"pass_prompt\" = \"🔑 預設密碼: {{ .ClientPassword }}\\n\\n請輸入您的密碼。\"\n\"email_prompt\" = \"📧 預設電子郵件: {{ .ClientEmail }}\\n\\n請輸入您的電子郵件。\"\n\"comment_prompt\" = \"💬 預設評論: {{ .ClientComment }}\\n\\n請輸入您的評論。\"\n\"inbound_client_data_id\" = \"🔄 入站: {{ .InboundRemark }}\\n\\n🔑 ID: {{ .ClientId }}\\n📧 電子郵件: {{ .ClientEmail }}\\n📊 流量: {{ .ClientTraffic }}\\n📅 到期日: {{ .ClientExp }}\\n🌐 IP 限制: {{ .IpLimit }}\\n💬 備註: {{ .ClientComment }}\\n\\n你現在可以將客戶加入入站了！\"\n\"inbound_client_data_pass\" = \"🔄 入站: {{ .InboundRemark }}\\n\\n🔑 密碼: {{ .ClientPass }}\\n📧 電子郵件: {{ .ClientEmail }}\\n📊 流量: {{ .ClientTraffic }}\\n📅 到期日: {{ .ClientExp }}\\n🌐 IP 限制: {{ .IpLimit }}\\n💬 備註: {{ .ClientComment }}\\n\\n你現在可以將客戶加入入站了！\"\n\"cancel\" = \"❌ 程序已取消！\\n\\n您可以隨時使用 /start 重新開始。 🔄\"\n\"error_add_client\" = \"⚠️ 錯誤:\\n\\n {{ .error }}\"\n\"using_default_value\" = \"好的，我會使用預設值。 😊\"\n\"incorrect_input\" = \"您的輸入無效。\\n短語應連續輸入，不能有空格。\\n正確示例: aaaaaa\\n錯誤示例: aaa aaa 🚫\"\n\"AreYouSure\" = \"你確定嗎？🤔\"\n\"SuccessResetTraffic\" = \"📧 電子郵件: {{ .ClientEmail }}\\n🏁 結果: ✅ 成功\"\n\"FailedResetTraffic\" = \"📧 電子郵件: {{ .ClientEmail }}\\n🏁 結果: ❌ 失敗 \\n\\n🛠️ 錯誤: [ {{ .ErrorMessage }} ]\"\n\"FinishProcess\" = \"🔚 所有客戶的流量重置已完成。\"\n\n[tgbot.buttons]\n\"closeKeyboard\" = \"❌ 關閉鍵盤\"\n\"cancel\" = \"❌ 取消\"\n\"cancelReset\" = \"❌ 取消重置\"\n\"cancelIpLimit\" = \"❌ 取消 IP 限制\"\n\"confirmResetTraffic\" = \"✅ 確認重置流量？\"\n\"confirmClearIps\" = \"✅ 確認清除 IP？\"\n\"confirmRemoveTGUser\" = \"✅ 確認移除 Telegram 使用者？\"\n\"confirmToggle\" = \"✅ 確認啟用/禁用使用者？\"\n\"dbBackup\" = \"獲取資料庫備份\"\n\"serverUsage\" = \"伺服器使用情況\"\n\"getInbounds\" = \"獲取入站資訊\"\n\"depleteSoon\" = \"即將耗盡\"\n\"clientUsage\" = \"獲取使用情況\"\n\"onlines\" = \"線上客戶端\"\n\"commands\" = \"命令\"\n\"refresh\" = \"🔄 重新整理\"\n\"clearIPs\" = \"❌ 清除 IP\"\n\"removeTGUser\" = \"❌ 移除 Telegram 使用者\"\n\"selectTGUser\" = \"👤 選擇 Telegram 使用者\"\n\"selectOneTGUser\" = \"👤 選擇一個 Telegram 使用者：\"\n\"resetTraffic\" = \"📈 重置流量\"\n\"resetExpire\" = \"📅 更改到期日期\"\n\"ipLog\" = \"🔢 IP 日誌\"\n\"ipLimit\" = \"🔢 IP 限制\"\n\"setTGUser\" = \"👤 設定 Telegram 使用者\"\n\"toggle\" = \"🔘 啟用/禁用\"\n\"custom\" = \"🔢 風俗\"\n\"confirmNumber\" = \"✅ 確認: {{ .Num }}\"\n\"confirmNumberAdd\" = \"✅ 確認新增：{{ .Num }}\"\n\"limitTraffic\" = \"🚧 流量限制\"\n\"getBanLogs\" = \"禁止日誌\"\n\"allClients\" = \"所有客戶\"\n\"addClient\" = \"新增客戶\"\n\"submitDisable\" = \"以停用方式送出 ☑️\"\n\"submitEnable\" = \"以啟用方式送出 ✅\"\n\"use_default\" = \"🏷️ 使用預設值\"\n\"change_id\" = \"⚙️🔑 ID\"\n\"change_password\" = \"⚙️🔑 密碼\"\n\"change_email\" = \"⚙️📧 電子郵件\"\n\"change_comment\" = \"⚙️💬 評論\"\n\"ResetAllTraffics\" = \"重設所有流量\"\n\"SortedTrafficUsageReport\" = \"排序過的流量使用報告\"\n\n[tgbot.answers]\n\"successfulOperation\" = \"✅ 成功！\"\n\"errorOperation\" = \"❗ 操作錯誤。\"\n\"getInboundsFailed\" = \"❌ 獲取入站資訊失敗。\"\n\"getClientsFailed\" = \"❌ 獲取客戶失敗。\"\n\"canceled\" = \"❌ {{ .Email }}：操作已取消。\"\n\"clientRefreshSuccess\" = \"✅ {{ .Email }}：客戶端重新整理成功。\"\n\"IpRefreshSuccess\" = \"✅ {{ .Email }}：IP 重新整理成功。\"\n\"TGIdRefreshSuccess\" = \"✅ {{ .Email }}：客戶端的 Telegram 使用者重新整理成功。\"\n\"resetTrafficSuccess\" = \"✅ {{ .Email }}：流量已重置成功。\"\n\"setTrafficLimitSuccess\" = \"✅ {{ .Email }}: 流量限制儲存成功。\"\n\"expireResetSuccess\" = \"✅ {{ .Email }}：過期天數已重置成功。\"\n\"resetIpSuccess\" = \"✅ {{ .Email }}：成功儲存 IP 限制數量為 {{ .Count }}。\"\n\"clearIpSuccess\" = \"✅ {{ .Email }}：IP 已成功清除。\"\n\"getIpLog\" = \"✅ {{ .Email }}：獲取 IP 日誌。\"\n\"getUserInfo\" = \"✅ {{ .Email }}：獲取 Telegram 使用者資訊。\"\n\"removedTGUserSuccess\" = \"✅ {{ .Email }}：Telegram 使用者已成功移除。\"\n\"enableSuccess\" = \"✅ {{ .Email }}：已成功啟用。\"\n\"disableSuccess\" = \"✅ {{ .Email }}：已成功禁用。\"\n\"askToAddUserId\" = \"未找到您的配置！\\r\\n請向管理員詢問，在您的配置中使用您的 Telegram 使用者 ChatID。\\r\\n\\r\\n您的使用者 ChatID：<code>{{ .TgUserID }}</code>\"\n\"chooseClient\" = \"為入站 {{ .Inbound }} 選擇一個客戶\"\n\"chooseInbound\" = \"選擇一個入站\"\n"
  },
  {
    "path": "web/web.go",
    "content": "// Package web provides the main web server implementation for the 3x-ui panel,\n// including HTTP/HTTPS serving, routing, templates, and background job scheduling.\npackage web\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"embed\"\n\t\"html/template\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/controller\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/job\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/locale\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/middleware\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/network\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/service\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/websocket\"\n\n\t\"github.com/gin-contrib/gzip\"\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-contrib/sessions/cookie\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/robfig/cron/v3\"\n)\n\n//go:embed assets\nvar assetsFS embed.FS\n\n//go:embed html/*\nvar htmlFS embed.FS\n\n//go:embed translation/*\nvar i18nFS embed.FS\n\nvar startTime = time.Now()\n\ntype wrapAssetsFS struct {\n\tembed.FS\n}\n\nfunc (f *wrapAssetsFS) Open(name string) (fs.File, error) {\n\tfile, err := f.FS.Open(\"assets/\" + name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &wrapAssetsFile{\n\t\tFile: file,\n\t}, nil\n}\n\ntype wrapAssetsFile struct {\n\tfs.File\n}\n\nfunc (f *wrapAssetsFile) Stat() (fs.FileInfo, error) {\n\tinfo, err := f.File.Stat()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &wrapAssetsFileInfo{\n\t\tFileInfo: info,\n\t}, nil\n}\n\ntype wrapAssetsFileInfo struct {\n\tfs.FileInfo\n}\n\nfunc (f *wrapAssetsFileInfo) ModTime() time.Time {\n\treturn startTime\n}\n\n// EmbeddedHTML returns the embedded HTML templates filesystem for reuse by other servers.\nfunc EmbeddedHTML() embed.FS {\n\treturn htmlFS\n}\n\n// EmbeddedAssets returns the embedded assets filesystem for reuse by other servers.\nfunc EmbeddedAssets() embed.FS {\n\treturn assetsFS\n}\n\n// Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs.\ntype Server struct {\n\thttpServer *http.Server\n\tlistener   net.Listener\n\n\tindex *controller.IndexController\n\tpanel *controller.XUIController\n\tapi   *controller.APIController\n\tws    *controller.WebSocketController\n\n\txrayService    service.XrayService\n\tsettingService service.SettingService\n\ttgbotService   service.Tgbot\n\n\twsHub *websocket.Hub\n\n\tcron *cron.Cron\n\n\tctx    context.Context\n\tcancel context.CancelFunc\n}\n\n// NewServer creates a new web server instance with a cancellable context.\nfunc NewServer() *Server {\n\tctx, cancel := context.WithCancel(context.Background())\n\treturn &Server{\n\t\tctx:    ctx,\n\t\tcancel: cancel,\n\t}\n}\n\n// getHtmlFiles walks the local `web/html` directory and returns a list of\n// template file paths. Used only in debug/development mode.\nfunc (s *Server) getHtmlFiles() ([]string, error) {\n\tfiles := make([]string, 0)\n\tdir, _ := os.Getwd()\n\terr := fs.WalkDir(os.DirFS(dir), \"web/html\", func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tfiles = append(files, path)\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn files, nil\n}\n\n// getHtmlTemplate parses embedded HTML templates from the bundled `htmlFS`\n// using the provided template function map and returns the resulting\n// template set for production usage.\nfunc (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) {\n\tt := template.New(\"\").Funcs(funcMap)\n\terr := fs.WalkDir(htmlFS, \"html\", func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\tnewT, err := t.ParseFS(htmlFS, path+\"/*.html\")\n\t\t\tif err != nil {\n\t\t\t\t// ignore\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tt = newT\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn t, nil\n}\n\n// initRouter initializes Gin, registers middleware, templates, static\n// assets, controllers and returns the configured engine.\nfunc (s *Server) initRouter() (*gin.Engine, error) {\n\tif config.IsDebug() {\n\t\tgin.SetMode(gin.DebugMode)\n\t} else {\n\t\tgin.DefaultWriter = io.Discard\n\t\tgin.DefaultErrorWriter = io.Discard\n\t\tgin.SetMode(gin.ReleaseMode)\n\t}\n\n\tengine := gin.Default()\n\n\twebDomain, err := s.settingService.GetWebDomain()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif webDomain != \"\" {\n\t\tengine.Use(middleware.DomainValidatorMiddleware(webDomain))\n\t}\n\n\tsecret, err := s.settingService.GetSecret()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbasePath, err := s.settingService.GetBasePath()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tengine.Use(gzip.Gzip(gzip.DefaultCompression))\n\tassetsBasePath := basePath + \"assets/\"\n\n\tstore := cookie.NewStore(secret)\n\t// Configure default session cookie options, including expiration (MaxAge)\n\tif sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil {\n\t\tstore.Options(sessions.Options{\n\t\t\tPath:     \"/\",\n\t\t\tMaxAge:   sessionMaxAge * 60, // minutes -> seconds\n\t\t\tHttpOnly: true,\n\t\t\tSameSite: http.SameSiteLaxMode,\n\t\t})\n\t}\n\tengine.Use(sessions.Sessions(\"3x-ui\", store))\n\tengine.Use(func(c *gin.Context) {\n\t\tc.Set(\"base_path\", basePath)\n\t})\n\tengine.Use(func(c *gin.Context) {\n\t\turi := c.Request.RequestURI\n\t\tif strings.HasPrefix(uri, assetsBasePath) {\n\t\t\tc.Header(\"Cache-Control\", \"max-age=31536000\")\n\t\t}\n\t})\n\n\t// init i18n\n\terr = locale.InitLocalizer(i18nFS, &s.settingService)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Apply locale middleware for i18n\n\ti18nWebFunc := func(key string, params ...string) string {\n\t\treturn locale.I18n(locale.Web, key, params...)\n\t}\n\t// Register template functions before loading templates\n\tfuncMap := template.FuncMap{\n\t\t\"i18n\": i18nWebFunc,\n\t}\n\tengine.SetFuncMap(funcMap)\n\tengine.Use(locale.LocalizerMiddleware())\n\n\t// set static files and template\n\tif config.IsDebug() {\n\t\t// for development\n\t\tfiles, err := s.getHtmlFiles()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// Use the registered func map with the loaded templates\n\t\tengine.LoadHTMLFiles(files...)\n\t\tengine.StaticFS(basePath+\"assets\", http.FS(os.DirFS(\"web/assets\")))\n\t} else {\n\t\t// for production\n\t\ttemplate, err := s.getHtmlTemplate(funcMap)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tengine.SetHTMLTemplate(template)\n\t\tengine.StaticFS(basePath+\"assets\", http.FS(&wrapAssetsFS{FS: assetsFS}))\n\t}\n\n\t// Apply the redirect middleware (`/xui` to `/panel`)\n\tengine.Use(middleware.RedirectMiddleware(basePath))\n\n\tg := engine.Group(basePath)\n\n\ts.index = controller.NewIndexController(g)\n\ts.panel = controller.NewXUIController(g)\n\ts.api = controller.NewAPIController(g)\n\n\t// Initialize WebSocket hub\n\ts.wsHub = websocket.NewHub()\n\tgo s.wsHub.Run()\n\n\t// Initialize WebSocket controller\n\ts.ws = controller.NewWebSocketController(s.wsHub)\n\t// Register WebSocket route with basePath (g already has basePath prefix)\n\tg.GET(\"/ws\", s.ws.HandleWebSocket)\n\n\t// Chrome DevTools endpoint for debugging web apps\n\tengine.GET(\"/.well-known/appspecific/com.chrome.devtools.json\", func(c *gin.Context) {\n\t\tc.JSON(http.StatusOK, gin.H{})\n\t})\n\n\t// Add a catch-all route to handle undefined paths and return 404\n\tengine.NoRoute(func(c *gin.Context) {\n\t\tc.AbortWithStatus(http.StatusNotFound)\n\t})\n\n\treturn engine, nil\n}\n\n// startTask schedules background jobs (Xray checks, traffic jobs, cron\n// jobs) which the panel relies on for periodic maintenance and monitoring.\nfunc (s *Server) startTask() {\n\terr := s.xrayService.RestartXray(true)\n\tif err != nil {\n\t\tlogger.Warning(\"start xray failed:\", err)\n\t}\n\t// Check whether xray is running every second\n\ts.cron.AddJob(\"@every 1s\", job.NewCheckXrayRunningJob())\n\n\t// Check if xray needs to be restarted every 30 seconds\n\ts.cron.AddFunc(\"@every 30s\", func() {\n\t\tif s.xrayService.IsNeedRestartAndSetFalse() {\n\t\t\terr := s.xrayService.RestartXray(false)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(\"restart xray failed:\", err)\n\t\t\t}\n\t\t}\n\t})\n\n\tgo func() {\n\t\ttime.Sleep(time.Second * 5)\n\t\t// Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray\n\t\ts.cron.AddJob(\"@every 10s\", job.NewXrayTrafficJob())\n\t}()\n\n\t// check client ips from log file every 10 sec\n\ts.cron.AddJob(\"@every 10s\", job.NewCheckClientIpJob())\n\n\t// check client ips from log file every day\n\ts.cron.AddJob(\"@daily\", job.NewClearLogsJob())\n\n\t// Inbound traffic reset jobs\n\t// Run once a day, midnight\n\ts.cron.AddJob(\"@daily\", job.NewPeriodicTrafficResetJob(\"daily\"))\n\t// Run once a week, midnight between Sat/Sun\n\ts.cron.AddJob(\"@weekly\", job.NewPeriodicTrafficResetJob(\"weekly\"))\n\t// Run once a month, midnight, first of month\n\ts.cron.AddJob(\"@monthly\", job.NewPeriodicTrafficResetJob(\"monthly\"))\n\n\t// LDAP sync scheduling\n\tif ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled {\n\t\truntime, err := s.settingService.GetLdapSyncCron()\n\t\tif err != nil || runtime == \"\" {\n\t\t\truntime = \"@every 1m\"\n\t\t}\n\t\tj := job.NewLdapSyncJob()\n\t\t// job has zero-value services with method receivers that read settings on demand\n\t\ts.cron.AddJob(runtime, j)\n\t}\n\n\t// Make a traffic condition every day, 8:30\n\tvar entry cron.EntryID\n\tisTgbotenabled, err := s.settingService.GetTgbotEnabled()\n\tif (err == nil) && (isTgbotenabled) {\n\t\truntime, err := s.settingService.GetTgbotRuntime()\n\t\tif err != nil || runtime == \"\" {\n\t\t\tlogger.Errorf(\"Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default\", err, runtime)\n\t\t\truntime = \"@daily\"\n\t\t}\n\t\tlogger.Infof(\"Tg notify enabled,run at %s\", runtime)\n\t\t_, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob())\n\t\tif err != nil {\n\t\t\tlogger.Warning(\"Add NewStatsNotifyJob error\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// check for Telegram bot callback query hash storage reset\n\t\ts.cron.AddJob(\"@every 2m\", job.NewCheckHashStorageJob())\n\n\t\t// Check CPU load and alarm to TgBot if threshold passes\n\t\tcpuThreshold, err := s.settingService.GetTgCpu()\n\t\tif (err == nil) && (cpuThreshold > 0) {\n\t\t\ts.cron.AddJob(\"@every 10s\", job.NewCheckCpuJob())\n\t\t}\n\t} else {\n\t\ts.cron.Remove(entry)\n\t}\n}\n\n// Start initializes and starts the web server with configured settings, routes, and background jobs.\nfunc (s *Server) Start() (err error) {\n\t// This is an anonymous function, no function name\n\tdefer func() {\n\t\tif err != nil {\n\t\t\ts.Stop()\n\t\t}\n\t}()\n\n\tloc, err := s.settingService.GetTimeLocation()\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds())\n\ts.cron.Start()\n\n\tengine, err := s.initRouter()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcertFile, err := s.settingService.GetCertFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\tkeyFile, err := s.settingService.GetKeyFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\tlisten, err := s.settingService.GetListen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tport, err := s.settingService.GetPort()\n\tif err != nil {\n\t\treturn err\n\t}\n\tlistenAddr := net.JoinHostPort(listen, strconv.Itoa(port))\n\tlistener, err := net.Listen(\"tcp\", listenAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif certFile != \"\" || keyFile != \"\" {\n\t\tcert, err := tls.LoadX509KeyPair(certFile, keyFile)\n\t\tif err == nil {\n\t\t\tc := &tls.Config{\n\t\t\t\tCertificates: []tls.Certificate{cert},\n\t\t\t}\n\t\t\tlistener = network.NewAutoHttpsListener(listener)\n\t\t\tlistener = tls.NewListener(listener, c)\n\t\t\tlogger.Info(\"Web server running HTTPS on\", listener.Addr())\n\t\t} else {\n\t\t\tlogger.Error(\"Error loading certificates:\", err)\n\t\t\tlogger.Info(\"Web server running HTTP on\", listener.Addr())\n\t\t}\n\t} else {\n\t\tlogger.Info(\"Web server running HTTP on\", listener.Addr())\n\t}\n\ts.listener = listener\n\n\ts.httpServer = &http.Server{\n\t\tHandler: engine,\n\t}\n\n\tgo func() {\n\t\ts.httpServer.Serve(listener)\n\t}()\n\n\ts.startTask()\n\n\tisTgbotenabled, err := s.settingService.GetTgbotEnabled()\n\tif (err == nil) && (isTgbotenabled) {\n\t\ttgBot := s.tgbotService.NewTgbot()\n\t\ttgBot.Start(i18nFS)\n\t}\n\n\treturn nil\n}\n\n// Stop gracefully shuts down the web server, stops Xray, cron jobs, and Telegram bot.\nfunc (s *Server) Stop() error {\n\ts.cancel()\n\ts.xrayService.StopXray()\n\tif s.cron != nil {\n\t\ts.cron.Stop()\n\t}\n\tif s.tgbotService.IsRunning() {\n\t\ts.tgbotService.Stop()\n\t}\n\t// Gracefully stop WebSocket hub\n\tif s.wsHub != nil {\n\t\ts.wsHub.Stop()\n\t}\n\tvar err1 error\n\tvar err2 error\n\tif s.httpServer != nil {\n\t\terr1 = s.httpServer.Shutdown(s.ctx)\n\t}\n\tif s.listener != nil {\n\t\terr2 = s.listener.Close()\n\t}\n\treturn common.Combine(err1, err2)\n}\n\n// GetCtx returns the server's context for cancellation and deadline management.\nfunc (s *Server) GetCtx() context.Context {\n\treturn s.ctx\n}\n\n// GetCron returns the server's cron scheduler instance.\nfunc (s *Server) GetCron() *cron.Cron {\n\treturn s.cron\n}\n\n// GetWSHub returns the WebSocket hub instance.\nfunc (s *Server) GetWSHub() any {\n\treturn s.wsHub\n}\n\nfunc (s *Server) RestartXray() error {\n\treturn s.xrayService.RestartXray(true)\n}\n"
  },
  {
    "path": "web/websocket/hub.go",
    "content": "// Package websocket provides WebSocket hub for real-time updates and notifications.\npackage websocket\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n)\n\n// MessageType represents the type of WebSocket message\ntype MessageType string\n\nconst (\n\tMessageTypeStatus       MessageType = \"status\"       // Server status update\n\tMessageTypeTraffic      MessageType = \"traffic\"      // Traffic statistics update\n\tMessageTypeInbounds     MessageType = \"inbounds\"     // Inbounds list update\n\tMessageTypeNotification MessageType = \"notification\" // System notification\n\tMessageTypeXrayState    MessageType = \"xray_state\"   // Xray state change\n\tMessageTypeOutbounds    MessageType = \"outbounds\"    // Outbounds list update\n)\n\n// Message represents a WebSocket message\ntype Message struct {\n\tType    MessageType `json:\"type\"`\n\tPayload any         `json:\"payload\"`\n\tTime    int64       `json:\"time\"`\n}\n\n// Client represents a WebSocket client connection\ntype Client struct {\n\tID     string\n\tSend   chan []byte\n\tHub    *Hub\n\tTopics map[MessageType]bool // Subscribed topics\n}\n\n// Hub maintains the set of active clients and broadcasts messages to them\ntype Hub struct {\n\t// Registered clients\n\tclients map[*Client]bool\n\n\t// Inbound messages from clients\n\tbroadcast chan []byte\n\n\t// Register requests from clients\n\tregister chan *Client\n\n\t// Unregister requests from clients\n\tunregister chan *Client\n\n\t// Mutex for thread-safe operations\n\tmu sync.RWMutex\n\n\t// Context for graceful shutdown\n\tctx    context.Context\n\tcancel context.CancelFunc\n\n\t// Worker pool for parallel broadcasting\n\tworkerPoolSize int\n\tbroadcastWg    sync.WaitGroup\n}\n\n// NewHub creates a new WebSocket hub\nfunc NewHub() *Hub {\n\tctx, cancel := context.WithCancel(context.Background())\n\n\t// Calculate optimal worker pool size (CPU cores * 2, but max 100)\n\tworkerPoolSize := runtime.NumCPU() * 2\n\tif workerPoolSize > 100 {\n\t\tworkerPoolSize = 100\n\t}\n\tif workerPoolSize < 10 {\n\t\tworkerPoolSize = 10\n\t}\n\n\treturn &Hub{\n\t\tclients:        make(map[*Client]bool),\n\t\tbroadcast:      make(chan []byte, 2048), // Increased from 256 to 2048 for high load\n\t\tregister:       make(chan *Client, 100), // Buffered channel for fast registration\n\t\tunregister:     make(chan *Client, 100), // Buffered channel for fast unregistration\n\t\tctx:            ctx,\n\t\tcancel:         cancel,\n\t\tworkerPoolSize: workerPoolSize,\n\t}\n}\n\n// Run starts the hub's main loop\nfunc (h *Hub) Run() {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlogger.Error(\"WebSocket hub panic recovered:\", r)\n\t\t\t// Restart the hub loop\n\t\t\tgo h.Run()\n\t\t}\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase <-h.ctx.Done():\n\t\t\t// Graceful shutdown: close all clients\n\t\t\th.mu.Lock()\n\t\t\tfor client := range h.clients {\n\t\t\t\t// Safely close channel (avoid double close panic)\n\t\t\t\tselect {\n\t\t\t\tcase _, stillOpen := <-client.Send:\n\t\t\t\t\tif stillOpen {\n\t\t\t\t\t\tclose(client.Send)\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tclose(client.Send)\n\t\t\t\t}\n\t\t\t}\n\t\t\th.clients = make(map[*Client]bool)\n\t\t\th.mu.Unlock()\n\t\t\t// Wait for all broadcast workers to finish\n\t\t\th.broadcastWg.Wait()\n\t\t\tlogger.Info(\"WebSocket hub stopped gracefully\")\n\t\t\treturn\n\n\t\tcase client := <-h.register:\n\t\t\tif client == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\th.mu.Lock()\n\t\t\th.clients[client] = true\n\t\t\tcount := len(h.clients)\n\t\t\th.mu.Unlock()\n\t\t\tlogger.Debugf(\"WebSocket client connected: %s (total: %d)\", client.ID, count)\n\n\t\tcase client := <-h.unregister:\n\t\t\tif client == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\th.mu.Lock()\n\t\t\tif _, ok := h.clients[client]; ok {\n\t\t\t\tdelete(h.clients, client)\n\t\t\t\t// Safely close channel (avoid double close panic)\n\t\t\t\t// Check if channel is already closed by trying to read from it\n\t\t\t\tselect {\n\t\t\t\tcase _, stillOpen := <-client.Send:\n\t\t\t\t\tif stillOpen {\n\t\t\t\t\t\t// Channel was open and had data, now it's empty, safe to close\n\t\t\t\t\t\tclose(client.Send)\n\t\t\t\t\t}\n\t\t\t\t\t// If stillOpen is false, channel was already closed, do nothing\n\t\t\t\tdefault:\n\t\t\t\t\t// Channel is empty and open, safe to close\n\t\t\t\t\tclose(client.Send)\n\t\t\t\t}\n\t\t\t}\n\t\t\tcount := len(h.clients)\n\t\t\th.mu.Unlock()\n\t\t\tlogger.Debugf(\"WebSocket client disconnected: %s (total: %d)\", client.ID, count)\n\n\t\tcase message := <-h.broadcast:\n\t\t\tif message == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Optimization: quickly copy client list and release lock\n\t\t\th.mu.RLock()\n\t\t\tclientCount := len(h.clients)\n\t\t\tif clientCount == 0 {\n\t\t\t\th.mu.RUnlock()\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Pre-allocate memory for client list\n\t\t\tclients := make([]*Client, 0, clientCount)\n\t\t\tfor client := range h.clients {\n\t\t\t\tclients = append(clients, client)\n\t\t\t}\n\t\t\th.mu.RUnlock()\n\n\t\t\t// Parallel broadcast using worker pool\n\t\t\th.broadcastParallel(clients, message)\n\t\t}\n\t}\n}\n\n// broadcastParallel sends message to all clients in parallel for maximum performance\nfunc (h *Hub) broadcastParallel(clients []*Client, message []byte) {\n\tif len(clients) == 0 {\n\t\treturn\n\t}\n\n\t// For small number of clients, use simple parallel sending\n\tif len(clients) < h.workerPoolSize {\n\t\tvar wg sync.WaitGroup\n\t\tfor _, client := range clients {\n\t\t\twg.Add(1)\n\t\t\tgo func(c *Client) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\t// Channel may be closed, safely ignore\n\t\t\t\t\t\tlogger.Debugf(\"WebSocket broadcast panic recovered for client %s: %v\", c.ID, r)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\tselect {\n\t\t\t\tcase c.Send <- message:\n\t\t\t\tdefault:\n\t\t\t\t\t// Client's send buffer is full, disconnect\n\t\t\t\t\tlogger.Debugf(\"WebSocket client %s send buffer full, disconnecting\", c.ID)\n\t\t\t\t\th.Unregister(c)\n\t\t\t\t}\n\t\t\t}(client)\n\t\t}\n\t\twg.Wait()\n\t\treturn\n\t}\n\n\t// For large number of clients, use worker pool for optimal performance\n\tclientChan := make(chan *Client, len(clients))\n\tfor _, client := range clients {\n\t\tclientChan <- client\n\t}\n\tclose(clientChan)\n\n\t// Start workers for parallel processing\n\th.broadcastWg.Add(h.workerPoolSize)\n\tfor i := 0; i < h.workerPoolSize; i++ {\n\t\tgo func() {\n\t\t\tdefer h.broadcastWg.Done()\n\t\t\tfor client := range clientChan {\n\t\t\t\tfunc() {\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\t\t// Channel may be closed, safely ignore\n\t\t\t\t\t\t\tlogger.Debugf(\"WebSocket broadcast panic recovered for client %s: %v\", client.ID, r)\n\t\t\t\t\t\t}\n\t\t\t\t\t}()\n\t\t\t\t\tselect {\n\t\t\t\t\tcase client.Send <- message:\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t// Client's send buffer is full, disconnect\n\t\t\t\t\t\tlogger.Debugf(\"WebSocket client %s send buffer full, disconnecting\", client.ID)\n\t\t\t\t\t\th.Unregister(client)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t}()\n\t}\n\n\t// Wait for all workers to finish\n\th.broadcastWg.Wait()\n}\n\n// Broadcast sends a message to all connected clients\nfunc (h *Hub) Broadcast(messageType MessageType, payload any) {\n\tif h == nil {\n\t\treturn\n\t}\n\tif payload == nil {\n\t\tlogger.Warning(\"Attempted to broadcast nil payload\")\n\t\treturn\n\t}\n\n\tmsg := Message{\n\t\tType:    messageType,\n\t\tPayload: payload,\n\t\tTime:    getCurrentTimestamp(),\n\t}\n\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\tlogger.Error(\"Failed to marshal WebSocket message:\", err)\n\t\treturn\n\t}\n\n\t// Limit message size to prevent memory issues\n\tconst maxMessageSize = 1024 * 1024 // 1MB\n\tif len(data) > maxMessageSize {\n\t\tlogger.Warningf(\"WebSocket message too large: %d bytes, dropping\", len(data))\n\t\treturn\n\t}\n\n\t// Non-blocking send with timeout to prevent delays\n\tselect {\n\tcase h.broadcast <- data:\n\tcase <-time.After(100 * time.Millisecond):\n\t\tlogger.Warning(\"WebSocket broadcast channel is full, dropping message\")\n\tcase <-h.ctx.Done():\n\t\t// Hub is shutting down\n\t}\n}\n\n// BroadcastToTopic sends a message only to clients subscribed to the specific topic\nfunc (h *Hub) BroadcastToTopic(messageType MessageType, payload any) {\n\tif h == nil {\n\t\treturn\n\t}\n\tif payload == nil {\n\t\tlogger.Warning(\"Attempted to broadcast nil payload to topic\")\n\t\treturn\n\t}\n\n\tmsg := Message{\n\t\tType:    messageType,\n\t\tPayload: payload,\n\t\tTime:    getCurrentTimestamp(),\n\t}\n\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\tlogger.Error(\"Failed to marshal WebSocket message:\", err)\n\t\treturn\n\t}\n\n\t// Limit message size to prevent memory issues\n\tconst maxMessageSize = 1024 * 1024 // 1MB\n\tif len(data) > maxMessageSize {\n\t\tlogger.Warningf(\"WebSocket message too large: %d bytes, dropping\", len(data))\n\t\treturn\n\t}\n\n\th.mu.RLock()\n\t// Filter clients by topics and quickly release lock\n\tsubscribedClients := make([]*Client, 0)\n\tfor client := range h.clients {\n\t\tif len(client.Topics) == 0 || client.Topics[messageType] {\n\t\t\tsubscribedClients = append(subscribedClients, client)\n\t\t}\n\t}\n\th.mu.RUnlock()\n\n\t// Parallel send to subscribed clients\n\tif len(subscribedClients) > 0 {\n\t\th.broadcastParallel(subscribedClients, data)\n\t}\n}\n\n// GetClientCount returns the number of connected clients\nfunc (h *Hub) GetClientCount() int {\n\th.mu.RLock()\n\tdefer h.mu.RUnlock()\n\treturn len(h.clients)\n}\n\n// Register registers a new client with the hub\nfunc (h *Hub) Register(client *Client) {\n\tif h == nil || client == nil {\n\t\treturn\n\t}\n\tselect {\n\tcase h.register <- client:\n\tcase <-h.ctx.Done():\n\t\t// Hub is shutting down\n\t}\n}\n\n// Unregister unregisters a client from the hub\nfunc (h *Hub) Unregister(client *Client) {\n\tif h == nil || client == nil {\n\t\treturn\n\t}\n\tselect {\n\tcase h.unregister <- client:\n\tcase <-h.ctx.Done():\n\t\t// Hub is shutting down\n\t}\n}\n\n// Stop gracefully stops the hub and closes all connections\nfunc (h *Hub) Stop() {\n\tif h == nil {\n\t\treturn\n\t}\n\tif h.cancel != nil {\n\t\th.cancel()\n\t}\n}\n\n// getCurrentTimestamp returns current Unix timestamp in milliseconds\nfunc getCurrentTimestamp() int64 {\n\treturn time.Now().UnixMilli()\n}\n"
  },
  {
    "path": "web/websocket/notifier.go",
    "content": "// Package websocket provides WebSocket hub for real-time updates and notifications.\npackage websocket\n\nimport (\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/web/global\"\n)\n\n// GetHub returns the global WebSocket hub instance\nfunc GetHub() *Hub {\n\twebServer := global.GetWebServer()\n\tif webServer == nil {\n\t\treturn nil\n\t}\n\thub := webServer.GetWSHub()\n\tif hub == nil {\n\t\treturn nil\n\t}\n\twsHub, ok := hub.(*Hub)\n\tif !ok {\n\t\tlogger.Warning(\"WebSocket hub type assertion failed\")\n\t\treturn nil\n\t}\n\treturn wsHub\n}\n\n// BroadcastStatus broadcasts server status update to all connected clients\nfunc BroadcastStatus(status any) {\n\thub := GetHub()\n\tif hub != nil {\n\t\thub.Broadcast(MessageTypeStatus, status)\n\t}\n}\n\n// BroadcastTraffic broadcasts traffic statistics update to all connected clients\nfunc BroadcastTraffic(traffic any) {\n\thub := GetHub()\n\tif hub != nil {\n\t\thub.Broadcast(MessageTypeTraffic, traffic)\n\t}\n}\n\n// BroadcastInbounds broadcasts inbounds list update to all connected clients\nfunc BroadcastInbounds(inbounds any) {\n\thub := GetHub()\n\tif hub != nil {\n\t\thub.Broadcast(MessageTypeInbounds, inbounds)\n\t}\n}\n\n// BroadcastOutbounds broadcasts outbounds list update to all connected clients\nfunc BroadcastOutbounds(outbounds any) {\n\thub := GetHub()\n\tif hub != nil {\n\t\thub.Broadcast(MessageTypeOutbounds, outbounds)\n\t}\n}\n\n// BroadcastNotification broadcasts a system notification to all connected clients\nfunc BroadcastNotification(title, message, level string) {\n\thub := GetHub()\n\tif hub != nil {\n\t\tnotification := map[string]string{\n\t\t\t\"title\":   title,\n\t\t\t\"message\": message,\n\t\t\t\"level\":   level, // info, warning, error, success\n\t\t}\n\t\thub.Broadcast(MessageTypeNotification, notification)\n\t}\n}\n\n// BroadcastXrayState broadcasts Xray state change to all connected clients\nfunc BroadcastXrayState(state string, errorMsg string) {\n\thub := GetHub()\n\tif hub != nil {\n\t\tstateUpdate := map[string]string{\n\t\t\t\"state\":    state,\n\t\t\t\"errorMsg\": errorMsg,\n\t\t}\n\t\thub.Broadcast(MessageTypeXrayState, stateUpdate)\n\t}\n}\n"
  },
  {
    "path": "windows_files/readme.txt",
    "content": "you can't install fail2ban on windows\nwe don't have bash menu for windows\nif you forgot your password you need to check your database with https://sqlitebrowser.org/\nthe app need to be open all the time\n\ndefault setting:\nhttp://localhost:2053/\nuser: admin\npass: admin\nport: 2053\n\n\nopenssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt\n"
  },
  {
    "path": "x-ui.rc",
    "content": "#!/sbin/openrc-run\n\ncommand=\"/usr/local/x-ui/x-ui\"\ncommand_background=true\npidfile=\"/run/x-ui.pid\"\ndescription=\"x-ui Service\"\nprocname=\"x-ui\"\ndepend() {\n    need net\n}\nstart_pre(){\n    cd /usr/local/x-ui\n}\nreload() {\n  ebegin \"Reloading ${RC_SVCNAME}\"\n  kill -USR1  $pidfile\n  eend $?\n}"
  },
  {
    "path": "x-ui.service.arch",
    "content": "[Unit]\nDescription=x-ui Service\nAfter=network.target\nWants=network.target\n\n[Service]\nEnvironmentFile=-/etc/conf.d/x-ui\nEnvironment=\"XRAY_VMESS_AEAD_FORCED=false\"\nType=simple\nWorkingDirectory=/usr/lib/x-ui/\nExecStart=/usr/lib/x-ui/x-ui\nExecReload=kill -USR1 $MAINPID\nRestart=on-failure\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "x-ui.service.debian",
    "content": "[Unit]\nDescription=x-ui Service\nAfter=network.target\nWants=network.target\n\n[Service]\nEnvironmentFile=-/etc/default/x-ui\nEnvironment=\"XRAY_VMESS_AEAD_FORCED=false\"\nType=simple\nWorkingDirectory=/usr/local/x-ui/\nExecStart=/usr/local/x-ui/x-ui\nExecReload=kill -USR1 $MAINPID\nRestart=on-failure\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "x-ui.service.rhel",
    "content": "[Unit]\nDescription=x-ui Service\nAfter=network.target\nWants=network.target\n\n[Service]\nEnvironmentFile=-/etc/sysconfig/x-ui\nEnvironment=\"XRAY_VMESS_AEAD_FORCED=false\"\nType=simple\nWorkingDirectory=/usr/local/x-ui/\nExecStart=/usr/local/x-ui/x-ui\nExecReload=kill -USR1 $MAINPID\nRestart=on-failure\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "x-ui.sh",
    "content": "#!/bin/bash\n\nred='\\033[0;31m'\ngreen='\\033[0;32m'\nblue='\\033[0;34m'\nyellow='\\033[0;33m'\nplain='\\033[0m'\n\n#Add some basic function here\nfunction LOGD() {\n    echo -e \"${yellow}[DEG] $* ${plain}\"\n}\n\nfunction LOGE() {\n    echo -e \"${red}[ERR] $* ${plain}\"\n}\n\nfunction LOGI() {\n    echo -e \"${green}[INF] $* ${plain}\"\n}\n\n# Port helpers: detect listener and owning process (best effort)\nis_port_in_use() {\n    local port=\"$1\"\n    if command -v ss >/dev/null 2>&1; then\n        ss -ltn 2>/dev/null | awk -v p=\":${port}$\" '$4 ~ p {exit 0} END {exit 1}'\n        return\n    fi\n    if command -v netstat >/dev/null 2>&1; then\n        netstat -lnt 2>/dev/null | awk -v p=\":${port} \" '$4 ~ p {exit 0} END {exit 1}'\n        return\n    fi\n    if command -v lsof >/dev/null 2>&1; then\n        lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0\n    fi\n    return 1\n}\n\n# Simple helpers for domain/IP validation\nis_ipv4() {\n    [[ \"$1\" =~ ^([0-9]{1,3}\\.){3}[0-9]{1,3}$ ]] && return 0 || return 1\n}\nis_ipv6() {\n    [[ \"$1\" =~ : ]] && return 0 || return 1\n}\nis_ip() {\n    is_ipv4 \"$1\" || is_ipv6 \"$1\"\n}\nis_domain() {\n    [[ \"$1\" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1\n}\n\n# check root\n[[ $EUID -ne 0 ]] && LOGE \"ERROR: You must be root to run this script! \\n\" && exit 1\n\n# Check OS and set release variable\nif [[ -f /etc/os-release ]]; then\n    source /etc/os-release\n    release=$ID\nelif [[ -f /usr/lib/os-release ]]; then\n    source /usr/lib/os-release\n    release=$ID\nelse\n    echo \"Failed to check the system OS, please contact the author!\" >&2\n    exit 1\nfi\necho \"The OS release is: $release\"\n\nos_version=\"\"\nos_version=$(grep \"^VERSION_ID\" /etc/os-release | cut -d '=' -f2 | tr -d '\"' | tr -d '.')\n\n# Declare Variables\nxui_folder=\"${XUI_MAIN_FOLDER:=/usr/local/x-ui}\"\nxui_service=\"${XUI_SERVICE:=/etc/systemd/system}\"\nlog_folder=\"${XUI_LOG_FOLDER:=/var/log/x-ui}\"\nmkdir -p \"${log_folder}\"\niplimit_log_path=\"${log_folder}/3xipl.log\"\niplimit_banned_log_path=\"${log_folder}/3xipl-banned.log\"\n\nconfirm() {\n    if [[ $# > 1 ]]; then\n        echo && read -rp \"$1 [Default $2]: \" temp\n        if [[ \"${temp}\" == \"\" ]]; then\n            temp=$2\n        fi\n    else\n        read -rp \"$1 [y/n]: \" temp\n    fi\n    if [[ \"${temp}\" == \"y\" || \"${temp}\" == \"Y\" ]]; then\n        return 0\n    else\n        return 1\n    fi\n}\n\nconfirm_restart() {\n    confirm \"Restart the panel, Attention: Restarting the panel will also restart xray\" \"y\"\n    if [[ $? == 0 ]]; then\n        restart\n    else\n        show_menu\n    fi\n}\n\nbefore_show_menu() {\n    echo && echo -n -e \"${yellow}Press enter to return to the main menu: ${plain}\" && read -r temp\n    show_menu\n}\n\ninstall() {\n    bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh)\n    if [[ $? == 0 ]]; then\n        if [[ $# == 0 ]]; then\n            start\n        else\n            start 0\n        fi\n    fi\n}\n\nupdate() {\n    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\"\n    if [[ $? != 0 ]]; then\n        LOGE \"Cancelled\"\n        if [[ $# == 0 ]]; then\n            before_show_menu\n        fi\n        return 0\n    fi\n    bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh)\n    if [[ $? == 0 ]]; then\n        LOGI \"Update is complete, Panel has automatically restarted \"\n        before_show_menu\n    fi\n}\n\nupdate_menu() {\n    echo -e \"${yellow}Updating Menu${plain}\"\n    confirm \"This function will update the menu to the latest changes.\" \"y\"\n    if [[ $? != 0 ]]; then\n        LOGE \"Cancelled\"\n        if [[ $# == 0 ]]; then\n            before_show_menu\n        fi\n        return 0\n    fi\n\n    curl -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh\n    chmod +x ${xui_folder}/x-ui.sh\n    chmod +x /usr/bin/x-ui\n\n    if [[ $? == 0 ]]; then\n        echo -e \"${green}Update successful. The panel has automatically restarted.${plain}\"\n        exit 0\n    else\n        echo -e \"${red}Failed to update the menu.${plain}\"\n        return 1\n    fi\n}\n\nlegacy_version() {\n    echo -n \"Enter the panel version (like 2.4.0):\"\n    read -r tag_version\n\n    if [ -z \"$tag_version\" ]; then\n        echo \"Panel version cannot be empty. Exiting.\"\n        exit 1\n    fi\n    # Use the entered panel version in the download link\n    install_command=\"bash <(curl -Ls \"https://raw.githubusercontent.com/mhsanaei/3x-ui/v$tag_version/install.sh\") v$tag_version\"\n\n    echo \"Downloading and installing panel version $tag_version...\"\n    eval $install_command\n}\n\n# Function to handle the deletion of the script file\ndelete_script() {\n    rm \"$0\" # Remove the script file itself\n    exit 1\n}\n\nuninstall() {\n    confirm \"Are you sure you want to uninstall the panel? xray will also uninstalled!\" \"n\"\n    if [[ $? != 0 ]]; then\n        if [[ $# == 0 ]]; then\n            show_menu\n        fi\n        return 0\n    fi\n\n    if [[ $release == \"alpine\" ]]; then\n        rc-service x-ui stop\n        rc-update del x-ui\n        rm /etc/init.d/x-ui -f\n    else\n        systemctl stop x-ui\n        systemctl disable x-ui\n        rm ${xui_service}/x-ui.service -f\n        systemctl daemon-reload\n        systemctl reset-failed\n    fi\n\n    rm /etc/x-ui/ -rf\n    rm ${xui_folder}/ -rf\n\n    echo \"\"\n    echo -e \"Uninstalled Successfully.\\n\"\n    echo \"If you need to install this panel again, you can use below command:\"\n    echo -e \"${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}\"\n    echo \"\"\n    # Trap the SIGTERM signal\n    trap delete_script SIGTERM\n    delete_script\n}\n\nreset_user() {\n    confirm \"Are you sure to reset the username and password of the panel?\" \"n\"\n    if [[ $? != 0 ]]; then\n        if [[ $# == 0 ]]; then\n            show_menu\n        fi\n        return 0\n    fi\n    \n    read -rp \"Please set the login username [default is a random username]: \" config_account\n    [[ -z $config_account ]] && config_account=$(gen_random_string 10)\n    read -rp \"Please set the login password [default is a random password]: \" config_password\n    [[ -z $config_password ]] && config_password=$(gen_random_string 18)\n\n    read -rp \"Do you want to disable currently configured two-factor authentication? (y/n): \" twoFactorConfirm\n    if [[ $twoFactorConfirm != \"y\" && $twoFactorConfirm != \"Y\" ]]; then\n        ${xui_folder}/x-ui setting -username \"${config_account}\" -password \"${config_password}\" -resetTwoFactor false >/dev/null 2>&1\n    else\n        ${xui_folder}/x-ui setting -username \"${config_account}\" -password \"${config_password}\" -resetTwoFactor true >/dev/null 2>&1\n        echo -e \"Two factor authentication has been disabled.\"\n    fi\n    \n    echo -e \"Panel login username has been reset to: ${green} ${config_account} ${plain}\"\n    echo -e \"Panel login password has been reset to: ${green} ${config_password} ${plain}\"\n    echo -e \"${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}\"\n    confirm_restart\n}\n\ngen_random_string() {\n    local length=\"$1\"\n    local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w \"$length\" | head -n 1)\n    echo \"$random_string\"\n}\n\nreset_webbasepath() {\n    echo -e \"${yellow}Resetting Web Base Path${plain}\"\n\n    read -rp \"Are you sure you want to reset the web base path? (y/n): \" confirm\n    if [[ $confirm != \"y\" && $confirm != \"Y\" ]]; then\n        echo -e \"${yellow}Operation canceled.${plain}\"\n        return\n    fi\n\n    config_webBasePath=$(gen_random_string 18)\n\n    # Apply the new web base path setting\n    ${xui_folder}/x-ui setting -webBasePath \"${config_webBasePath}\" >/dev/null 2>&1\n\n    echo -e \"Web base path has been reset to: ${green}${config_webBasePath}${plain}\"\n    echo -e \"${green}Please use the new web base path to access the panel.${plain}\"\n    restart\n}\n\nreset_config() {\n    confirm \"Are you sure you want to reset all panel settings, Account data will not be lost, Username and password will not change\" \"n\"\n    if [[ $? != 0 ]]; then\n        if [[ $# == 0 ]]; then\n            show_menu\n        fi\n        return 0\n    fi\n    ${xui_folder}/x-ui setting -reset\n    echo -e \"All panel settings have been reset to default.\"\n    restart\n}\n\ncheck_config() {\n    local info=$(${xui_folder}/x-ui setting -show true)\n    if [[ $? != 0 ]]; then\n        LOGE \"get current settings error, please check logs\"\n        show_menu\n        return\n    fi\n    LOGI \"${info}\"\n\n    local existing_webBasePath=$(echo \"$info\" | grep -Eo 'webBasePath: .+' | awk '{print $2}')\n    local existing_port=$(echo \"$info\" | grep -Eo 'port: .+' | awk '{print $2}')\n    local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')\n    local server_ip=$(curl -s --max-time 3 https://api.ipify.org)\n    if [ -z \"$server_ip\" ]; then\n        server_ip=$(curl -s --max-time 3 https://4.ident.me)\n    fi\n\n    if [[ -n \"$existing_cert\" ]]; then\n        local domain=$(basename \"$(dirname \"$existing_cert\")\")\n\n        if [[ \"$domain\" =~ ^[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$ ]]; then\n            echo -e \"${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}\"\n        else\n            echo -e \"${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}\"\n        fi\n    else\n        echo -e \"${red}⚠ WARNING: No SSL certificate configured!${plain}\"\n        echo -e \"${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}\"\n        read -rp \"Generate SSL certificate for IP now? [y/N]: \" gen_ssl\n        if [[ \"$gen_ssl\" == \"y\" || \"$gen_ssl\" == \"Y\" ]]; then\n            stop >/dev/null 2>&1\n            ssl_cert_issue_for_ip\n            if [[ $? -eq 0 ]]; then\n                echo -e \"${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}\"\n                # ssl_cert_issue_for_ip already restarts the panel, but ensure it's running\n                start >/dev/null 2>&1\n            else\n                LOGE \"IP certificate setup failed.\"\n                echo -e \"${yellow}You can try again via option 19 (SSL Certificate Management).${plain}\"\n                start >/dev/null 2>&1\n            fi\n        else\n            echo -e \"${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}\"\n            echo -e \"${yellow}For security, please configure SSL certificate using option 19 (SSL Certificate Management)${plain}\"\n        fi\n    fi\n}\n\nset_port() {\n    echo -n \"Enter port number[1-65535]: \"\n    read -r port\n    if [[ -z \"${port}\" ]]; then\n        LOGD \"Cancelled\"\n        before_show_menu\n    else\n        ${xui_folder}/x-ui setting -port ${port}\n        echo -e \"The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel\"\n        confirm_restart\n    fi\n}\n\nstart() {\n    check_status\n    if [[ $? == 0 ]]; then\n        echo \"\"\n        LOGI \"Panel is running, No need to start again, If you need to restart, please select restart\"\n    else\n        if [[ $release == \"alpine\" ]]; then\n            rc-service x-ui start\n        else\n            systemctl start x-ui\n        fi\n        sleep 2\n        check_status\n        if [[ $? == 0 ]]; then\n            LOGI \"x-ui Started Successfully\"\n        else\n            LOGE \"panel Failed to start, Probably because it takes longer than two seconds to start, Please check the log information later\"\n        fi\n    fi\n\n    if [[ $# == 0 ]]; then\n        before_show_menu\n    fi\n}\n\nstop() {\n    check_status\n    if [[ $? == 1 ]]; then\n        echo \"\"\n        LOGI \"Panel stopped, No need to stop again!\"\n    else\n        if [[ $release == \"alpine\" ]]; then\n            rc-service x-ui stop\n        else\n            systemctl stop x-ui\n        fi\n        sleep 2\n        check_status\n        if [[ $? == 1 ]]; then\n            LOGI \"x-ui and xray stopped successfully\"\n        else\n            LOGE \"Panel stop failed, Probably because the stop time exceeds two seconds, Please check the log information later\"\n        fi\n    fi\n\n    if [[ $# == 0 ]]; then\n        before_show_menu\n    fi\n}\n\nrestart() {\n    if [[ $release == \"alpine\" ]]; then\n        rc-service x-ui restart\n    else\n        systemctl restart x-ui\n    fi\n    sleep 2\n    check_status\n    if [[ $? == 0 ]]; then\n        LOGI \"x-ui and xray Restarted successfully\"\n    else\n        LOGE \"Panel restart failed, Probably because it takes longer than two seconds to start, Please check the log information later\"\n    fi\n    if [[ $# == 0 ]]; then\n        before_show_menu\n    fi\n}\n\nrestart_xray() {\n    systemctl reload x-ui\n    LOGI \"xray-core Restart signal sent successfully, Please check the log information to confirm whether xray restarted successfully\"\n    sleep 2\n    show_xray_status\n    if [[ $# == 0 ]]; then\n        before_show_menu\n    fi\n}\n\nstatus() {\n    if [[ $release == \"alpine\" ]]; then\n        rc-service x-ui status\n    else\n        systemctl status x-ui -l\n    fi\n    if [[ $# == 0 ]]; then\n        before_show_menu\n    fi\n}\n\nenable() {\n    if [[ $release == \"alpine\" ]]; then\n        rc-update add x-ui default\n    else\n        systemctl enable x-ui\n    fi\n    if [[ $? == 0 ]]; then\n        LOGI \"x-ui Set to boot automatically on startup successfully\"\n    else\n        LOGE \"x-ui Failed to set Autostart\"\n    fi\n\n    if [[ $# == 0 ]]; then\n        before_show_menu\n    fi\n}\n\ndisable() {\n    if [[ $release == \"alpine\" ]]; then\n        rc-update del x-ui\n    else\n        systemctl disable x-ui\n    fi\n    if [[ $? == 0 ]]; then\n        LOGI \"x-ui Autostart Cancelled successfully\"\n    else\n        LOGE \"x-ui Failed to cancel autostart\"\n    fi\n\n    if [[ $# == 0 ]]; then\n        before_show_menu\n    fi\n}\n\nshow_log() {\n    if [[ $release == \"alpine\" ]]; then\n        echo -e \"${green}\\t1.${plain} Debug Log\"\n        echo -e \"${green}\\t0.${plain} Back to Main Menu\"\n        read -rp \"Choose an option: \" choice\n\n        case \"$choice\" in\n        0)\n            show_menu\n            ;;\n        1)\n            grep -F 'x-ui[' /var/log/messages\n            if [[ $# == 0 ]]; then\n                before_show_menu\n            fi\n            ;;\n        *)\n            echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n            show_log\n            ;;\n        esac\n    else\n        echo -e \"${green}\\t1.${plain} Debug Log\"\n        echo -e \"${green}\\t2.${plain} Clear All logs\"\n        echo -e \"${green}\\t0.${plain} Back to Main Menu\"\n        read -rp \"Choose an option: \" choice\n\n        case \"$choice\" in\n        0)\n            show_menu\n            ;;\n        1)\n            journalctl -u x-ui -e --no-pager -f -p debug\n            if [[ $# == 0 ]]; then\n                before_show_menu\n            fi\n            ;;\n        2)\n            sudo journalctl --rotate\n            sudo journalctl --vacuum-time=1s\n            echo \"All Logs cleared.\"\n            restart\n            ;;\n        *)\n            echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n            show_log\n            ;;\n        esac\n    fi\n}\n\nbbr_menu() {\n    echo -e \"${green}\\t1.${plain} Enable BBR\"\n    echo -e \"${green}\\t2.${plain} Disable BBR\"\n    echo -e \"${green}\\t0.${plain} Back to Main Menu\"\n    read -rp \"Choose an option: \" choice\n    case \"$choice\" in\n    0)\n        show_menu\n        ;;\n    1)\n        enable_bbr\n        bbr_menu\n        ;;\n    2)\n        disable_bbr\n        bbr_menu\n        ;;\n    *)\n        echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n        bbr_menu\n        ;;\n    esac\n}\n\ndisable_bbr() {\n\n    if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != \"bbr\" ]] || [[ ! $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then\n        echo -e \"${yellow}BBR is not currently enabled.${plain}\"\n        before_show_menu\n    fi\n\n    if [ -f \"/etc/sysctl.d/99-bbr-x-ui.conf\" ]; then\n        old_settings=$(head -1 /etc/sysctl.d/99-bbr-x-ui.conf | tr -d '#')\n        sysctl -w net.core.default_qdisc=\"${old_settings%:*}\"\n        sysctl -w net.ipv4.tcp_congestion_control=\"${old_settings#*:}\"\n        rm /etc/sysctl.d/99-bbr-x-ui.conf\n        sysctl --system\n    else\n        # Replace BBR with CUBIC configurations\n        if [ -f \"/etc/sysctl.conf\" ]; then\n            sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf\n            sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf\n            sysctl -p\n        fi\n    fi\n\n    if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != \"bbr\" ]]; then\n        echo -e \"${green}BBR has been replaced with CUBIC successfully.${plain}\"\n    else\n        echo -e \"${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}\"\n    fi\n}\n\nenable_bbr() {\n    if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == \"bbr\" ]] && [[ $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then\n        echo -e \"${green}BBR is already enabled!${plain}\"\n        before_show_menu\n    fi\n\n    # Enable BBR\n    if [ -d \"/etc/sysctl.d/\" ]; then\n        {\n            echo \"#$(sysctl -n net.core.default_qdisc):$(sysctl -n net.ipv4.tcp_congestion_control)\"\n            echo \"net.core.default_qdisc = fq\"\n            echo \"net.ipv4.tcp_congestion_control = bbr\"\n        } > \"/etc/sysctl.d/99-bbr-x-ui.conf\"\n        if [ -f \"/etc/sysctl.conf\" ]; then\n            # Backup old settings from sysctl.conf, if any\n            sed -i 's/^net.core.default_qdisc/# &/'          /etc/sysctl.conf\n            sed -i 's/^net.ipv4.tcp_congestion_control/# &/' /etc/sysctl.conf\n        fi\n        sysctl --system\n    else\n        sed -i '/net.core.default_qdisc/d' /etc/sysctl.conf\n        sed -i '/net.ipv4.tcp_congestion_control/d' /etc/sysctl.conf\n        echo \"net.core.default_qdisc=fq\" | tee -a /etc/sysctl.conf\n        echo \"net.ipv4.tcp_congestion_control=bbr\" | tee -a /etc/sysctl.conf\n        sysctl -p\n    fi\n\n    # Verify that BBR is enabled\n    if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == \"bbr\" ]]; then\n        echo -e \"${green}BBR has been enabled successfully.${plain}\"\n    else\n        echo -e \"${red}Failed to enable BBR. Please check your system configuration.${plain}\"\n    fi\n}\n\nupdate_shell() {\n    curl -fLRo /usr/bin/x-ui -z /usr/bin/x-ui https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh\n    if [[ $? != 0 ]]; then\n        echo \"\"\n        LOGE \"Failed to download script, Please check whether the machine can connect Github\"\n        before_show_menu\n    else\n        chmod +x /usr/bin/x-ui\n        LOGI \"Upgrade script succeeded, Please rerun the script\"\n        before_show_menu\n    fi\n}\n\n# 0: running, 1: not running, 2: not installed\ncheck_status() {\n    if [[ $release == \"alpine\" ]]; then\n        if [[ ! -f /etc/init.d/x-ui ]]; then\n            return 2\n        fi\n        if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then\n            return 0\n        else\n            return 1\n        fi\n    else\n        if [[ ! -f ${xui_service}/x-ui.service ]]; then\n            return 2\n        fi\n        temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d \"(\" -f2 | cut -d \")\" -f1)\n        if [[ \"${temp}\" == \"running\" ]]; then\n            return 0\n        else\n            return 1\n        fi\n    fi\n}\n\ncheck_enabled() {\n    if [[ $release == \"alpine\" ]]; then\n        if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then\n            return 0\n        else\n            return 1\n        fi\n    else\n        temp=$(systemctl is-enabled x-ui)\n        if [[ \"${temp}\" == \"enabled\" ]]; then\n            return 0\n        else\n            return 1\n        fi\n    fi\n}\n\ncheck_uninstall() {\n    check_status\n    if [[ $? != 2 ]]; then\n        echo \"\"\n        LOGE \"Panel installed, Please do not reinstall\"\n        if [[ $# == 0 ]]; then\n            before_show_menu\n        fi\n        return 1\n    else\n        return 0\n    fi\n}\n\ncheck_install() {\n    check_status\n    if [[ $? == 2 ]]; then\n        echo \"\"\n        LOGE \"Please install the panel first\"\n        if [[ $# == 0 ]]; then\n            before_show_menu\n        fi\n        return 1\n    else\n        return 0\n    fi\n}\n\nshow_status() {\n    check_status\n    case $? in\n    0)\n        echo -e \"Panel state: ${green}Running${plain}\"\n        show_enable_status\n        ;;\n    1)\n        echo -e \"Panel state: ${yellow}Not Running${plain}\"\n        show_enable_status\n        ;;\n    2)\n        echo -e \"Panel state: ${red}Not Installed${plain}\"\n        ;;\n    esac\n    show_xray_status\n}\n\nshow_enable_status() {\n    check_enabled\n    if [[ $? == 0 ]]; then\n        echo -e \"Start automatically: ${green}Yes${plain}\"\n    else\n        echo -e \"Start automatically: ${red}No${plain}\"\n    fi\n}\n\ncheck_xray_status() {\n    count=$(ps -ef | grep \"xray-linux\" | grep -v \"grep\" | wc -l)\n    if [[ count -ne 0 ]]; then\n        return 0\n    else\n        return 1\n    fi\n}\n\nshow_xray_status() {\n    check_xray_status\n    if [[ $? == 0 ]]; then\n        echo -e \"xray state: ${green}Running${plain}\"\n    else\n        echo -e \"xray state: ${red}Not Running${plain}\"\n    fi\n}\n\nfirewall_menu() {\n    echo -e \"${green}\\t1.${plain} ${green}Install${plain} Firewall\"\n    echo -e \"${green}\\t2.${plain} Port List [numbered]\"\n    echo -e \"${green}\\t3.${plain} ${green}Open${plain} Ports\"\n    echo -e \"${green}\\t4.${plain} ${red}Delete${plain} Ports from List\"\n    echo -e \"${green}\\t5.${plain} ${green}Enable${plain} Firewall\"\n    echo -e \"${green}\\t6.${plain} ${red}Disable${plain} Firewall\"\n    echo -e \"${green}\\t7.${plain} Firewall Status\"\n    echo -e \"${green}\\t0.${plain} Back to Main Menu\"\n    read -rp \"Choose an option: \" choice\n    case \"$choice\" in\n    0)\n        show_menu\n        ;;\n    1)\n        install_firewall\n        firewall_menu\n        ;;\n    2)\n        ufw status numbered\n        firewall_menu\n        ;;\n    3)\n        open_ports\n        firewall_menu\n        ;;\n    4)\n        delete_ports\n        firewall_menu\n        ;;\n    5)\n        ufw enable\n        firewall_menu\n        ;;\n    6)\n        ufw disable\n        firewall_menu\n        ;;\n    7)\n        ufw status verbose\n        firewall_menu\n        ;;\n    *)\n        echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n        firewall_menu\n        ;;\n    esac\n}\n\ninstall_firewall() {\n    if ! command -v ufw &>/dev/null; then\n        echo \"ufw firewall is not installed. Installing now...\"\n        apt-get update\n        apt-get install -y ufw\n    else\n        echo \"ufw firewall is already installed\"\n    fi\n\n    # Check if the firewall is inactive\n    if ufw status | grep -q \"Status: active\"; then\n        echo \"Firewall is already active\"\n    else\n        echo \"Activating firewall...\"\n        # Open the necessary ports\n        ufw allow ssh\n        ufw allow http\n        ufw allow https\n        ufw allow 2053/tcp #webPort\n        ufw allow 2096/tcp #subport\n\n        # Enable the firewall\n        ufw --force enable\n    fi\n}\n\nopen_ports() {\n    # Prompt the user to enter the ports they want to open\n    read -rp \"Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): \" ports\n\n    # Check if the input is valid\n    if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then\n        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\n        exit 1\n    fi\n\n    # Open the specified ports using ufw\n    IFS=',' read -ra PORT_LIST <<<\"$ports\"\n    for port in \"${PORT_LIST[@]}\"; do\n        if [[ $port == *-* ]]; then\n            # Split the range into start and end ports\n            start_port=$(echo $port | cut -d'-' -f1)\n            end_port=$(echo $port | cut -d'-' -f2)\n            # Open the port range\n            ufw allow $start_port:$end_port/tcp\n            ufw allow $start_port:$end_port/udp\n        else\n            # Open the single port\n            ufw allow \"$port\"\n        fi\n    done\n\n    # Confirm that the ports are opened\n    echo \"Opened the specified ports:\"\n    for port in \"${PORT_LIST[@]}\"; do\n        if [[ $port == *-* ]]; then\n            start_port=$(echo $port | cut -d'-' -f1)\n            end_port=$(echo $port | cut -d'-' -f2)\n            # Check if the port range has been successfully opened\n            (ufw status | grep -q \"$start_port:$end_port\") && echo \"$start_port-$end_port\"\n        else\n            # Check if the individual port has been successfully opened\n            (ufw status | grep -q \"$port\") && echo \"$port\"\n        fi\n    done\n}\n\ndelete_ports() {\n    # Display current rules with numbers\n    echo \"Current UFW rules:\"\n    ufw status numbered\n\n    # Ask the user how they want to delete rules\n    echo \"Do you want to delete rules by:\"\n    echo \"1) Rule numbers\"\n    echo \"2) Ports\"\n    read -rp \"Enter your choice (1 or 2): \" choice\n\n    if [[ $choice -eq 1 ]]; then\n        # Deleting by rule numbers\n        read -rp \"Enter the rule numbers you want to delete (1, 2, etc.): \" rule_numbers\n\n        # Validate the input\n        if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then\n            echo \"Error: Invalid input. Please enter a comma-separated list of rule numbers.\" >&2\n            exit 1\n        fi\n\n        # Split numbers into an array\n        IFS=',' read -ra RULE_NUMBERS <<<\"$rule_numbers\"\n        for rule_number in \"${RULE_NUMBERS[@]}\"; do\n            # Delete the rule by number\n            ufw delete \"$rule_number\" || echo \"Failed to delete rule number $rule_number\"\n        done\n\n        echo \"Selected rules have been deleted.\"\n\n    elif [[ $choice -eq 2 ]]; then\n        # Deleting by ports\n        read -rp \"Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): \" ports\n\n        # Validate the input\n        if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then\n            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\n            exit 1\n        fi\n\n        # Split ports into an array\n        IFS=',' read -ra PORT_LIST <<<\"$ports\"\n        for port in \"${PORT_LIST[@]}\"; do\n            if [[ $port == *-* ]]; then\n                # Split the port range\n                start_port=$(echo $port | cut -d'-' -f1)\n                end_port=$(echo $port | cut -d'-' -f2)\n                # Delete the port range\n                ufw delete allow $start_port:$end_port/tcp\n                ufw delete allow $start_port:$end_port/udp\n            else\n                # Delete a single port\n                ufw delete allow \"$port\"\n            fi\n        done\n\n        # Confirmation of deletion\n        echo \"Deleted the specified ports:\"\n        for port in \"${PORT_LIST[@]}\"; do\n            if [[ $port == *-* ]]; then\n                start_port=$(echo $port | cut -d'-' -f1)\n                end_port=$(echo $port | cut -d'-' -f2)\n                # Check if the port range has been deleted\n                (ufw status | grep -q \"$start_port:$end_port\") || echo \"$start_port-$end_port\"\n            else\n                # Check if the individual port has been deleted\n                (ufw status | grep -q \"$port\") || echo \"$port\"\n            fi\n        done\n    else\n        echo \"${red}Error:${plain} Invalid choice. Please enter 1 or 2.\" >&2\n        exit 1\n    fi\n}\n\nupdate_all_geofiles() {\n    update_geofiles \"main\"\n    update_geofiles \"IR\"\n    update_geofiles \"RU\"\n}\n\nupdate_geofiles() {\n    case \"${1}\" in\n      \"main\") dat_files=(geoip geosite); dat_source=\"Loyalsoldier/v2ray-rules-dat\";;\n        \"IR\") dat_files=(geoip_IR geosite_IR); dat_source=\"chocolate4u/Iran-v2ray-rules\" ;;\n        \"RU\") dat_files=(geoip_RU geosite_RU); dat_source=\"runetfreedom/russia-v2ray-rules-dat\";;\n    esac\n    for dat in \"${dat_files[@]}\"; do\n        # Remove suffix for remote filename (e.g., geoip_IR -> geoip)\n        remote_file=\"${dat%%_*}\"\n        curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \\\n            https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat\n    done\n}\n\nupdate_geo() {\n    echo -e \"${green}\\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)\"\n    echo -e \"${green}\\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)\"\n    echo -e \"${green}\\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)\"\n    echo -e \"${green}\\t4.${plain} All\"\n    echo -e \"${green}\\t0.${plain} Back to Main Menu\"\n    read -rp \"Choose an option: \" choice\n\n    case \"$choice\" in\n    0)\n        show_menu\n        ;;\n    1)\n        update_geofiles \"main\"\n        echo -e \"${green}Loyalsoldier datasets have been updated successfully!${plain}\"\n        restart\n        ;;\n    2)\n        update_geofiles \"IR\"\n        echo -e \"${green}chocolate4u datasets have been updated successfully!${plain}\"\n        restart\n        ;;\n    3)\n        update_geofiles \"RU\"\n        echo -e \"${green}runetfreedom datasets have been updated successfully!${plain}\"\n        restart\n        ;;\n    4)\n        update_all_geofiles\n        echo -e \"${green}All geo files have been updated successfully!${plain}\"\n        restart\n        ;;\n    *)\n        echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n        update_geo\n        ;;\n    esac\n\n    before_show_menu\n}\n\ninstall_acme() {\n    # Check if acme.sh is already installed\n    if command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        LOGI \"acme.sh is already installed.\"\n        return 0\n    fi\n\n    LOGI \"Installing acme.sh...\"\n    cd ~ || return 1 # Ensure you can change to the home directory\n\n    curl -s https://get.acme.sh | sh\n    if [ $? -ne 0 ]; then\n        LOGE \"Installation of acme.sh failed.\"\n        return 1\n    else\n        LOGI \"Installation of acme.sh succeeded.\"\n    fi\n\n    return 0\n}\n\nssl_cert_issue_main() {\n    echo -e \"${green}\\t1.${plain} Get SSL (Domain)\"\n    echo -e \"${green}\\t2.${plain} Revoke\"\n    echo -e \"${green}\\t3.${plain} Force Renew\"\n    echo -e \"${green}\\t4.${plain} Show Existing Domains\"\n    echo -e \"${green}\\t5.${plain} Set Cert paths for the panel\"\n    echo -e \"${green}\\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)\"\n    echo -e \"${green}\\t0.${plain} Back to Main Menu\"\n\n    read -rp \"Choose an option: \" choice\n    case \"$choice\" in\n    0)\n        show_menu\n        ;;\n    1)\n        ssl_cert_issue\n        ssl_cert_issue_main\n        ;;\n    2)\n        local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \\;)\n        if [ -z \"$domains\" ]; then\n            echo \"No certificates found to revoke.\"\n        else\n            echo \"Existing domains:\"\n            echo \"$domains\"\n            read -rp \"Please enter a domain from the list to revoke the certificate: \" domain\n            if echo \"$domains\" | grep -qw \"$domain\"; then\n                ~/.acme.sh/acme.sh --revoke -d ${domain}\n                LOGI \"Certificate revoked for domain: $domain\"\n            else\n                echo \"Invalid domain entered.\"\n            fi\n        fi\n        ssl_cert_issue_main\n        ;;\n    3)\n        local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \\;)\n        if [ -z \"$domains\" ]; then\n            echo \"No certificates found to renew.\"\n        else\n            echo \"Existing domains:\"\n            echo \"$domains\"\n            read -rp \"Please enter a domain from the list to renew the SSL certificate: \" domain\n            if echo \"$domains\" | grep -qw \"$domain\"; then\n                ~/.acme.sh/acme.sh --renew -d ${domain} --force\n                LOGI \"Certificate forcefully renewed for domain: $domain\"\n            else\n                echo \"Invalid domain entered.\"\n            fi\n        fi\n        ssl_cert_issue_main\n        ;;\n    4)\n        local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \\;)\n        if [ -z \"$domains\" ]; then\n            echo \"No certificates found.\"\n        else\n            echo \"Existing domains and their paths:\"\n            for domain in $domains; do\n                local cert_path=\"/root/cert/${domain}/fullchain.pem\"\n                local key_path=\"/root/cert/${domain}/privkey.pem\"\n                if [[ -f \"${cert_path}\" && -f \"${key_path}\" ]]; then\n                    echo -e \"Domain: ${domain}\"\n                    echo -e \"\\tCertificate Path: ${cert_path}\"\n                    echo -e \"\\tPrivate Key Path: ${key_path}\"\n                else\n                    echo -e \"Domain: ${domain} - Certificate or Key missing.\"\n                fi\n            done\n        fi\n        ssl_cert_issue_main\n        ;;\n    5)\n        local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \\;)\n        if [ -z \"$domains\" ]; then\n            echo \"No certificates found.\"\n        else\n            echo \"Available domains:\"\n            echo \"$domains\"\n            read -rp \"Please choose a domain to set the panel paths: \" domain\n\n            if echo \"$domains\" | grep -qw \"$domain\"; then\n                local webCertFile=\"/root/cert/${domain}/fullchain.pem\"\n                local webKeyFile=\"/root/cert/${domain}/privkey.pem\"\n\n                if [[ -f \"${webCertFile}\" && -f \"${webKeyFile}\" ]]; then\n                    ${xui_folder}/x-ui cert -webCert \"$webCertFile\" -webCertKey \"$webKeyFile\"\n                    echo \"Panel paths set for domain: $domain\"\n                    echo \"  - Certificate File: $webCertFile\"\n                    echo \"  - Private Key File: $webKeyFile\"\n                    restart\n                else\n                    echo \"Certificate or private key not found for domain: $domain.\"\n                fi\n            else\n                echo \"Invalid domain entered.\"\n            fi\n        fi\n        ssl_cert_issue_main\n        ;;\n    6)\n        echo -e \"${yellow}Let's Encrypt SSL Certificate for IP Address${plain}\"\n        echo -e \"This will obtain a certificate for your server's IP using the shortlived profile.\"\n        echo -e \"${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}\"\n        echo -e \"${yellow}Port 80 must be open and accessible from the internet.${plain}\"\n        confirm \"Do you want to proceed?\" \"y\"\n        if [[ $? == 0 ]]; then\n            ssl_cert_issue_for_ip\n        fi\n        ssl_cert_issue_main\n        ;;\n\n    *)\n        echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n        ssl_cert_issue_main\n        ;;\n    esac\n}\n\nssl_cert_issue_for_ip() {\n    LOGI \"Starting automatic SSL certificate generation for server IP...\"\n    LOGI \"Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)\"\n    \n    local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')\n    local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')\n    \n    # Get server IP\n    local server_ip=$(curl -s --max-time 3 https://api.ipify.org)\n    if [ -z \"$server_ip\" ]; then\n        server_ip=$(curl -s --max-time 3 https://4.ident.me)\n    fi\n    \n    if [ -z \"$server_ip\" ]; then\n        LOGE \"Failed to get server IP address\"\n        return 1\n    fi\n    \n    LOGI \"Server IP detected: ${server_ip}\"\n    \n    # Ask for optional IPv6\n    local ipv6_addr=\"\"\n    read -rp \"Do you have an IPv6 address to include? (leave empty to skip): \" ipv6_addr\n    ipv6_addr=\"${ipv6_addr// /}\"  # Trim whitespace\n    \n    # check for acme.sh first\n    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        LOGI \"acme.sh not found, installing...\"\n        install_acme\n        if [ $? -ne 0 ]; then\n            LOGE \"Failed to install acme.sh\"\n            return 1\n        fi\n    fi\n    \n    # install socat\n    case \"${release}\" in\n    ubuntu | debian | armbian)\n        apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1\n        ;;\n    fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)\n        dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1\n        ;;\n    centos)\n        if [[ \"${VERSION_ID}\" =~ ^7 ]]; then\n            yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1\n        else\n            dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1\n        fi\n        ;;\n    arch | manjaro | parch)\n        pacman -Sy --noconfirm socat >/dev/null 2>&1\n        ;;\n    opensuse-tumbleweed | opensuse-leap)\n        zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1\n        ;;\n    alpine)\n        apk add socat curl openssl >/dev/null 2>&1\n        ;;\n    *)\n        LOGW \"Unsupported OS for automatic socat installation\"\n        ;;\n    esac\n    \n    # Create certificate directory\n    certPath=\"/root/cert/ip\"\n    mkdir -p \"$certPath\"\n    \n    # Build domain arguments\n    local domain_args=\"-d ${server_ip}\"\n    if [[ -n \"$ipv6_addr\" ]] && is_ipv6 \"$ipv6_addr\"; then\n        domain_args=\"${domain_args} -d ${ipv6_addr}\"\n        LOGI \"Including IPv6 address: ${ipv6_addr}\"\n    fi\n    \n    # Choose port for HTTP-01 listener (default 80, allow override)\n    local WebPort=\"\"\n    read -rp \"Port to use for ACME HTTP-01 listener (default 80): \" WebPort\n    WebPort=\"${WebPort:-80}\"\n    if ! [[ \"${WebPort}\" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then\n        LOGE \"Invalid port provided. Falling back to 80.\"\n        WebPort=80\n    fi\n    LOGI \"Using port ${WebPort} to issue certificate for IP: ${server_ip}\"\n    if [[ \"${WebPort}\" -ne 80 ]]; then\n        LOGI \"Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation.\"\n    fi\n\n    while true; do\n        if is_port_in_use \"${WebPort}\"; then\n            LOGI \"Port ${WebPort} is currently in use.\"\n\n            local alt_port=\"\"\n            read -rp \"Enter another port for acme.sh standalone listener (leave empty to abort): \" alt_port\n            alt_port=\"${alt_port// /}\"\n            if [[ -z \"${alt_port}\" ]]; then\n                LOGE \"Port ${WebPort} is busy; cannot proceed with issuance.\"\n                return 1\n            fi\n            if ! [[ \"${alt_port}\" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then\n                LOGE \"Invalid port provided.\"\n                return 1\n            fi\n            WebPort=\"${alt_port}\"\n            continue\n        else\n            LOGI \"Port ${WebPort} is free and ready for standalone validation.\"\n            break\n        fi\n    done\n    \n    # Reload command - restarts panel after renewal\n    local reloadCmd=\"systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null\"\n    \n    # issue the certificate for IP with shortlived profile\n    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force\n    ~/.acme.sh/acme.sh --issue \\\n        ${domain_args} \\\n        --standalone \\\n        --server letsencrypt \\\n        --certificate-profile shortlived \\\n        --days 6 \\\n        --httpport ${WebPort} \\\n        --force\n    \n    if [ $? -ne 0 ]; then\n        LOGE \"Failed to issue certificate for IP: ${server_ip}\"\n        LOGE \"Make sure port ${WebPort} is open and the server is accessible from the internet\"\n        # Cleanup acme.sh data for both IPv4 and IPv6 if specified\n        rm -rf ~/.acme.sh/${server_ip} 2>/dev/null\n        [[ -n \"$ipv6_addr\" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null\n        rm -rf ${certPath} 2>/dev/null\n        return 1\n    else\n        LOGI \"Certificate issued successfully for IP: ${server_ip}\"\n    fi\n    \n    # Install the certificate\n    # Note: acme.sh may report \"Reload error\" and exit non-zero if reloadcmd fails,\n    # but the cert files are still installed. We check for files instead of exit code.\n    ~/.acme.sh/acme.sh --installcert -d ${server_ip} \\\n        --key-file \"${certPath}/privkey.pem\" \\\n        --fullchain-file \"${certPath}/fullchain.pem\" \\\n        --reloadcmd \"${reloadCmd}\" 2>&1 || true\n    \n    # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)\n    if [[ ! -f \"${certPath}/fullchain.pem\" || ! -f \"${certPath}/privkey.pem\" ]]; then\n        LOGE \"Certificate files not found after installation\"\n        # Cleanup acme.sh data for both IPv4 and IPv6 if specified\n        rm -rf ~/.acme.sh/${server_ip} 2>/dev/null\n        [[ -n \"$ipv6_addr\" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null\n        rm -rf ${certPath} 2>/dev/null\n        return 1\n    fi\n    \n    LOGI \"Certificate files installed successfully\"\n    \n    # enable auto-renew\n    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1\n    chmod 600 $certPath/privkey.pem 2>/dev/null\n    chmod 644 $certPath/fullchain.pem 2>/dev/null\n    \n    # Set certificate paths for the panel\n    local webCertFile=\"${certPath}/fullchain.pem\"\n    local webKeyFile=\"${certPath}/privkey.pem\"\n    \n    if [[ -f \"$webCertFile\" && -f \"$webKeyFile\" ]]; then\n        ${xui_folder}/x-ui cert -webCert \"$webCertFile\" -webCertKey \"$webKeyFile\"\n        LOGI \"Certificate configured for panel\"\n        LOGI \"  - Certificate File: $webCertFile\"\n        LOGI \"  - Private Key File: $webKeyFile\"\n        LOGI \"  - Validity: ~6 days (auto-renews via acme.sh cron)\"\n        echo -e \"${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}\"\n        LOGI \"Panel will restart to apply SSL certificate...\"\n        restart\n        return 0\n    else\n        LOGE \"Certificate files not found after installation\"\n        return 1\n    fi\n}\n\nssl_cert_issue() {\n    local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')\n    local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')\n    # check for acme.sh first\n    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n        echo \"acme.sh could not be found. we will install it\"\n        install_acme\n        if [ $? -ne 0 ]; then\n            LOGE \"install acme failed, please check logs\"\n            exit 1\n        fi\n    fi\n\n    # install socat\n    case \"${release}\" in\n    ubuntu | debian | armbian)\n        apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1\n        ;;\n    fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)\n        dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1\n        ;;\n    centos)\n        if [[ \"${VERSION_ID}\" =~ ^7 ]]; then\n            yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1\n        else\n            dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1\n        fi\n        ;;\n    arch | manjaro | parch)\n        pacman -Sy --noconfirm socat >/dev/null 2>&1\n        ;;\n    opensuse-tumbleweed | opensuse-leap)\n        zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1\n        ;;\n    alpine)\n        apk add socat curl openssl >/dev/null 2>&1\n        ;;\n    *)\n        LOGW \"Unsupported OS for automatic socat installation\"\n        ;;\n    esac\n    if [ $? -ne 0 ]; then\n        LOGE \"install socat failed, please check logs\"\n        exit 1\n    else\n        LOGI \"install socat succeed...\"\n    fi\n\n    # get the domain here, and we need to verify it\n    local domain=\"\"\n    while true; do\n        read -rp \"Please enter your domain name: \" domain\n        domain=\"${domain// /}\"  # Trim whitespace\n        \n        if [[ -z \"$domain\" ]]; then\n            LOGE \"Domain name cannot be empty. Please try again.\"\n            continue\n        fi\n        \n        if ! is_domain \"$domain\"; then\n            LOGE \"Invalid domain format: ${domain}. Please enter a valid domain name.\"\n            continue\n        fi\n        \n        break\n    done\n    LOGD \"Your domain is: ${domain}, checking it...\"\n\n    # check if there already exists a certificate\n    local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')\n    if [ \"${currentCert}\" == \"${domain}\" ]; then\n        local certInfo=$(~/.acme.sh/acme.sh --list)\n        LOGE \"System already has certificates for this domain. Cannot issue again. Current certificate details:\"\n        LOGI \"$certInfo\"\n        exit 1\n    else\n        LOGI \"Your domain is ready for issuing certificates now...\"\n    fi\n\n    # create a directory for the certificate\n    certPath=\"/root/cert/${domain}\"\n    if [ ! -d \"$certPath\" ]; then\n        mkdir -p \"$certPath\"\n    else\n        rm -rf \"$certPath\"\n        mkdir -p \"$certPath\"\n    fi\n\n    # get the port number for the standalone server\n    local WebPort=80\n    read -rp \"Please choose which port to use (default is 80): \" WebPort\n    if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then\n        LOGE \"Your input ${WebPort} is invalid, will use default port 80.\"\n        WebPort=80\n    fi\n    LOGI \"Will use port: ${WebPort} to issue certificates. Please make sure this port is open.\"\n\n    # issue the certificate\n    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force\n    ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force\n    if [ $? -ne 0 ]; then\n        LOGE \"Issuing certificate failed, please check logs.\"\n        rm -rf ~/.acme.sh/${domain}\n        exit 1\n    else\n        LOGE \"Issuing certificate succeeded, installing certificates...\"\n    fi\n\n    reloadCmd=\"x-ui restart\"\n\n    LOGI \"Default --reloadcmd for ACME is: ${yellow}x-ui restart\"\n    LOGI \"This command will run on every certificate issue and renew.\"\n    read -rp \"Would you like to modify --reloadcmd for ACME? (y/n): \" setReloadcmd\n    if [[ \"$setReloadcmd\" == \"y\" || \"$setReloadcmd\" == \"Y\" ]]; then\n        echo -e \"\\n${green}\\t1.${plain} Preset: systemctl reload nginx ; x-ui restart\"\n        echo -e \"${green}\\t2.${plain} Input your own command\"\n        echo -e \"${green}\\t0.${plain} Keep default reloadcmd\"\n        read -rp \"Choose an option: \" choice\n        case \"$choice\" in\n        1)\n            LOGI \"Reloadcmd is: systemctl reload nginx ; x-ui restart\"\n            reloadCmd=\"systemctl reload nginx ; x-ui restart\"\n            ;;\n        2)  \n            LOGD \"It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails\"\n            read -rp \"Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): \" reloadCmd\n            LOGI \"Your reloadcmd is: ${reloadCmd}\"\n            ;;\n        *)\n            LOGI \"Keep default reloadcmd\"\n            ;;\n        esac\n    fi\n\n    # install the certificate\n    ~/.acme.sh/acme.sh --installcert -d ${domain} \\\n        --key-file /root/cert/${domain}/privkey.pem \\\n        --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd \"${reloadCmd}\"\n\n    if [ $? -ne 0 ]; then\n        LOGE \"Installing certificate failed, exiting.\"\n        rm -rf ~/.acme.sh/${domain}\n        exit 1\n    else\n        LOGI \"Installing certificate succeeded, enabling auto renew...\"\n    fi\n\n    # enable auto-renew\n    ~/.acme.sh/acme.sh --upgrade --auto-upgrade\n    if [ $? -ne 0 ]; then\n        LOGE \"Auto renew failed, certificate details:\"\n        ls -lah cert/*\n        chmod 600 $certPath/privkey.pem\n        chmod 644 $certPath/fullchain.pem\n        exit 1\n    else\n        LOGI \"Auto renew succeeded, certificate details:\"\n        ls -lah cert/*\n        chmod 600 $certPath/privkey.pem\n        chmod 644 $certPath/fullchain.pem\n    fi\n\n    # Prompt user to set panel paths after successful certificate installation\n    read -rp \"Would you like to set this certificate for the panel? (y/n): \" setPanel\n    if [[ \"$setPanel\" == \"y\" || \"$setPanel\" == \"Y\" ]]; then\n        local webCertFile=\"/root/cert/${domain}/fullchain.pem\"\n        local webKeyFile=\"/root/cert/${domain}/privkey.pem\"\n\n        if [[ -f \"$webCertFile\" && -f \"$webKeyFile\" ]]; then\n            ${xui_folder}/x-ui cert -webCert \"$webCertFile\" -webCertKey \"$webKeyFile\"\n            LOGI \"Panel paths set for domain: $domain\"\n            LOGI \"  - Certificate File: $webCertFile\"\n            LOGI \"  - Private Key File: $webKeyFile\"\n            echo -e \"${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}\"\n            restart\n        else\n            LOGE \"Error: Certificate or private key file not found for domain: $domain.\"\n        fi\n    else\n        LOGI \"Skipping panel path setting.\"\n    fi\n}\n\nssl_cert_issue_CF() {\n    local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')\n    local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')\n    LOGI \"****** Instructions for Use ******\"\n    LOGI \"Follow the steps below to complete the process:\"\n    LOGI \"1. Cloudflare Registered E-mail.\"\n    LOGI \"2. Cloudflare Global API Key.\"\n    LOGI \"3. The Domain Name.\"\n    LOGI \"4. Once the certificate is issued, you will be prompted to set the certificate for the panel (optional).\"\n    LOGI \"5. The script also supports automatic renewal of the SSL certificate after installation.\"\n\n    confirm \"Do you confirm the information and wish to proceed? [y/n]\" \"y\"\n\n    if [ $? -eq 0 ]; then\n        # Check for acme.sh first\n        if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then\n            echo \"acme.sh could not be found. We will install it.\"\n            install_acme\n            if [ $? -ne 0 ]; then\n                LOGE \"Install acme failed, please check logs.\"\n                exit 1\n            fi\n        fi\n\n        CF_Domain=\"\"\n\n        LOGD \"Please set a domain name:\"\n        read -rp \"Input your domain here: \" CF_Domain\n        LOGD \"Your domain name is set to: ${CF_Domain}\"\n\n        # Set up Cloudflare API details\n        CF_GlobalKey=\"\"\n        CF_AccountEmail=\"\"\n        LOGD \"Please set the API key:\"\n        read -rp \"Input your key here: \" CF_GlobalKey\n        LOGD \"Your API key is: ${CF_GlobalKey}\"\n\n        LOGD \"Please set up registered email:\"\n        read -rp \"Input your email here: \" CF_AccountEmail\n        LOGD \"Your registered email address is: ${CF_AccountEmail}\"\n\n        # Set the default CA to Let's Encrypt\n        ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force\n        if [ $? -ne 0 ]; then\n            LOGE \"Default CA, Let'sEncrypt fail, script exiting...\"\n            exit 1\n        fi\n\n        export CF_Key=\"${CF_GlobalKey}\"\n        export CF_Email=\"${CF_AccountEmail}\"\n\n        # Issue the certificate using Cloudflare DNS\n        ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force\n        if [ $? -ne 0 ]; then\n            LOGE \"Certificate issuance failed, script exiting...\"\n            exit 1\n        else\n            LOGI \"Certificate issued successfully, Installing...\"\n        fi\n\n         # Install the certificate\n        certPath=\"/root/cert/${CF_Domain}\"\n        if [ -d \"$certPath\" ]; then\n            rm -rf ${certPath}\n        fi\n\n        mkdir -p ${certPath}\n        if [ $? -ne 0 ]; then\n            LOGE \"Failed to create directory: ${certPath}\"\n            exit 1\n        fi\n\n        reloadCmd=\"x-ui restart\"\n\n        LOGI \"Default --reloadcmd for ACME is: ${yellow}x-ui restart\"\n        LOGI \"This command will run on every certificate issue and renew.\"\n        read -rp \"Would you like to modify --reloadcmd for ACME? (y/n): \" setReloadcmd\n        if [[ \"$setReloadcmd\" == \"y\" || \"$setReloadcmd\" == \"Y\" ]]; then\n            echo -e \"\\n${green}\\t1.${plain} Preset: systemctl reload nginx ; x-ui restart\"\n            echo -e \"${green}\\t2.${plain} Input your own command\"\n            echo -e \"${green}\\t0.${plain} Keep default reloadcmd\"\n            read -rp \"Choose an option: \" choice\n            case \"$choice\" in\n            1)\n                LOGI \"Reloadcmd is: systemctl reload nginx ; x-ui restart\"\n                reloadCmd=\"systemctl reload nginx ; x-ui restart\"\n                ;;\n            2)  \n                LOGD \"It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails\"\n                read -rp \"Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): \" reloadCmd\n                LOGI \"Your reloadcmd is: ${reloadCmd}\"\n                ;;\n            *)\n                LOGI \"Keep default reloadcmd\"\n                ;;\n            esac\n        fi\n        ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \\\n            --key-file ${certPath}/privkey.pem \\\n            --fullchain-file ${certPath}/fullchain.pem --reloadcmd \"${reloadCmd}\"\n        \n        if [ $? -ne 0 ]; then\n            LOGE \"Certificate installation failed, script exiting...\"\n            exit 1\n        else\n            LOGI \"Certificate installed successfully, Turning on automatic updates...\"\n        fi\n\n        # Enable auto-update\n        ~/.acme.sh/acme.sh --upgrade --auto-upgrade\n        if [ $? -ne 0 ]; then\n            LOGE \"Auto update setup failed, script exiting...\"\n            exit 1\n        else\n            LOGI \"The certificate is installed and auto-renewal is turned on. Specific information is as follows:\"\n            ls -lah ${certPath}/*\n            chmod 600 ${certPath}/privkey.pem\n            chmod 644 ${certPath}/fullchain.pem\n        fi\n\n        # Prompt user to set panel paths after successful certificate installation\n        read -rp \"Would you like to set this certificate for the panel? (y/n): \" setPanel\n        if [[ \"$setPanel\" == \"y\" || \"$setPanel\" == \"Y\" ]]; then\n            local webCertFile=\"${certPath}/fullchain.pem\"\n            local webKeyFile=\"${certPath}/privkey.pem\"\n\n            if [[ -f \"$webCertFile\" && -f \"$webKeyFile\" ]]; then\n                ${xui_folder}/x-ui cert -webCert \"$webCertFile\" -webCertKey \"$webKeyFile\"\n                LOGI \"Panel paths set for domain: $CF_Domain\"\n                LOGI \"  - Certificate File: $webCertFile\"\n                LOGI \"  - Private Key File: $webKeyFile\"\n                echo -e \"${green}Access URL: https://${CF_Domain}:${existing_port}${existing_webBasePath}${plain}\"\n                restart\n            else\n                LOGE \"Error: Certificate or private key file not found for domain: $CF_Domain.\"\n            fi\n        else\n            LOGI \"Skipping panel path setting.\"\n        fi\n    else\n        show_menu\n    fi\n}\n\nrun_speedtest() {\n    # Check if Speedtest is already installed\n    if ! command -v speedtest &>/dev/null; then\n        # If not installed, determine installation method\n        if command -v snap &>/dev/null; then\n            # Use snap to install Speedtest\n            echo \"Installing Speedtest using snap...\"\n            snap install speedtest\n        else\n            # Fallback to using package managers\n            local pkg_manager=\"\"\n            local speedtest_install_script=\"\"\n\n            if command -v dnf &>/dev/null; then\n                pkg_manager=\"dnf\"\n                speedtest_install_script=\"https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh\"\n            elif command -v yum &>/dev/null; then\n                pkg_manager=\"yum\"\n                speedtest_install_script=\"https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh\"\n            elif command -v apt-get &>/dev/null; then\n                pkg_manager=\"apt-get\"\n                speedtest_install_script=\"https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh\"\n            elif command -v apt &>/dev/null; then\n                pkg_manager=\"apt\"\n                speedtest_install_script=\"https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh\"\n            fi\n\n            if [[ -z $pkg_manager ]]; then\n                echo \"Error: Package manager not found. You may need to install Speedtest manually.\"\n                return 1\n            else\n                echo \"Installing Speedtest using $pkg_manager...\"\n                curl -s $speedtest_install_script | bash\n                $pkg_manager install -y speedtest\n            fi\n        fi\n    fi\n\n    speedtest\n}\n\n\n\nip_validation() {\n    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]))$\"\n    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)$\"\n}\n\niplimit_main() {\n    echo -e \"\\n${green}\\t1.${plain} Install Fail2ban and configure IP Limit\"\n    echo -e \"${green}\\t2.${plain} Change Ban Duration\"\n    echo -e \"${green}\\t3.${plain} Unban Everyone\"\n    echo -e \"${green}\\t4.${plain} Ban Logs\"\n    echo -e \"${green}\\t5.${plain} Ban an IP Address\"\n    echo -e \"${green}\\t6.${plain} Unban an IP Address\"\n    echo -e \"${green}\\t7.${plain} Real-Time Logs\"\n    echo -e \"${green}\\t8.${plain} Service Status\"\n    echo -e \"${green}\\t9.${plain} Service Restart\"\n    echo -e \"${green}\\t10.${plain} Uninstall Fail2ban and IP Limit\"\n    echo -e \"${green}\\t0.${plain} Back to Main Menu\"\n    read -rp \"Choose an option: \" choice\n    case \"$choice\" in\n    0)\n        show_menu\n        ;;\n    1)\n        confirm \"Proceed with installation of Fail2ban & IP Limit?\" \"y\"\n        if [[ $? == 0 ]]; then\n            install_iplimit\n        else\n            iplimit_main\n        fi\n        ;;\n    2)\n        read -rp \"Please enter new Ban Duration in Minutes [default 30]: \" NUM\n        if [[ $NUM =~ ^[0-9]+$ ]]; then\n            create_iplimit_jails ${NUM}\n            if [[ $release == \"alpine\" ]]; then\n                rc-service fail2ban restart\n            else\n                systemctl restart fail2ban\n            fi\n        else\n            echo -e \"${red}${NUM} is not a number! Please, try again.${plain}\"\n        fi\n        iplimit_main\n        ;;\n    3)\n        confirm \"Proceed with Unbanning everyone from IP Limit jail?\" \"y\"\n        if [[ $? == 0 ]]; then\n            fail2ban-client reload --restart --unban 3x-ipl\n            truncate -s 0 \"${iplimit_banned_log_path}\"\n            echo -e \"${green}All users Unbanned successfully.${plain}\"\n            iplimit_main\n        else\n            echo -e \"${yellow}Cancelled.${plain}\"\n        fi\n        iplimit_main\n        ;;\n    4)\n        show_banlog\n        iplimit_main\n        ;;\n    5)\n        read -rp \"Enter the IP address you want to ban: \" ban_ip\n        ip_validation\n        if [[ $ban_ip =~ $ipv4_regex || $ban_ip =~ $ipv6_regex ]]; then\n            fail2ban-client set 3x-ipl banip \"$ban_ip\"\n            echo -e \"${green}IP Address ${ban_ip} has been banned successfully.${plain}\"\n        else\n            echo -e \"${red}Invalid IP address format! Please try again.${plain}\"\n        fi\n        iplimit_main\n        ;;\n    6)\n        read -rp \"Enter the IP address you want to unban: \" unban_ip\n        ip_validation\n        if [[ $unban_ip =~ $ipv4_regex || $unban_ip =~ $ipv6_regex ]]; then\n            fail2ban-client set 3x-ipl unbanip \"$unban_ip\"\n            echo -e \"${green}IP Address ${unban_ip} has been unbanned successfully.${plain}\"\n        else\n            echo -e \"${red}Invalid IP address format! Please try again.${plain}\"\n        fi\n        iplimit_main\n        ;;\n    7)\n        tail -f /var/log/fail2ban.log\n        iplimit_main\n        ;;\n    8)\n        service fail2ban status\n        iplimit_main\n        ;;\n    9)\n        if [[ $release == \"alpine\" ]]; then\n            rc-service fail2ban restart\n        else\n            systemctl restart fail2ban\n        fi\n        iplimit_main\n        ;;\n    10)\n        remove_iplimit\n        iplimit_main\n        ;;\n    *)\n        echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n        iplimit_main\n        ;;\n    esac\n}\n\ninstall_iplimit() {\n    if ! command -v fail2ban-client &>/dev/null; then\n        echo -e \"${green}Fail2ban is not installed. Installing now...!${plain}\\n\"\n\n        # Check the OS and install necessary packages\n        case \"${release}\" in\n        ubuntu)\n            apt-get update\n            if [[ \"${os_version}\" -ge 24 ]]; then\n                apt-get install python3-pip -y\n                python3 -m pip install pyasynchat --break-system-packages\n            fi\n            apt-get install fail2ban -y\n            ;;\n        debian)\n            apt-get update\n            if [ \"$os_version\" -ge 12 ]; then\n                apt-get install -y python3-systemd\n            fi\n            apt-get install -y fail2ban\n            ;;\n        armbian)\n            apt-get update && apt-get install fail2ban -y\n            ;;\n        fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)\n            dnf -y update && dnf -y install fail2ban\n            ;;\n        centos)\n            if [[ \"${VERSION_ID}\" =~ ^7 ]]; then\n                yum update -y && yum install epel-release -y\n                yum -y install fail2ban\n            else\n                dnf -y update && dnf -y install fail2ban\n            fi\n            ;;\n        arch | manjaro | parch)\n            pacman -Syu --noconfirm fail2ban\n            ;;\n        alpine)\n            apk add fail2ban\n            ;;\n        *)\n            echo -e \"${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\\n\"\n            exit 1\n            ;;\n        esac\n\n        if ! command -v fail2ban-client &>/dev/null; then\n            echo -e \"${red}Fail2ban installation failed.${plain}\\n\"\n            exit 1\n        fi\n\n        echo -e \"${green}Fail2ban installed successfully!${plain}\\n\"\n    else\n        echo -e \"${yellow}Fail2ban is already installed.${plain}\\n\"\n    fi\n\n    echo -e \"${green}Configuring IP Limit...${plain}\\n\"\n\n    # make sure there's no conflict for jail files\n    iplimit_remove_conflicts\n\n    # Check if log file exists\n    if ! test -f \"${iplimit_banned_log_path}\"; then\n        touch ${iplimit_banned_log_path}\n    fi\n\n    # Check if service log file exists so fail2ban won't return error\n    if ! test -f \"${iplimit_log_path}\"; then\n        touch ${iplimit_log_path}\n    fi\n\n    # Create the iplimit jail files\n    # we didn't pass the bantime here to use the default value\n    create_iplimit_jails\n\n    # Launching fail2ban\n    if [[ $release == \"alpine\" ]]; then\n        if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then\n            rc-service fail2ban start\n        else\n            rc-service fail2ban restart\n        fi\n        rc-update add fail2ban\n    else\n        if ! systemctl is-active --quiet fail2ban; then\n            systemctl start fail2ban\n        else\n            systemctl restart fail2ban\n        fi\n        systemctl enable fail2ban\n    fi\n\n    echo -e \"${green}IP Limit installed and configured successfully!${plain}\\n\"\n    before_show_menu\n}\n\nremove_iplimit() {\n    echo -e \"${green}\\t1.${plain} Only remove IP Limit configurations\"\n    echo -e \"${green}\\t2.${plain} Uninstall Fail2ban and IP Limit\"\n    echo -e \"${green}\\t0.${plain} Back to Main Menu\"\n    read -rp \"Choose an option: \" num\n    case \"$num\" in\n    1)\n        rm -f /etc/fail2ban/filter.d/3x-ipl.conf\n        rm -f /etc/fail2ban/action.d/3x-ipl.conf\n        rm -f /etc/fail2ban/jail.d/3x-ipl.conf\n        if [[ $release == \"alpine\" ]]; then\n            rc-service fail2ban restart\n        else\n            systemctl restart fail2ban\n        fi\n        echo -e \"${green}IP Limit removed successfully!${plain}\\n\"\n        before_show_menu\n        ;;\n    2)\n        rm -rf /etc/fail2ban\n        if [[ $release == \"alpine\" ]]; then\n            rc-service fail2ban stop\n        else\n            systemctl stop fail2ban\n        fi\n        case \"${release}\" in\n        ubuntu | debian | armbian)\n            apt-get remove -y fail2ban\n            apt-get purge -y fail2ban -y\n            apt-get autoremove -y\n            ;;\n        fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)\n            dnf remove fail2ban -y\n            dnf autoremove -y\n            ;;\n        centos)\n            if [[ \"${VERSION_ID}\" =~ ^7 ]]; then    \n                yum remove fail2ban -y\n                yum autoremove -y\n            else\n                dnf remove fail2ban -y\n                dnf autoremove -y\n            fi\n            ;;\n        arch | manjaro | parch)\n            pacman -Rns --noconfirm fail2ban\n            ;;\n        alpine)\n            apk del fail2ban\n            ;;\n        *)\n            echo -e \"${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\\n\"\n            exit 1\n            ;;\n        esac\n        echo -e \"${green}Fail2ban and IP Limit removed successfully!${plain}\\n\"\n        before_show_menu\n        ;;\n    0)\n        show_menu\n        ;;\n    *)\n        echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n        remove_iplimit\n        ;;\n    esac\n}\n\nshow_banlog() {\n    local system_log=\"/var/log/fail2ban.log\"\n\n    echo -e \"${green}Checking ban logs...${plain}\\n\"\n\n    if [[ $release == \"alpine\" ]]; then\n        if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then\n            echo -e \"${red}Fail2ban service is not running!${plain}\\n\"\n            return 1\n        fi\n    else\n        if ! systemctl is-active --quiet fail2ban; then\n            echo -e \"${red}Fail2ban service is not running!${plain}\\n\"\n            return 1\n        fi\n    fi\n\n    if [[ -f \"$system_log\" ]]; then\n        echo -e \"${green}Recent system ban activities from fail2ban.log:${plain}\"\n        grep \"3x-ipl\" \"$system_log\" | grep -E \"Ban|Unban\" | tail -n 10 || echo -e \"${yellow}No recent system ban activities found${plain}\"\n        echo \"\"\n    fi\n\n    if [[ -f \"${iplimit_banned_log_path}\" ]]; then\n        echo -e \"${green}3X-IPL ban log entries:${plain}\"\n        if [[ -s \"${iplimit_banned_log_path}\" ]]; then\n            grep -v \"INIT\" \"${iplimit_banned_log_path}\" | tail -n 10 || echo -e \"${yellow}No ban entries found${plain}\"\n        else\n            echo -e \"${yellow}Ban log file is empty${plain}\"\n        fi\n    else\n        echo -e \"${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}\"\n    fi\n\n    echo -e \"\\n${green}Current jail status:${plain}\"\n    fail2ban-client status 3x-ipl || echo -e \"${yellow}Unable to get jail status${plain}\"\n}\n\ncreate_iplimit_jails() {\n    # Use default bantime if not passed => 30 minutes\n    local bantime=\"${1:-30}\"\n\n    # Uncomment 'allowipv6 = auto' in fail2ban.conf\n    sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf\n\n    # On Debian 12+ fail2ban's default backend should be changed to systemd\n    if [[  \"${release}\" == \"debian\" && ${os_version} -ge 12 ]]; then\n        sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf\n    fi\n\n    cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf\n[3x-ipl]\nenabled=true\nbackend=auto\nfilter=3x-ipl\naction=3x-ipl\nlogpath=${iplimit_log_path}\nmaxretry=2\nfindtime=32\nbantime=${bantime}m\nEOF\n\n    cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf\n[Definition]\ndatepattern = ^%%Y/%%m/%%d %%H:%%M:%%S\nfailregex   = \\[LIMIT_IP\\]\\s*Email\\s*=\\s*<F-USER>.+</F-USER>\\s*\\|\\|\\s*Disconnecting OLD IP\\s*=\\s*<ADDR>\\s*\\|\\|\\s*Timestamp\\s*=\\s*\\d+\nignoreregex =\nEOF\n\n    cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf\n[INCLUDES]\nbefore = iptables-allports.conf\n\n[Definition]\nactionstart = <iptables> -N f2b-<name>\n              <iptables> -A f2b-<name> -j <returntype>\n              <iptables> -I <chain> -p <protocol> -j f2b-<name>\n\nactionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>\n             <actionflush>\n             <iptables> -X f2b-<name>\n\nactioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \\t]'\n\nactionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>\n            echo \"\\$(date +\"%%Y/%%m/%%d %%H:%%M:%%S\")   BAN   [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds.\" >> ${iplimit_banned_log_path}\n\nactionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>\n              echo \"\\$(date +\"%%Y/%%m/%%d %%H:%%M:%%S\")   UNBAN   [Email] = <F-USER> [IP] = <ip> unbanned.\" >> ${iplimit_banned_log_path}\n\n[Init]\nname = default\nprotocol = tcp\nchain = INPUT\nEOF\n\n    echo -e \"${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}\"\n}\n\niplimit_remove_conflicts() {\n    local jail_files=(\n        /etc/fail2ban/jail.conf\n        /etc/fail2ban/jail.local\n    )\n\n    for file in \"${jail_files[@]}\"; do\n        # Check for [3x-ipl] config in jail file then remove it\n        if test -f \"${file}\" && grep -qw '3x-ipl' ${file}; then\n            sed -i \"/\\[3x-ipl\\]/,/^$/d\" ${file}\n            echo -e \"${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\\n\"\n        fi\n    done\n}\n\nSSH_port_forwarding() {\n    local URL_lists=(\n        \"https://api4.ipify.org\"\n\t\t\"https://ipv4.icanhazip.com\"\n\t\t\"https://v4.api.ipinfo.io/ip\"\n\t\t\"https://ipv4.myexternalip.com/raw\"\n\t\t\"https://4.ident.me\"\n\t\t\"https://check-host.net/ip\"\n    )\n    local server_ip=\"\"\n    for ip_address in \"${URL_lists[@]}\"; do\n        local response=$(curl -s -w \"\\n%{http_code}\" --max-time 3 \"${ip_address}\" 2>/dev/null)\n        local http_code=$(echo \"$response\" | tail -n1)\n        local ip_result=$(echo \"$response\" | head -n-1 | tr -d '[:space:]')\n        if [[ \"${http_code}\" == \"200\" && -n \"${ip_result}\" ]]; then\n            server_ip=\"${ip_result}\"\n            break\n        fi\n    done\n\n    local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')\n    local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')\n    local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')\n    local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}')\n    local existing_key=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}')\n\n    local config_listenIP=\"\"\n    local listen_choice=\"\"\n\n    if [[ -n \"$existing_cert\" && -n \"$existing_key\" ]]; then\n        echo -e \"${green}Panel is secure with SSL.${plain}\"\n        before_show_menu\n    fi\n    if [[ -z \"$existing_cert\" && -z \"$existing_key\" && (-z \"$existing_listenIP\" || \"$existing_listenIP\" == \"0.0.0.0\") ]]; then\n        echo -e \"\\n${red}Warning: No Cert and Key found! The panel is not secure.${plain}\"\n        echo \"Please obtain a certificate or set up SSH port forwarding.\"\n    fi\n\n    if [[ -n \"$existing_listenIP\" && \"$existing_listenIP\" != \"0.0.0.0\" && (-z \"$existing_cert\" && -z \"$existing_key\") ]]; then\n        echo -e \"\\n${green}Current SSH Port Forwarding Configuration:${plain}\"\n        echo -e \"Standard SSH command:\"\n        echo -e \"${yellow}ssh -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}\"\n        echo -e \"\\nIf using SSH key:\"\n        echo -e \"${yellow}ssh -i <sshkeypath> -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}\"\n        echo -e \"\\nAfter connecting, access the panel at:\"\n        echo -e \"${yellow}http://localhost:2222${existing_webBasePath}${plain}\"\n    fi\n\n    echo -e \"\\nChoose an option:\"\n    echo -e \"${green}1.${plain} Set listen IP\"\n    echo -e \"${green}2.${plain} Clear listen IP\"\n    echo -e \"${green}0.${plain} Back to Main Menu\"\n    read -rp \"Choose an option: \" num\n\n    case \"$num\" in\n    1)\n        if [[ -z \"$existing_listenIP\" || \"$existing_listenIP\" == \"0.0.0.0\" ]]; then\n            echo -e \"\\nNo listenIP configured. Choose an option:\"\n            echo -e \"1. Use default IP (127.0.0.1)\"\n            echo -e \"2. Set a custom IP\"\n            read -rp \"Select an option (1 or 2): \" listen_choice\n\n            config_listenIP=\"127.0.0.1\"\n            [[ \"$listen_choice\" == \"2\" ]] && read -rp \"Enter custom IP to listen on: \" config_listenIP\n\n            ${xui_folder}/x-ui setting -listenIP \"${config_listenIP}\" >/dev/null 2>&1\n            echo -e \"${green}listen IP has been set to ${config_listenIP}.${plain}\"\n            echo -e \"\\n${green}SSH Port Forwarding Configuration:${plain}\"\n            echo -e \"Standard SSH command:\"\n            echo -e \"${yellow}ssh -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}\"\n            echo -e \"\\nIf using SSH key:\"\n            echo -e \"${yellow}ssh -i <sshkeypath> -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}\"\n            echo -e \"\\nAfter connecting, access the panel at:\"\n            echo -e \"${yellow}http://localhost:2222${existing_webBasePath}${plain}\"\n            restart\n        else\n            config_listenIP=\"${existing_listenIP}\"\n            echo -e \"${green}Current listen IP is already set to ${config_listenIP}.${plain}\"\n        fi\n        ;;\n    2)\n        ${xui_folder}/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1\n        echo -e \"${green}Listen IP has been cleared.${plain}\"\n        restart\n        ;;\n    0)\n        show_menu\n        ;;\n    *)\n        echo -e \"${red}Invalid option. Please select a valid number.${plain}\\n\"\n        SSH_port_forwarding\n        ;;\n    esac\n}\n\nshow_usage() {\n    echo -e \"┌────────────────────────────────────────────────────────────────┐\n│  ${blue}x-ui control menu usages (subcommands):${plain}                       │\n│                                                                │\n│  ${blue}x-ui${plain}                       - Admin Management Script          │\n│  ${blue}x-ui start${plain}                 - Start                            │\n│  ${blue}x-ui stop${plain}                  - Stop                             │\n│  ${blue}x-ui restart${plain}               - Restart                          │\n|  ${blue}x-ui restart-xray${plain}          - Restart Xray                     │\n│  ${blue}x-ui status${plain}                - Current Status                   │\n│  ${blue}x-ui settings${plain}              - Current Settings                 │\n│  ${blue}x-ui enable${plain}                - Enable Autostart on OS Startup   │\n│  ${blue}x-ui disable${plain}               - Disable Autostart on OS Startup  │\n│  ${blue}x-ui log${plain}                   - Check logs                       │\n│  ${blue}x-ui banlog${plain}                - Check Fail2ban ban logs          │\n│  ${blue}x-ui update${plain}                - Update                           │\n│  ${blue}x-ui update-all-geofiles${plain}   - Update all geo files             │\n│  ${blue}x-ui legacy${plain}                - Legacy version                   │\n│  ${blue}x-ui install${plain}               - Install                          │\n│  ${blue}x-ui uninstall${plain}             - Uninstall                        │\n└────────────────────────────────────────────────────────────────┘\"\n}\n\nshow_menu() {\n    echo -e \"\n╔────────────────────────────────────────────────╗\n│   ${green}3X-UI Panel Management Script${plain}                │\n│   ${green}0.${plain} Exit Script                               │\n│────────────────────────────────────────────────│\n│   ${green}1.${plain} Install                                   │\n│   ${green}2.${plain} Update                                    │\n│   ${green}3.${plain} Update Menu                               │\n│   ${green}4.${plain} Legacy Version                            │\n│   ${green}5.${plain} Uninstall                                 │\n│────────────────────────────────────────────────│\n│   ${green}6.${plain} Reset Username & Password                 │\n│   ${green}7.${plain} Reset Web Base Path                       │\n│   ${green}8.${plain} Reset Settings                            │\n│   ${green}9.${plain} Change Port                               │\n│  ${green}10.${plain} View Current Settings                     │\n│────────────────────────────────────────────────│\n│  ${green}11.${plain} Start                                     │\n│  ${green}12.${plain} Stop                                      │\n│  ${green}13.${plain} Restart                                   │\n|  ${green}14.${plain} Restart Xray                              │\n│  ${green}15.${plain} Check Status                              │\n│  ${green}16.${plain} Logs Management                           │\n│────────────────────────────────────────────────│\n│  ${green}17.${plain} Enable Autostart                          │\n│  ${green}18.${plain} Disable Autostart                         │\n│────────────────────────────────────────────────│\n│  ${green}19.${plain} SSL Certificate Management                │\n│  ${green}20.${plain} Cloudflare SSL Certificate                │\n│  ${green}21.${plain} IP Limit Management                       │\n│  ${green}22.${plain} Firewall Management                       │\n│  ${green}23.${plain} SSH Port Forwarding Management            │\n│────────────────────────────────────────────────│\n│  ${green}24.${plain} Enable BBR                                │\n│  ${green}25.${plain} Update Geo Files                          │\n│  ${green}26.${plain} Speedtest by Ookla                        │\n╚────────────────────────────────────────────────╝\n\"\n    show_status\n    echo && read -rp \"Please enter your selection [0-26]: \" num\n\n    case \"${num}\" in\n    0)\n        exit 0\n        ;;\n    1)\n        check_uninstall && install\n        ;;\n    2)\n        check_install && update\n        ;;\n    3)\n        check_install && update_menu\n        ;;\n    4)\n        check_install && legacy_version\n        ;;\n    5)\n        check_install && uninstall\n        ;;\n    6)\n        check_install && reset_user\n        ;;\n    7)\n        check_install && reset_webbasepath\n        ;;\n    8)\n        check_install && reset_config\n        ;;\n    9)\n        check_install && set_port\n        ;;\n    10)\n        check_install && check_config\n        ;;\n    11)\n        check_install && start\n        ;;\n    12)\n        check_install && stop\n        ;;\n    13)\n        check_install && restart\n        ;;\n    14)\n        check_install && restart_xray\n        ;;\n    15)\n        check_install && status\n        ;;\n    16)\n        check_install && show_log\n        ;;\n    17)\n        check_install && enable\n        ;;\n    18)\n        check_install && disable\n        ;;\n    19)\n        ssl_cert_issue_main\n        ;;\n    20)\n        ssl_cert_issue_CF\n        ;;\n    21)\n        iplimit_main\n        ;;\n    22)\n        firewall_menu\n        ;;\n    23)\n        SSH_port_forwarding\n        ;;\n    24)\n        bbr_menu\n        ;;\n    25)\n        update_geo\n        ;;\n    26)\n        run_speedtest\n        ;;\n    *)\n        LOGE \"Please enter the correct number [0-26]\"\n        ;;\n    esac\n}\n\nif [[ $# > 0 ]]; then\n    case $1 in\n    \"start\")\n        check_install 0 && start 0\n        ;;\n    \"stop\")\n        check_install 0 && stop 0\n        ;;\n    \"restart\")\n        check_install 0 && restart 0\n        ;;\n    \"restart-xray\")\n        check_install 0 && restart_xray 0\n        ;;\n    \"status\")\n        check_install 0 && status 0\n        ;;\n    \"settings\")\n        check_install 0 && check_config 0\n        ;;\n    \"enable\")\n        check_install 0 && enable 0\n        ;;\n    \"disable\")\n        check_install 0 && disable 0\n        ;;\n    \"log\")\n        check_install 0 && show_log 0\n        ;;\n    \"banlog\")\n        check_install 0 && show_banlog 0\n        ;;\n    \"update\")\n        check_install 0 && update 0\n        ;;\n    \"legacy\")\n        check_install 0 && legacy_version 0\n        ;;\n    \"install\")\n        check_uninstall 0 && install 0\n        ;;\n    \"uninstall\")\n        check_install 0 && uninstall 0\n        ;;\n    \"update-all-geofiles\")\n        check_install 0 && update_all_geofiles 0 && restart 0\n        ;;\n    *) show_usage ;;\n    esac\nelse\n    show_menu\nfi\n"
  },
  {
    "path": "xray/api.go",
    "content": "// Package xray provides integration with the Xray proxy core.\n// It includes API client functionality, configuration management, traffic monitoring,\n// and process control for Xray instances.\npackage xray\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n\n\t\"github.com/xtls/xray-core/app/proxyman/command\"\n\tstatsService \"github.com/xtls/xray-core/app/stats/command\"\n\t\"github.com/xtls/xray-core/common/protocol\"\n\t\"github.com/xtls/xray-core/common/serial\"\n\t\"github.com/xtls/xray-core/infra/conf\"\n\t\"github.com/xtls/xray-core/proxy/shadowsocks\"\n\t\"github.com/xtls/xray-core/proxy/shadowsocks_2022\"\n\t\"github.com/xtls/xray-core/proxy/trojan\"\n\t\"github.com/xtls/xray-core/proxy/vless\"\n\t\"github.com/xtls/xray-core/proxy/vmess\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\n// XrayAPI is a gRPC client for managing Xray core configuration, inbounds, outbounds, and statistics.\ntype XrayAPI struct {\n\tHandlerServiceClient *command.HandlerServiceClient\n\tStatsServiceClient   *statsService.StatsServiceClient\n\tgrpcClient           *grpc.ClientConn\n\tisConnected          bool\n}\n\n// Init connects to the Xray API server and initializes handler and stats service clients.\nfunc (x *XrayAPI) Init(apiPort int) error {\n\tif apiPort <= 0 || apiPort > math.MaxUint16 {\n\t\treturn fmt.Errorf(\"invalid Xray API port: %d\", apiPort)\n\t}\n\n\taddr := fmt.Sprintf(\"127.0.0.1:%d\", apiPort)\n\tconn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to connect to Xray API: %w\", err)\n\t}\n\n\tx.grpcClient = conn\n\tx.isConnected = true\n\n\thsClient := command.NewHandlerServiceClient(conn)\n\tssClient := statsService.NewStatsServiceClient(conn)\n\n\tx.HandlerServiceClient = &hsClient\n\tx.StatsServiceClient = &ssClient\n\n\treturn nil\n}\n\n// Close closes the gRPC connection and resets the XrayAPI client state.\nfunc (x *XrayAPI) Close() {\n\tif x.grpcClient != nil {\n\t\tx.grpcClient.Close()\n\t}\n\tx.HandlerServiceClient = nil\n\tx.StatsServiceClient = nil\n\tx.isConnected = false\n}\n\n// AddInbound adds a new inbound configuration to the Xray core via gRPC.\nfunc (x *XrayAPI) AddInbound(inbound []byte) error {\n\tclient := *x.HandlerServiceClient\n\n\tconf := new(conf.InboundDetourConfig)\n\terr := json.Unmarshal(inbound, conf)\n\tif err != nil {\n\t\tlogger.Debug(\"Failed to unmarshal inbound:\", err)\n\t\treturn err\n\t}\n\tconfig, err := conf.Build()\n\tif err != nil {\n\t\tlogger.Debug(\"Failed to build inbound Detur:\", err)\n\t\treturn err\n\t}\n\tinboundConfig := command.AddInboundRequest{Inbound: config}\n\n\t_, err = client.AddInbound(context.Background(), &inboundConfig)\n\n\treturn err\n}\n\n// DelInbound removes an inbound configuration from the Xray core by tag.\nfunc (x *XrayAPI) DelInbound(tag string) error {\n\tclient := *x.HandlerServiceClient\n\t_, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{\n\t\tTag: tag,\n\t})\n\treturn err\n}\n\n// AddUser adds a user to an inbound in the Xray core using the specified protocol and user data.\nfunc (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error {\n\tvar account *serial.TypedMessage\n\tswitch Protocol {\n\tcase \"vmess\":\n\t\taccount = serial.ToTypedMessage(&vmess.Account{\n\t\t\tId: user[\"id\"].(string),\n\t\t})\n\tcase \"vless\":\n\t\tvlessAccount := &vless.Account{\n\t\t\tId:   user[\"id\"].(string),\n\t\t\tFlow: user[\"flow\"].(string),\n\t\t}\n\t\t// Add testseed if provided\n\t\tif testseedVal, ok := user[\"testseed\"]; ok {\n\t\t\tif testseedArr, ok := testseedVal.([]any); ok && len(testseedArr) >= 4 {\n\t\t\t\ttestseed := make([]uint32, len(testseedArr))\n\t\t\t\tfor i, v := range testseedArr {\n\t\t\t\t\tif num, ok := v.(float64); ok {\n\t\t\t\t\t\ttestseed[i] = uint32(num)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tvlessAccount.Testseed = testseed\n\t\t\t} else if testseedArr, ok := testseedVal.([]uint32); ok && len(testseedArr) >= 4 {\n\t\t\t\tvlessAccount.Testseed = testseedArr\n\t\t\t}\n\t\t}\n\t\t// Add testpre if provided (for outbound, but can be in user for compatibility)\n\t\tif testpreVal, ok := user[\"testpre\"]; ok {\n\t\t\tif testpre, ok := testpreVal.(float64); ok && testpre > 0 {\n\t\t\t\tvlessAccount.Testpre = uint32(testpre)\n\t\t\t} else if testpre, ok := testpreVal.(uint32); ok && testpre > 0 {\n\t\t\t\tvlessAccount.Testpre = testpre\n\t\t\t}\n\t\t}\n\t\taccount = serial.ToTypedMessage(vlessAccount)\n\tcase \"trojan\":\n\t\taccount = serial.ToTypedMessage(&trojan.Account{\n\t\t\tPassword: user[\"password\"].(string),\n\t\t})\n\tcase \"shadowsocks\":\n\t\tvar ssCipherType shadowsocks.CipherType\n\t\tswitch user[\"cipher\"].(string) {\n\t\tcase \"aes-128-gcm\":\n\t\t\tssCipherType = shadowsocks.CipherType_AES_128_GCM\n\t\tcase \"aes-256-gcm\":\n\t\t\tssCipherType = shadowsocks.CipherType_AES_256_GCM\n\t\tcase \"chacha20-poly1305\", \"chacha20-ietf-poly1305\":\n\t\t\tssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305\n\t\tcase \"xchacha20-poly1305\", \"xchacha20-ietf-poly1305\":\n\t\t\tssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305\n\t\tdefault:\n\t\t\tssCipherType = shadowsocks.CipherType_NONE\n\t\t}\n\n\t\tif ssCipherType != shadowsocks.CipherType_NONE {\n\t\t\taccount = serial.ToTypedMessage(&shadowsocks.Account{\n\t\t\t\tPassword:   user[\"password\"].(string),\n\t\t\t\tCipherType: ssCipherType,\n\t\t\t})\n\t\t} else {\n\t\t\taccount = serial.ToTypedMessage(&shadowsocks_2022.ServerConfig{\n\t\t\t\tKey:   user[\"password\"].(string),\n\t\t\t\tEmail: user[\"email\"].(string),\n\t\t\t})\n\t\t}\n\tdefault:\n\t\treturn nil\n\t}\n\n\tclient := *x.HandlerServiceClient\n\n\t_, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{\n\t\tTag: inboundTag,\n\t\tOperation: serial.ToTypedMessage(&command.AddUserOperation{\n\t\t\tUser: &protocol.User{\n\t\t\t\tEmail:   user[\"email\"].(string),\n\t\t\t\tAccount: account,\n\t\t\t},\n\t\t}),\n\t})\n\treturn err\n}\n\n// RemoveUser removes a user from an inbound in the Xray core by email.\nfunc (x *XrayAPI) RemoveUser(inboundTag, email string) error {\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\top := &command.RemoveUserOperation{Email: email}\n\treq := &command.AlterInboundRequest{\n\t\tTag:       inboundTag,\n\t\tOperation: serial.ToTypedMessage(op),\n\t}\n\n\t_, err := (*x.HandlerServiceClient).AlterInbound(ctx, req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to remove user: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// GetTraffic queries traffic statistics from the Xray core, optionally resetting counters.\nfunc (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {\n\tif x.grpcClient == nil {\n\t\treturn nil, nil, common.NewError(\"xray api is not initialized\")\n\t}\n\n\ttrafficRegex := regexp.MustCompile(`(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)`)\n\tclientTrafficRegex := regexp.MustCompile(`user>>>([^>]+)>>>traffic>>>(downlink|uplink)`)\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\n\tdefer cancel()\n\n\tif x.StatsServiceClient == nil {\n\t\treturn nil, nil, common.NewError(\"xray StatusServiceClient is not initialized\")\n\t}\n\n\tresp, err := (*x.StatsServiceClient).QueryStats(ctx, &statsService.QueryStatsRequest{Reset_: reset})\n\tif err != nil {\n\t\tlogger.Debug(\"Failed to query Xray stats:\", err)\n\t\treturn nil, nil, err\n\t}\n\n\ttagTrafficMap := make(map[string]*Traffic)\n\temailTrafficMap := make(map[string]*ClientTraffic)\n\n\tfor _, stat := range resp.GetStat() {\n\t\tif matches := trafficRegex.FindStringSubmatch(stat.Name); len(matches) == 4 {\n\t\t\tprocessTraffic(matches, stat.Value, tagTrafficMap)\n\t\t} else if matches := clientTrafficRegex.FindStringSubmatch(stat.Name); len(matches) == 3 {\n\t\t\tprocessClientTraffic(matches, stat.Value, emailTrafficMap)\n\t\t}\n\t}\n\treturn mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil\n}\n\n// processTraffic aggregates a traffic stat into trafficMap using regex matches and value.\nfunc processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) {\n\tisInbound := matches[1] == \"inbound\"\n\ttag := matches[2]\n\tisDown := matches[3] == \"downlink\"\n\n\tif tag == \"api\" {\n\t\treturn\n\t}\n\n\ttraffic, ok := trafficMap[tag]\n\tif !ok {\n\t\ttraffic = &Traffic{\n\t\t\tIsInbound:  isInbound,\n\t\t\tIsOutbound: !isInbound,\n\t\t\tTag:        tag,\n\t\t}\n\t\ttrafficMap[tag] = traffic\n\t}\n\n\tif isDown {\n\t\ttraffic.Down = value\n\t} else {\n\t\ttraffic.Up = value\n\t}\n}\n\n// processClientTraffic updates clientTrafficMap with upload/download values for a client email.\nfunc processClientTraffic(matches []string, value int64, clientTrafficMap map[string]*ClientTraffic) {\n\temail := matches[1]\n\tisDown := matches[2] == \"downlink\"\n\n\ttraffic, ok := clientTrafficMap[email]\n\tif !ok {\n\t\ttraffic = &ClientTraffic{Email: email}\n\t\tclientTrafficMap[email] = traffic\n\t}\n\n\tif isDown {\n\t\ttraffic.Down = value\n\t} else {\n\t\ttraffic.Up = value\n\t}\n}\n\n// mapToSlice converts a map of pointers to a slice of pointers.\nfunc mapToSlice[T any](m map[string]*T) []*T {\n\tresult := make([]*T, 0, len(m))\n\tfor _, v := range m {\n\t\tresult = append(result, v)\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "xray/client_traffic.go",
    "content": "package xray\n\n// ClientTraffic represents traffic statistics and limits for a specific client.\n// It tracks upload/download usage, expiry times, and online status for inbound clients.\ntype ClientTraffic struct {\n\tId         int    `json:\"id\" form:\"id\" gorm:\"primaryKey;autoIncrement\"`\n\tInboundId  int    `json:\"inboundId\" form:\"inboundId\"`\n\tEnable     bool   `json:\"enable\" form:\"enable\"`\n\tEmail      string `json:\"email\" form:\"email\" gorm:\"unique\"`\n\tUUID       string `json:\"uuid\" form:\"uuid\" gorm:\"-\"`\n\tSubId      string `json:\"subId\" form:\"subId\" gorm:\"-\"`\n\tUp         int64  `json:\"up\" form:\"up\"`\n\tDown       int64  `json:\"down\" form:\"down\"`\n\tAllTime    int64  `json:\"allTime\" form:\"allTime\"`\n\tExpiryTime int64  `json:\"expiryTime\" form:\"expiryTime\"`\n\tTotal      int64  `json:\"total\" form:\"total\"`\n\tReset      int    `json:\"reset\" form:\"reset\" gorm:\"default:0\"`\n\tLastOnline int64  `json:\"lastOnline\" form:\"lastOnline\" gorm:\"default:0\"`\n}\n"
  },
  {
    "path": "xray/config.go",
    "content": "package xray\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/util/json_util\"\n)\n\n// Config represents the complete Xray configuration structure.\n// It contains all sections of an Xray config file including inbounds, outbounds, routing, etc.\ntype Config struct {\n\tLogConfig        json_util.RawMessage `json:\"log\"`\n\tRouterConfig     json_util.RawMessage `json:\"routing\"`\n\tDNSConfig        json_util.RawMessage `json:\"dns\"`\n\tInboundConfigs   []InboundConfig      `json:\"inbounds\"`\n\tOutboundConfigs  json_util.RawMessage `json:\"outbounds\"`\n\tTransport        json_util.RawMessage `json:\"transport\"`\n\tPolicy           json_util.RawMessage `json:\"policy\"`\n\tAPI              json_util.RawMessage `json:\"api\"`\n\tStats            json_util.RawMessage `json:\"stats\"`\n\tReverse          json_util.RawMessage `json:\"reverse\"`\n\tFakeDNS          json_util.RawMessage `json:\"fakedns\"`\n\tObservatory      json_util.RawMessage `json:\"observatory\"`\n\tBurstObservatory json_util.RawMessage `json:\"burstObservatory\"`\n\tMetrics          json_util.RawMessage `json:\"metrics\"`\n}\n\n// Equals compares two Config instances for deep equality.\nfunc (c *Config) Equals(other *Config) bool {\n\tif len(c.InboundConfigs) != len(other.InboundConfigs) {\n\t\treturn false\n\t}\n\tfor i, inbound := range c.InboundConfigs {\n\t\tif !inbound.Equals(&other.InboundConfigs[i]) {\n\t\t\treturn false\n\t\t}\n\t}\n\tif !bytes.Equal(c.LogConfig, other.LogConfig) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.RouterConfig, other.RouterConfig) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.DNSConfig, other.DNSConfig) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.Transport, other.Transport) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.Policy, other.Policy) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.API, other.API) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.Stats, other.Stats) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.Reverse, other.Reverse) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.FakeDNS, other.FakeDNS) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.Metrics, other.Metrics) {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "xray/inbound.go",
    "content": "package xray\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/util/json_util\"\n)\n\n// InboundConfig represents an Xray inbound configuration.\n// It defines how Xray accepts incoming connections including protocol, port, and settings.\ntype InboundConfig struct {\n\tListen         json_util.RawMessage `json:\"listen\"` // listen cannot be an empty string\n\tPort           int                  `json:\"port\"`\n\tProtocol       string               `json:\"protocol\"`\n\tSettings       json_util.RawMessage `json:\"settings\"`\n\tStreamSettings json_util.RawMessage `json:\"streamSettings\"`\n\tTag            string               `json:\"tag\"`\n\tSniffing       json_util.RawMessage `json:\"sniffing\"`\n}\n\n// Equals compares two InboundConfig instances for deep equality.\nfunc (c *InboundConfig) Equals(other *InboundConfig) bool {\n\tif !bytes.Equal(c.Listen, other.Listen) {\n\t\treturn false\n\t}\n\tif c.Port != other.Port {\n\t\treturn false\n\t}\n\tif c.Protocol != other.Protocol {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.Settings, other.Settings) {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.StreamSettings, other.StreamSettings) {\n\t\treturn false\n\t}\n\tif c.Tag != other.Tag {\n\t\treturn false\n\t}\n\tif !bytes.Equal(c.Sniffing, other.Sniffing) {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "xray/log_writer.go",
    "content": "package xray\n\nimport (\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n)\n\n// NewLogWriter returns a new LogWriter for processing Xray log output.\nfunc NewLogWriter() *LogWriter {\n\treturn &LogWriter{}\n}\n\n// LogWriter processes and filters log output from the Xray process, handling crash detection and message filtering.\ntype LogWriter struct {\n\tlastLine string\n}\n\n// Write processes and filters log output from the Xray process, handling crash detection and message filtering.\nfunc (lw *LogWriter) Write(m []byte) (n int, err error) {\n\tcrashRegex := regexp.MustCompile(`(?i)(panic|exception|stack trace|fatal error)`)\n\n\t// Convert the data to a string\n\tmessage := strings.TrimSpace(string(m))\n\tmsgLowerAll := strings.ToLower(message)\n\n\t// Suppress noisy Windows process-kill signal that surfaces as exit status 1\n\tif runtime.GOOS == \"windows\" && strings.Contains(msgLowerAll, \"exit status 1\") {\n\t\treturn len(m), nil\n\t}\n\n\t// Check if the message contains a crash\n\tif crashRegex.MatchString(message) {\n\t\tlogger.Debug(\"Core crash detected:\\n\", message)\n\t\tlw.lastLine = message\n\t\terr1 := writeCrashReport(m)\n\t\tif err1 != nil {\n\t\t\tlogger.Error(\"Unable to write crash report:\", err1)\n\t\t}\n\t\treturn len(m), nil\n\t}\n\n\tregex := regexp.MustCompile(`^(\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}) \\[([^\\]]+)\\] (.+)$`)\n\tmessages := strings.SplitSeq(message, \"\\n\")\n\n\tfor msg := range messages {\n\t\tmatches := regex.FindStringSubmatch(msg)\n\n\t\tif len(matches) > 3 {\n\t\t\tlevel := matches[2]\n\t\t\tmsgBody := matches[3]\n\t\t\tmsgBodyLower := strings.ToLower(msgBody)\n\n\t\t\tif strings.Contains(msgBodyLower, \"tls handshake error\") ||\n\t\t\t\tstrings.Contains(msgBodyLower, \"connection ends\") {\n\t\t\t\tlogger.Debug(\"XRAY: \" + msgBody)\n\t\t\t\tlw.lastLine = \"\"\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif strings.Contains(msgBodyLower, \"failed\") {\n\t\t\t\tlogger.Error(\"XRAY: \" + msgBody)\n\t\t\t} else {\n\t\t\t\tswitch level {\n\t\t\t\tcase \"Debug\":\n\t\t\t\t\tlogger.Debug(\"XRAY: \" + msgBody)\n\t\t\t\tcase \"Info\":\n\t\t\t\t\tlogger.Info(\"XRAY: \" + msgBody)\n\t\t\t\tcase \"Warning\":\n\t\t\t\t\tlogger.Warning(\"XRAY: \" + msgBody)\n\t\t\t\tcase \"Error\":\n\t\t\t\t\tlogger.Error(\"XRAY: \" + msgBody)\n\t\t\t\tdefault:\n\t\t\t\t\tlogger.Debug(\"XRAY: \" + msg)\n\t\t\t\t}\n\t\t\t}\n\t\t\tlw.lastLine = \"\"\n\t\t} else if msg != \"\" {\n\t\t\tmsgLower := strings.ToLower(msg)\n\n\t\t\tif strings.Contains(msgLower, \"tls handshake error\") ||\n\t\t\t\tstrings.Contains(msgLower, \"connection ends\") {\n\t\t\t\tlogger.Debug(\"XRAY: \" + msg)\n\t\t\t\tlw.lastLine = msg\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif strings.Contains(msgLower, \"failed\") {\n\t\t\t\tlogger.Error(\"XRAY: \" + msg)\n\t\t\t} else {\n\t\t\t\tlogger.Debug(\"XRAY: \" + msg)\n\t\t\t}\n\t\t\tlw.lastLine = msg\n\t\t}\n\t}\n\n\treturn len(m), nil\n}\n"
  },
  {
    "path": "xray/process.go",
    "content": "package xray\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/mhsanaei/3x-ui/v2/config\"\n\t\"github.com/mhsanaei/3x-ui/v2/logger\"\n\t\"github.com/mhsanaei/3x-ui/v2/util/common\"\n)\n\n// GetBinaryName returns the Xray binary filename for the current OS and architecture.\nfunc GetBinaryName() string {\n\treturn fmt.Sprintf(\"xray-%s-%s\", runtime.GOOS, runtime.GOARCH)\n}\n\n// GetBinaryPath returns the full path to the Xray binary executable.\nfunc GetBinaryPath() string {\n\treturn config.GetBinFolderPath() + \"/\" + GetBinaryName()\n}\n\n// GetConfigPath returns the path to the Xray configuration file in the binary folder.\nfunc GetConfigPath() string {\n\treturn config.GetBinFolderPath() + \"/config.json\"\n}\n\n// GetGeositePath returns the path to the geosite data file used by Xray.\nfunc GetGeositePath() string {\n\treturn config.GetBinFolderPath() + \"/geosite.dat\"\n}\n\n// GetGeoipPath returns the path to the geoip data file used by Xray.\nfunc GetGeoipPath() string {\n\treturn config.GetBinFolderPath() + \"/geoip.dat\"\n}\n\n// GetIPLimitLogPath returns the path to the IP limit log file.\nfunc GetIPLimitLogPath() string {\n\treturn config.GetLogFolder() + \"/3xipl.log\"\n}\n\n// GetIPLimitBannedLogPath returns the path to the banned IP log file.\nfunc GetIPLimitBannedLogPath() string {\n\treturn config.GetLogFolder() + \"/3xipl-banned.log\"\n}\n\n// GetIPLimitBannedPrevLogPath returns the path to the previous banned IP log file.\nfunc GetIPLimitBannedPrevLogPath() string {\n\treturn config.GetLogFolder() + \"/3xipl-banned.prev.log\"\n}\n\n// GetAccessPersistentLogPath returns the path to the persistent access log file.\nfunc GetAccessPersistentLogPath() string {\n\treturn config.GetLogFolder() + \"/3xipl-ap.log\"\n}\n\n// GetAccessPersistentPrevLogPath returns the path to the previous persistent access log file.\nfunc GetAccessPersistentPrevLogPath() string {\n\treturn config.GetLogFolder() + \"/3xipl-ap.prev.log\"\n}\n\n// GetAccessLogPath reads the Xray config and returns the access log file path.\nfunc GetAccessLogPath() (string, error) {\n\tconfig, err := os.ReadFile(GetConfigPath())\n\tif err != nil {\n\t\tlogger.Warningf(\"Failed to read configuration file: %s\", err)\n\t\treturn \"\", err\n\t}\n\n\tjsonConfig := map[string]any{}\n\terr = json.Unmarshal([]byte(config), &jsonConfig)\n\tif err != nil {\n\t\tlogger.Warningf(\"Failed to parse JSON configuration: %s\", err)\n\t\treturn \"\", err\n\t}\n\n\tif jsonConfig[\"log\"] != nil {\n\t\tjsonLog := jsonConfig[\"log\"].(map[string]any)\n\t\tif jsonLog[\"access\"] != nil {\n\t\t\taccessLogPath := jsonLog[\"access\"].(string)\n\t\t\treturn accessLogPath, nil\n\t\t}\n\t}\n\treturn \"\", err\n}\n\n// stopProcess calls Stop on the given Process instance.\nfunc stopProcess(p *Process) {\n\tp.Stop()\n}\n\n// Process wraps an Xray process instance and provides management methods.\ntype Process struct {\n\t*process\n}\n\n// NewProcess creates a new Xray process and sets up cleanup on garbage collection.\nfunc NewProcess(xrayConfig *Config) *Process {\n\tp := &Process{newProcess(xrayConfig)}\n\truntime.SetFinalizer(p, stopProcess)\n\treturn p\n}\n\n// NewTestProcess creates a new Xray process that uses a specific config file path.\n// Used for test runs (e.g. outbound test) so the main config.json is not overwritten.\n// The config file at configPath is removed when the process is stopped.\nfunc NewTestProcess(xrayConfig *Config, configPath string) *Process {\n\tp := &Process{newTestProcess(xrayConfig, configPath)}\n\truntime.SetFinalizer(p, stopProcess)\n\treturn p\n}\n\ntype process struct {\n\tcmd *exec.Cmd\n\n\tversion string\n\tapiPort int\n\n\tonlineClients []string\n\n\tconfig     *Config\n\tconfigPath string // if set, use this path instead of GetConfigPath() and remove on Stop\n\tlogWriter  *LogWriter\n\texitErr    error\n\tstartTime  time.Time\n}\n\n// newProcess creates a new internal process struct for Xray.\nfunc newProcess(config *Config) *process {\n\treturn &process{\n\t\tversion:   \"Unknown\",\n\t\tconfig:    config,\n\t\tlogWriter: NewLogWriter(),\n\t\tstartTime: time.Now(),\n\t}\n}\n\n// newTestProcess creates a process that writes and runs with a specific config path.\nfunc newTestProcess(config *Config, configPath string) *process {\n\tp := newProcess(config)\n\tp.configPath = configPath\n\treturn p\n}\n\n// IsRunning returns true if the Xray process is currently running.\nfunc (p *process) IsRunning() bool {\n\tif p.cmd == nil || p.cmd.Process == nil {\n\t\treturn false\n\t}\n\tif p.cmd.ProcessState == nil {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// GetErr returns the last error encountered by the Xray process.\nfunc (p *process) GetErr() error {\n\treturn p.exitErr\n}\n\n// GetResult returns the last log line or error from the Xray process.\nfunc (p *process) GetResult() string {\n\tif len(p.logWriter.lastLine) == 0 && p.exitErr != nil {\n\t\treturn p.exitErr.Error()\n\t}\n\treturn p.logWriter.lastLine\n}\n\n// GetVersion returns the version string of the Xray process.\nfunc (p *process) GetVersion() string {\n\treturn p.version\n}\n\n// GetAPIPort returns the API port used by the Xray process.\nfunc (p *Process) GetAPIPort() int {\n\treturn p.apiPort\n}\n\n// GetConfig returns the configuration used by the Xray process.\nfunc (p *Process) GetConfig() *Config {\n\treturn p.config\n}\n\n// GetOnlineClients returns the list of online clients for the Xray process.\nfunc (p *Process) GetOnlineClients() []string {\n\treturn p.onlineClients\n}\n\n// SetOnlineClients sets the list of online clients for the Xray process.\nfunc (p *Process) SetOnlineClients(users []string) {\n\tp.onlineClients = users\n}\n\n// GetUptime returns the uptime of the Xray process in seconds.\nfunc (p *Process) GetUptime() uint64 {\n\treturn uint64(time.Since(p.startTime).Seconds())\n}\n\n// refreshAPIPort updates the API port from the inbound configs.\nfunc (p *process) refreshAPIPort() {\n\tfor _, inbound := range p.config.InboundConfigs {\n\t\tif inbound.Tag == \"api\" {\n\t\t\tp.apiPort = inbound.Port\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// refreshVersion updates the version string by running the Xray binary with -version.\nfunc (p *process) refreshVersion() {\n\tcmd := exec.Command(GetBinaryPath(), \"-version\")\n\tdata, err := cmd.Output()\n\tif err != nil {\n\t\tp.version = \"Unknown\"\n\t} else {\n\t\tdatas := bytes.Split(data, []byte(\" \"))\n\t\tif len(datas) <= 1 {\n\t\t\tp.version = \"Unknown\"\n\t\t} else {\n\t\t\tp.version = string(datas[1])\n\t\t}\n\t}\n}\n\n// Start launches the Xray process with the current configuration.\nfunc (p *process) Start() (err error) {\n\tif p.IsRunning() {\n\t\treturn errors.New(\"xray is already running\")\n\t}\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Failure in running xray-core process: \", err)\n\t\t\tp.exitErr = err\n\t\t}\n\t}()\n\n\tdata, err := json.MarshalIndent(p.config, \"\", \"  \")\n\tif err != nil {\n\t\treturn common.NewErrorf(\"Failed to generate XRAY configuration files: %v\", err)\n\t}\n\n\terr = os.MkdirAll(config.GetLogFolder(), 0o770)\n\tif err != nil {\n\t\tlogger.Warningf(\"Failed to create log folder: %s\", err)\n\t}\n\n\tconfigPath := GetConfigPath()\n\tif p.configPath != \"\" {\n\t\tconfigPath = p.configPath\n\t}\n\terr = os.WriteFile(configPath, data, fs.ModePerm)\n\tif err != nil {\n\t\treturn common.NewErrorf(\"Failed to write configuration file: %v\", err)\n\t}\n\n\tcmd := exec.Command(GetBinaryPath(), \"-c\", configPath)\n\tp.cmd = cmd\n\n\tcmd.Stdout = p.logWriter\n\tcmd.Stderr = p.logWriter\n\n\tgo func() {\n\t\terr := cmd.Run()\n\t\tif err != nil {\n\t\t\t// On Windows, killing the process results in \"exit status 1\" which isn't an error for us\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\terrStr := strings.ToLower(err.Error())\n\t\t\t\tif strings.Contains(errStr, \"exit status 1\") {\n\t\t\t\t\t// Suppress noisy log on graceful stop\n\t\t\t\t\tp.exitErr = err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tlogger.Error(\"Failure in running xray-core:\", err)\n\t\t\tp.exitErr = err\n\t\t}\n\t}()\n\n\tp.refreshVersion()\n\tp.refreshAPIPort()\n\n\treturn nil\n}\n\n// Stop terminates the running Xray process.\nfunc (p *process) Stop() error {\n\tif !p.IsRunning() {\n\t\treturn errors.New(\"xray is not running\")\n\t}\n\n\t// Remove temporary config file used for test runs so main config is never touched\n\tif p.configPath != \"\" {\n\t\tif p.configPath != GetConfigPath() {\n\t\t\t// Check if file exists before removing\n\t\t\tif _, err := os.Stat(p.configPath); err == nil {\n\t\t\t\t_ = os.Remove(p.configPath)\n\t\t\t}\n\t\t}\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\treturn p.cmd.Process.Kill()\n\t} else {\n\t\treturn p.cmd.Process.Signal(syscall.SIGTERM)\n\t}\n}\n\n// writeCrashReport writes a crash report to the binary folder with a timestamped filename.\nfunc writeCrashReport(m []byte) error {\n\tcrashReportPath := config.GetBinFolderPath() + \"/core_crash_\" + time.Now().Format(\"20060102_150405\") + \".log\"\n\treturn os.WriteFile(crashReportPath, m, os.ModePerm)\n}\n"
  },
  {
    "path": "xray/traffic.go",
    "content": "package xray\n\n// Traffic represents network traffic statistics for Xray connections.\n// It tracks upload and download bytes for inbound or outbound traffic.\ntype Traffic struct {\n\tIsInbound  bool\n\tIsOutbound bool\n\tTag        string\n\tUp         int64\n\tDown       int64\n}\n"
  }
]