[
  {
    "path": ".dockerignore",
    "content": "*\n!release/\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: appleboy\npatreon: # Replace with a single Patreon username\nopen_collective: gorush\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\notechie: # Replace with a single Otechie username\ncustom: ['https://www.paypal.me/appleboy46']\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: weekly\n  - package-ecosystem: gomod\n    directory: /\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/workflows/codeql.yaml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [master]\n  schedule:\n    - cron: \"30 1 * * 0\"\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    permissions:\n      # required for all workflows\n      security-events: write\n\n      # only required for workflows in private repositories\n      actions: read\n      contents: read\n\n    strategy:\n      fail-fast: false\n      matrix:\n        # Override automatic language detection by changing the below list\n        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']\n        # TODO: Enable for javascript later\n        language: [\"go\"]\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v4\n        with:\n          languages: ${{ matrix.language }}\n          # If you wish to specify custom queries, you can do so here or in a config file.\n          # By default, queries listed here will override any specified in a config file.\n          # Prefix the list here with \"+\" to use these queries and those in the config file.\n          # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/docker.yaml",
    "content": "name: Docker Image\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - \"v*\"\n  pull_request:\n    branches:\n      - \"master\"\n\njobs:\n  build-docker:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\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 binary\n        run: |\n          make build_linux_amd64\n          make build_linux_arm\n          make build_linux_arm64\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\n      - name: Login to Docker Hub\n        if: github.event_name != 'pull_request'\n        uses: docker/login-action@v4\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        if: github.event_name != 'pull_request'\n        uses: docker/login-action@v4\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Docker meta\n        id: docker-meta\n        uses: docker/metadata-action@v6\n        with:\n          images: |\n            ${{ github.repository }}\n            ghcr.io/${{ github.repository }}\n          tags: |\n            type=raw,value=latest,enable={{is_default_branch}}\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n\n      - name: Build and push\n        uses: docker/build-push-action@v7\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm,linux/arm64\n          file: docker/Dockerfile\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.docker-meta.outputs.tags }}\n          labels: ${{ steps.docker-meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "content": "name: Goreleaser\n\non:\n  push:\n    tags:\n      - \"*\"\n\npermissions:\n  contents: write\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Setup go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          # either 'goreleaser' (default) or 'goreleaser-pro'\n          distribution: goreleaser\n          version: latest\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/testing.yml",
    "content": "name: Run Lint and Testing\n\non:\n  push:\n\njobs:\n  lint:\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: check golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.7\n          args: --verbose\n\n      - uses: hadolint/hadolint-action@v3.3.0\n        name: hadolint for Dockerfile\n        with:\n          dockerfile: docker/Dockerfile\n\n  testing:\n    runs-on: ubuntu-latest\n    container: node:16-bullseye\n\n    # Service containers to run with `container-job`\n    services:\n      # Label used to access the service container\n      redis:\n        # Docker Hub image\n        image: redis\n        # Set health checks to wait until redis has started\n        options: >-\n          --health-cmd \"redis-cli ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\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: testing\n        env:\n          FCM_CREDENTIAL: ${{ secrets.FCM_CREDENTIAL }}\n          FCM_TEST_TOKEN: ${{ secrets.FCM_TEST_TOKEN }}\n        run: make test\n\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v5\n        with:\n          flags: ${{ matrix.os }},go-${{ matrix.go }}\n"
  },
  {
    "path": ".github/workflows/trivy-scan.yml",
    "content": "name: Trivy Security Scan\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n  schedule:\n    - cron: '0 2 * * *'  # Run daily at 2 AM UTC\n  workflow_dispatch:  # Allow manual triggering\n\njobs:\n  trivy-scan-repo:\n    name: Scan Repository (Filesystem)\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Run Trivy vulnerability scanner in repo mode\n        uses: aquasecurity/trivy-action@0.35.0\n        with:\n          scan-type: 'fs'\n          ignore-unfixed: true\n          format: 'table'\n          exit-code: '1'\n          severity: 'CRITICAL,HIGH,MEDIUM'\n\n  trivy-scan-dockerhub:\n    name: Scan Docker Hub Image\n    runs-on: ubuntu-latest\n    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Run Trivy vulnerability scanner (Docker Hub)\n        uses: aquasecurity/trivy-action@0.35.0\n        with:\n          image-ref: 'appleboy/gorush:latest'\n          format: 'sarif'\n          output: 'trivy-dockerhub-results.sarif'\n\n      - name: Upload Trivy scan results to GitHub Security tab (Docker Hub)\n        uses: github/codeql-action/upload-sarif@v4\n        if: always()\n        with:\n          sarif_file: 'trivy-dockerhub-results.sarif'\n\n      - name: Run Trivy vulnerability scanner (Docker Hub Table format)\n        uses: aquasecurity/trivy-action@0.35.0\n        with:\n          image-ref: 'appleboy/gorush:latest'\n          format: 'table'\n          exit-code: '1'\n          ignore-unfixed: true\n          vuln-type: 'os,library'\n          severity: 'CRITICAL,HIGH'\n\n  trivy-scan-ghcr:\n    name: Scan GHCR Image\n    runs-on: ubuntu-latest\n    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Run Trivy vulnerability scanner (GHCR)\n        uses: aquasecurity/trivy-action@0.35.0\n        with:\n          image-ref: 'ghcr.io/appleboy/gorush:latest'\n          format: 'sarif'\n          output: 'trivy-ghcr-results.sarif'\n\n      - name: Upload Trivy scan results to GitHub Security tab (GHCR)\n        uses: github/codeql-action/upload-sarif@v4\n        if: always()\n        with:\n          sarif_file: 'trivy-ghcr-results.sarif'\n\n      - name: Run Trivy vulnerability scanner (GHCR Table format)\n        uses: aquasecurity/trivy-action@0.35.0\n        with:\n          image-ref: 'ghcr.io/appleboy/gorush:latest'\n          format: 'table'\n          exit-code: '1'\n          ignore-unfixed: true\n          vuln-type: 'os,library'\n          severity: 'CRITICAL,HIGH'\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n\ngin-bin\nkey.pem\n.DS_Store\ngorush/log/*.log\ngorush.db\n.cover\n*.db*\ncoverage.txt\ndist\ncustom\nrelease\ncoverage.txt\nnode_modules\nconfig.yml\ndist/\n.idea\n.claude\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\noutput:\n  sort-order:\n    - file\nlinters:\n  default: none\n  enable:\n    - bidichk\n    - bodyclose\n    - depguard\n    - errcheck\n    - forbidigo\n    - gocheckcompilerdirectives\n    - gocritic\n    - govet\n    - ineffassign\n    - mirror\n    - modernize\n    - nakedret\n    - nilnil\n    - nolintlint\n    - perfsprint\n    - revive\n    - staticcheck\n    - testifylint\n    - unconvert\n    - unparam\n    - unused\n    - usestdlibvars\n    - usetesting\n    - wastedassign\n  settings:\n    depguard:\n      rules:\n        main:\n          deny:\n            - pkg: io/ioutil\n              desc: use os or io instead\n            - pkg: golang.org/x/exp\n              desc: it's experimental and unreliable\n            - pkg: github.com/pkg/errors\n              desc: use builtin errors package instead\n    nolintlint:\n      allow-unused: false\n      require-explanation: true\n      require-specific: true\n    gocritic:\n      enabled-checks:\n        - equalFold\n      disabled-checks: []\n    revive:\n      severity: error\n      rules:\n        - name: blank-imports\n        - name: constant-logical-expr\n        - name: context-as-argument\n        - name: context-keys-type\n        - name: dot-imports\n        - name: empty-lines\n        - name: error-return\n        - name: error-strings\n        - name: exported\n        - name: identical-branches\n        - name: if-return\n        - name: increment-decrement\n        - name: modifies-value-receiver\n        - name: package-comments\n        - name: redefines-builtin-id\n        - name: superfluous-else\n        - name: time-naming\n        - name: unexported-return\n        - name: var-declaration\n        - name: var-naming\n          disabled: true\n    staticcheck:\n      checks:\n        - all\n    testifylint: {}\n    usetesting:\n      os-temp-dir: true\n    perfsprint:\n      concat-loop: false\n    govet:\n      enable:\n        - nilness\n        - unusedwrite\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      - linters:\n          - errcheck\n          - staticcheck\n          - unparam\n        path: _test\\.go\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - gofmt\n    - gofumpt\n    - golines\n  settings:\n    gofumpt:\n      extra-rules: true\n  exclusions:\n    generated: lax\nrun:\n  timeout: 10m\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "version: 2\n\nbefore:\n  hooks:\n    - go mod tidy\n\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    goos:\n      - darwin\n      - linux\n      - windows\n      - freebsd\n    goarch:\n      - \"386\"\n      - amd64\n      - arm\n      - arm64\n    goarm:\n      - \"5\"\n      - \"6\"\n      - \"7\"\n    ignore:\n      - goos: darwin\n        goarch: arm\n      - goos: darwin\n        goarch: ppc64le\n      - goos: darwin\n        goarch: s390x\n      - goos: windows\n        goarch: ppc64le\n      - goos: windows\n        goarch: s390x\n      - goos: windows\n        goarch: arm\n        goarm: \"5\"\n      - goos: windows\n        goarch: arm\n        goarm: \"6\"\n      - goos: windows\n        goarch: arm\n        goarm: \"7\"\n      - goos: windows\n        goarch: arm64\n      - goos: freebsd\n        goarch: ppc64le\n      - goos: freebsd\n        goarch: s390x\n      - goos: freebsd\n        goarch: arm\n        goarm: \"5\"\n      - goos: freebsd\n        goarch: arm\n        goarm: \"6\"\n      - goos: freebsd\n        goarch: arm\n        goarm: \"7\"\n      - goos: freebsd\n        goarch: arm64\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w\n      - -X main.version={{.Version}}\n      - -X main.commit={{.ShortCommit}}\n    binary: >-\n      {{ .ProjectName }}-\n      {{- if .IsSnapshot }}{{ .Branch }}-\n      {{- else }}{{- .Version }}-{{ end }}\n      {{- .Os }}-\n      {{- if eq .Arch \"amd64\" }}amd64\n      {{- else if eq .Arch \"amd64_v1\" }}amd64\n      {{- else if eq .Arch \"386\" }}386\n      {{- else }}{{ .Arch }}{{ end }}\n      {{- if .Arm }}-{{ .Arm }}{{ end }}\n    no_unique_dist_dir: true\n\narchives:\n  - format: binary\n    name_template: \"{{ .Binary }}\"\n    allow_different_binary_count: true\n\nchecksum:\n  name_template: \"checksums.txt\"\n\nsnapshot:\n  version_template: \"{{ incpatch .Version }}\"\n\nchangelog:\n  use: github\n  groups:\n    - title: Features\n      regexp: \"^.*feat[(\\\\w)]*:+.*$\"\n      order: 0\n    - title: \"Bug fixes\"\n      regexp: \"^.*fix[(\\\\w)]*:+.*$\"\n      order: 1\n    - title: \"Enhancements\"\n      regexp: \"^.*chore[(\\\\w)]*:+.*$\"\n      order: 2\n    - title: \"Refactor\"\n      regexp: \"^.*refactor[(\\\\w)]*:+.*$\"\n      order: 3\n    - title: \"Build process updates\"\n      regexp: ^.*?(build|ci)(\\(.+\\))??!?:.+$\n      order: 4\n    - title: \"Documentation updates\"\n      regexp: ^.*?docs?(\\(.+\\))??!?:.+$\n      order: 4\n    - title: Others\n      order: 999\n"
  },
  {
    "path": ".hadolint.yaml",
    "content": "ignored:\n  - DL3018\n"
  },
  {
    "path": ".roomodes",
    "content": "customModes:\n  - slug: go-code-tester\n    name: 🧪 Go Code Tester\n    description: Go testing and quality expert\n    roleDefinition: >-\n      You are Roo, a Golang testing and quality assurance expert specializing in Go testing ecosystem. Your expertise includes:\n      - Writing Go unit tests using the standard testing package\n      - Table-driven tests and subtests in Go\n      - Go benchmarks and performance testing\n      - Test coverage analysis with go test -cover\n      - Mock generation and testing with gomock, testify/mock\n      - Integration testing for Go web services and APIs\n      - Testing Go HTTP handlers and middleware\n      - Go race condition detection with go test -race\n      - Testing Go concurrency and goroutines\n      - Go fuzz testing (go test -fuzz)\n      - Testcontainers for Go integration testing\n      - Go testing best practices and conventions\n    whenToUse: >-\n      Use this mode when you need to write Go tests, improve test coverage, debug test failures,\n      set up Go testing frameworks, create test automation for Go projects, or ensure code quality through\n      comprehensive Go testing strategies. Perfect for TDD workflows in Go, bug hunting, and\n      establishing robust testing pipelines for Go applications.\n    groups:\n      - read\n      - edit\n      - command\n      - mcp\n    customInstructions: >-\n      Focus on creating comprehensive, maintainable Go tests that follow Go testing conventions.\n      Always consider edge cases, error conditions, and boundary value testing.\n      When writing Go tests, ensure they:\n      - Follow Go naming conventions (TestXxx functions)\n      - Use table-driven tests for multiple test cases\n      - Leverage t.Run() for subtests when appropriate\n      - Include proper error handling and assertions\n      - Use testify/assert or require for cleaner assertions\n      - Follow the AAA pattern (Arrange, Act, Assert)\n      - Include benchmarks for performance-critical code\n      - Use build tags for integration tests when needed\n\n      Prefer Go standard library testing package with minimal dependencies.\n      Use descriptive test function names that clearly explain the scenario being tested.\n      Always run tests with go test -v -race -cover for comprehensive validation.\n\n  - slug: go-code-reviewer\n    name: 🔍 Go Code Reviewer\n    description: Go code review and quality expert\n    roleDefinition: >-\n      You are Roo, a Go code review expert specializing in code quality, performance, and best practices. Your expertise includes:\n      - Go code style and formatting analysis (gofmt, golint, golangci-lint)\n      - Performance optimization and memory efficiency review\n      - Concurrency and goroutine safety analysis\n      - Error handling patterns and best practices\n      - Code security vulnerability assessment\n      - Go idioms and design patterns evaluation\n      - API design and interface recommendations\n      - Dependency management and module structure review\n      - Code maintainability and readability assessment\n      - Go standard library usage optimization\n      - Race condition detection and prevention\n      - Memory leak identification and prevention\n      - Code complexity analysis and refactoring suggestions\n      - Documentation and comment quality evaluation\n    whenToUse: >-\n      Use this mode when you need to review Go code for quality, performance, security, or maintainability issues.\n      Perfect for code reviews, pull request analysis, refactoring guidance, performance optimization,\n      security audits, and ensuring Go best practices compliance. Ideal for identifying potential bugs,\n      improving code structure, and mentoring developers on Go coding standards.\n    groups:\n      - read\n      - - edit\n        - fileRegex: \\.go$\n          description: Go source files only\n      - command\n      - mcp\n    customInstructions: >-\n      When reviewing Go code, focus on:\n\n      CODE QUALITY:\n      - Follow Go coding conventions and style guidelines\n      - Check proper error handling patterns (avoid ignoring errors)\n      - Ensure proper variable and function naming (camelCase, exported vs unexported)\n      - Verify correct use of Go idioms and patterns\n      - Assess code readability and maintainability\n\n      PERFORMANCE:\n      - Identify unnecessary memory allocations\n      - Review string concatenation patterns (prefer strings.Builder for multiple concatenations)\n      - Check for efficient slice and map usage\n      - Analyze goroutine usage and potential leaks\n      - Review context usage in long-running operations\n\n      SECURITY:\n      - Check for SQL injection vulnerabilities\n      - Review input validation and sanitization\n      - Identify potential race conditions\n      - Check for proper secrets handling\n      - Review error message information leakage\n\n      CONCURRENCY:\n      - Verify proper channel usage and closing\n      - Check for goroutine leaks and proper cleanup\n      - Review mutex usage and deadlock prevention\n      - Analyze shared state access patterns\n      - Ensure proper context propagation\n\n      ARCHITECTURE:\n      - Review package structure and dependencies\n      - Check interface usage and abstraction levels\n      - Assess separation of concerns\n      - Review error types and custom error handling\n      - Evaluate API design and backwards compatibility\n\n      Always provide specific, actionable feedback with code examples when suggesting improvements.\n      Prioritize critical issues (security, correctness) over style preferences.\n      Use go vet, golangci-lint, and other static analysis tools when available.\n\n  - slug: go-code-developer\n    name: 🚀 Go Code Developer\n    description: Go development and implementation expert\n    roleDefinition: >-\n      You are Roo, a Go development expert specializing in writing high-quality, idiomatic Go code. Your expertise includes:\n      - Go syntax, language features, and standard library mastery\n      - Writing clean, readable, and maintainable Go code\n      - Go modules and dependency management\n      - Implementing Go interfaces and struct design\n      - Goroutines, channels, and concurrent programming patterns\n      - Error handling best practices and custom error types\n      - Go HTTP server development with net/http\n      - JSON/XML marshaling and unmarshaling\n      - Database integration with SQL drivers and ORMs\n      - Command-line application development with flag package\n      - Go build system, cross-compilation, and deployment\n      - Context usage for cancellation and timeouts\n      - Go generics and type parameters (Go 1.18+)\n      - File I/O and system programming in Go\n      - Go toolchain usage (go fmt, go vet, go mod, etc.)\n    whenToUse: >-\n      Use this mode when you need to write, implement, or refactor Go code. Perfect for creating new Go applications,\n      implementing features, building APIs, developing CLI tools, or any Go programming task. Ideal for code implementation,\n      algorithm development, data structure design, and building complete Go solutions from scratch.\n    groups:\n      - read\n      - edit\n      - command\n      - mcp\n    customInstructions: >-\n      When writing Go code, always follow these principles:\n\n      CODE STYLE:\n      - Follow Go conventions: use gofmt for formatting, follow naming conventions\n      - Use descriptive variable and function names (camelCase for unexported, PascalCase for exported)\n      - Keep functions small and focused on single responsibility\n      - Prefer composition over inheritance\n      - Use interfaces to define behavior, not data\n      - Write comments for exported functions and types using proper GoDoc format\n      - Use error wrapping with fmt.Errorf and %w verb\n      - Avoid global variables; prefer dependency injection\n      - Use slices and maps idiomatically\n      - Don't use if else patterns unnecessarily; prefer early returns\n\n      ERROR HANDLING:\n      - Always handle errors explicitly, never ignore them\n      - Use custom error types when appropriate\n      - Wrap errors with context using fmt.Errorf with %w verb\n      - Return errors as the last return value\n      - Use errors.Is() and errors.As() for error checking\n\n      CONCURRENCY:\n      - Use goroutines for concurrent operations\n      - Employ channels for communication between goroutines\n      - Always close channels when done sending\n      - Use context.Context for cancellation and timeouts\n      - Avoid shared mutable state, prefer message passing\n\n      PERFORMANCE:\n      - Use string builders for multiple string concatenations\n      - Preallocate slices and maps when size is known\n      - Avoid unnecessary allocations in hot paths\n      - Use sync.Pool for object reuse in high-frequency scenarios\n      - Profile code when performance is critical\n\n      STANDARD PRACTICES:\n      - Use go modules for dependency management\n      - Write self-documenting code with clear function signatures\n      - Leverage the standard library before adding external dependencies\n      - Use build tags for conditional compilation\n      - Implement proper logging with structured logging when needed\n\n      Always write idiomatic Go code that is simple, readable, and efficient.\n      Use Go's built-in tools like go fmt, go vet, and go test to maintain code quality.\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nGorush is a push notification microserver written in Go that supports sending notifications to iOS (APNS), Android (FCM), and Huawei (HMS) devices. It provides both HTTP REST API and gRPC interfaces.\n\n## Architecture\n\n### Request Flow\n\n1. **main.go** parses CLI flags, loads config, initializes platform clients (APNS/FCM/HMS), then starts HTTP (Gin) and gRPC servers via `graceful.Manager`\n2. **router/** receives push requests at `POST /api/push`, validates them, and enqueues `PushNotification` messages into the queue\n3. **app/worker.go** creates the queue worker (local/NSQ/NATS/Redis) with `notify.Run(cfg)` as the processing function\n4. **notify/** dispatches notifications to platform-specific push functions (`PushToIOS`, `PushToAndroid`, `PushToHuawei`)\n5. **status/** + **storage/** track success/failure counts across configurable backends\n\n### Key Packages\n\n- **app/**: Application orchestration — CLI send helpers (`sender.go`), config validation/merge (`config.go`), CLI options (`options.go`), queue worker creation (`worker.go`)\n- **config/**: YAML config with Viper, env var overrides. Reference config: `config/testdata/config.yml`\n- **core/**: Shared types and interfaces — `Platform` enum, queue engine constants (`core/queue.go`), storage interface (`core/storage.go`), health check interface (`core/health.go`)\n- **notify/**: Platform-specific push implementations. Each platform has its own file (`notification_apns.go`, `notification_fcm.go`, `notification_hms.go`). `global.go` holds shared client state\n- **router/**: Gin HTTP server with REST endpoints and Prometheus metrics\n- **rpc/**: gRPC server. Proto definitions in `rpc/proto/`\n- **storage/**: Storage backends (memory, Redis, BoltDB, BuntDB, LevelDB, BadgerDB) all implement `core.Storage`\n- **logx/**: Logging utilities wrapping zerolog/logrus\n\n## Development Commands\n\n### Build and Run\n\n```bash\nmake build                  # Build binary to release/gorush\nmake install                # Install to $GOPATH/bin\nmake dev                    # Hot reload with air\n```\n\n### Testing\n\n```bash\n# Full test suite (requires FCM credentials)\nexport FCM_CREDENTIAL=\"/path/to/firebase-credentials.json\"\nexport FCM_TEST_TOKEN=\"your_test_device_token\"\nmake test\n\n# Run a single test\ngo test -v -tags sqlite -run TestFunctionName ./package/...\n\n# Run tests for a specific package\ngo test -v -tags sqlite ./notify/...\ngo test -v -tags sqlite ./config/...\n```\n\nThe `-tags sqlite` flag is required for all test commands (it's the default build tag).\n\n### Linting and Formatting\n\n```bash\nmake lint                   # Run golangci-lint (auto-installs if missing)\nmake fmt                    # Format code with golangci-lint fmt\n```\n\nLinter config is in `.golangci.yml` (v2 format). Uses golangci-lint v2.\n\n### Protocol Buffers\n\n```bash\nmake generate_proto         # Generate both Go and JS proto files\n```\n\n## Build Tags\n\n- `sqlite` — default tag, required for standard builds and tests\n- `lambda` — for AWS Lambda builds (replaces sqlite)\n- Set custom tags via `TAGS` environment variable\n\n## Configuration\n\nConfig uses Viper with YAML files. Key sections: `core`, `grpc`, `android`, `ios`, `huawei`, `queue`, `stat`, `log`, `api`. See `config/testdata/config.yml` for all options. Environment variables can override any config value.\n\n## API Endpoints\n\n- `POST /api/push` — send push notifications\n- `GET /api/stat/go` — Go runtime stats\n- `GET /api/stat/app` — push statistics\n- `GET /api/config` — current config\n- `GET /metrics` — Prometheus metrics\n- `GET /healthz` — health check\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Bo-Yi Wu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "EXECUTABLE := gorush\nGO ?= go\nGOFILES := $(shell find . -name \"*.go\" -type f)\nTAGS ?= sqlite\nLDFLAGS ?= -X main.version=$(VERSION) -X main.commit=$(COMMIT)\n\nPROTOC_GEN_GO=v1.36.6\nPROTOC_GEN_GO_GRPC=v1.5.1\n\nifneq ($(shell uname), Darwin)\n\tEXTLDFLAGS = -extldflags \"-static\" $(null)\nelse\n\tEXTLDFLAGS =\nendif\n\nifneq ($(DRONE_TAG),)\n\tVERSION ?= $(DRONE_TAG)\nelse\n\tVERSION ?= $(shell git describe --tags --always || git rev-parse --short HEAD)\nendif\n\nCOMMIT ?= $(shell git rev-parse --short HEAD)\n\nall: build\n\n.PHONY: help\nhelp: ## Print this help message.\n\t@echo \"Usage: make [target]\"\n\t@echo \"\"\n\t@echo \"Targets:\"\n\t@echo \"\"\n\t@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-30s\\033[0m %s\\n\", $$1, $$2}'\n\ninit: ## check the FCM_CREDENTIAL and FCM_TEST_TOKEN\n\t@echo \"==> Check FCM_CREDENTIAL and FCM_TEST_TOKEN\"\nifeq ($(FCM_CREDENTIAL),)\n\t@echo \"Missing FCM_CREDENTIAL Parameter\"\n\t@exit 1\nendif\nifeq ($(FCM_TEST_TOKEN),)\n\t@echo \"Missing FCM_TEST_TOKEN Parameter\"\n\t@exit 1\nendif\n\t@echo \"Already set FCM_CREDENTIAL and endif global variable.\"\n\n.PHONY: install ## Install the gorush binary\ninstall: $(GOFILES)\n\t$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'\n\t@echo \"\\n==>\\033[32m Installed gorush to ${GOPATH}/bin/gorush\\033[m\"\n\n.PHONY: build ## Build the gorush binary\nbuild: $(EXECUTABLE)\n\n.PHONY: $(EXECUTABLE)\n$(EXECUTABLE): $(GOFILES)\n\t$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/$@\n\n.PHONY: test ## Run the tests\ntest: init\n\t@$(GO) test -v -cover -tags $(TAGS) -coverprofile coverage.txt ./... && echo \"\\n==>\\033[32m Ok\\033[m\\n\" || exit 1\n\nbuild_linux_amd64: ## build the gorush binary for linux amd64\n\tCGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/amd64/$(EXECUTABLE)\n\nbuild_linux_i386: ## build the gorush binary for linux i386\n\tCGO_ENABLED=0 GOOS=linux GOARCH=386 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/i386/$(EXECUTABLE)\n\nbuild_linux_arm64: ## build the gorush binary for linux arm64\n\tCGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm64/$(EXECUTABLE)\n\nbuild_linux_arm: ## build the gorush binary for linux arm\n\tCGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm/$(EXECUTABLE)\n\nbuild_linux_lambda: ## build the gorush binary for linux lambda\n\tCGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags 'lambda' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/lambda/$(EXECUTABLE)\n\nbuild_darwin_amd64: ## build the gorush binary for darwin amd64\n\tCGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/amd64/$(EXECUTABLE)\n\nbuild_darwin_i386: ## build the gorush binary for darwin i386\n\tCGO_ENABLED=0 GOOS=darwin GOARCH=386 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/i386/$(EXECUTABLE)\n\nbuild_darwin_arm64: ## build the gorush binary for darwin arm64\n\tCGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/arm64/$(EXECUTABLE)\n\nbuild_darwin_arm: ## build the gorush binary for darwin arm\n\tCGO_ENABLED=0 GOOS=darwin GOARCH=arm GOARM=7 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/arm/$(EXECUTABLE)\n\nbuild_darwin_lambda: ## build the gorush binary for darwin lambda\n\tCGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -tags 'lambda' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/lambda/$(EXECUTABLE)\n\nclean: ## Clean the build\n\t$(GO) clean -modcache -x -i ./...\n\tfind . -name coverage.txt -delete\n\tfind . -name *.tar.gz -delete\n\tfind . -name *.db -delete\n\t-rm -rf release dist .cover\n\n.PHONY: proto_install\nproto_install: ## install the protoc-gen-go and protoc-gen-go-grpc\n\t$(GO) install google.golang.org/protobuf/cmd/protoc-gen-go@$(PROTOC_GEN_GO)\n\t$(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$(PROTOC_GEN_GO_GRPC)\n\ngenerate_proto_js: ## generate the proto file for nodejs\n\tnpm install grpc-tools\n\tprotoc -I rpc/proto rpc/proto/gorush.proto --js_out=import_style=commonjs,binary:rpc/example/node/ --grpc_out=rpc/example/node/ --plugin=protoc-gen-grpc=\"node_modules/.bin/grpc_tools_node_protoc_plugin\"\n\ngenerate_proto_go: ## generate the proto file for golang\n\tprotoc -I rpc/proto rpc/proto/gorush.proto --go_out=rpc/proto --go-grpc_out=require_unimplemented_servers=false:rpc/proto\n\ngenerate_proto: generate_proto_go generate_proto_js\n\n.PHONY: air\nair: ## install air for hot reload\n\t@hash air > /dev/null 2>&1; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install github.com/cosmtrek/air@latest; \\\n\tfi\n\n.PHONY: dev ## run the air for hot reload\ndev: air\n\tair --build.cmd \"make\" --build.bin release/gorush\n\nversion: ## print the version\n\t@echo $(VERSION)\n\n.PHONY: lint\nlint: ## Run golangci-lint\n\t@hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; \\\n\tfi\n\tgolangci-lint run ./...\n\n.PHONY: fmt\nfmt: ## Format code using golangci-lint\n\t@hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; \\\n\tfi\n\tgolangci-lint fmt ./...\n"
  },
  {
    "path": "README.md",
    "content": "# gorush\n\nA push notification micro server using [Gin](https://github.com/gin-gonic/gin) framework written in Go (Golang) and see the [demo app](https://github.com/appleboy/flutter-gorush).\n\n[![Run Lint and Testing](https://github.com/appleboy/gorush/actions/workflows/testing.yml/badge.svg)](https://github.com/appleboy/gorush/actions/workflows/testing.yml)\n[![Trivy Security Scan](https://github.com/appleboy/gorush/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/appleboy/gorush/actions/workflows/trivy-daily-scan.yml)\n[![GoDoc](https://godoc.org/github.com/appleboy/gorush?status.svg)](https://pkg.go.dev/github.com/appleboy/gorush)\n[![codecov](https://codecov.io/gh/appleboy/gorush/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/gorush)\n[![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/gorush)](https://goreportcard.com/report/github.com/appleboy/gorush)\n[![Docker Pulls](https://img.shields.io/docker/pulls/appleboy/gorush.svg)](https://hub.docker.com/r/appleboy/gorush/)\n[![Netlify Status](https://api.netlify.com/api/v1/badges/8ab14c9f-44fd-4d9a-8bba-f73f76d253b1/deploy-status)](https://app.netlify.com/sites/gorush/deploys)\n[![Financial Contributors on Open Collective](https://opencollective.com/gorush/all/badge.svg?label=financial+contributors)](https://opencollective.com/gorush)\n\n## Quick Start\n\nGet started with gorush in 3 simple steps:\n\n```bash\n# 1. Download the latest binary\nwget https://github.com/appleboy/gorush/releases/download/v1.18.9/gorush-1.18.9-linux-amd64 -O gorush\nchmod +x gorush\n\n# 2. Start the server (default port 8088)\n./gorush\n\n# 3. Send your first notification\ncurl -X POST http://localhost:8088/api/push \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"notifications\": [{\n      \"tokens\": [\"your_device_token\"],\n      \"platform\": 2,\n      \"title\": \"Hello World\",\n      \"message\": \"Your first notification!\"\n    }]\n  }'\n```\n\n## Contents\n\n- [Quick Start](#quick-start) - Get up and running in 3 steps\n- [Support Platform](#support-platform) - iOS, Android, Huawei\n- [Features](#features) - What gorush can do\n- [Configuration](#configuration) - YAML config and options\n  - [Basic Configuration](#basic-configuration)\n  - [Advanced Configuration](#advanced-configuration)\n- [Installation](#installation) - Binary, package managers, Docker, source\n  - [Recommended: Install Script](#recommended-install-script)\n  - [Manual Download](#manual-download)\n  - [Package Managers](#package-managers)\n  - [Build from Source](#build-from-source)\n  - [Docker](#docker)\n- [Usage](#usage) - CLI commands and REST API examples\n  - [Starting the Server](#starting-the-server)\n  - [Command Line Notifications](#command-line-notifications)\n  - [REST API Usage](#rest-api-usage)\n  - [CLI Options Reference](#cli-options-reference)\n- [Web API](#web-api) - Complete API reference\n  - [Overview](#overview)\n  - [Send Notifications](#send-notifications---post-apipush)\n  - [Statistics APIs](#statistics-apis)\n  - [Request Parameters](#request-parameters)\n- [Deployment](#deployment) - Docker, Kubernetes, AWS Lambda, gRPC\n  - [Docker Deployment](#docker-deployment)\n  - [Kubernetes](#kubernetes)\n  - [AWS Lambda](#aws-lambda)\n  - [Netlify Functions](#netlify-functions)\n  - [gRPC Service](#grpc-service)\n- [FAQ](#faq) - Common issues and best practices\n  - [Common Issues](#common-issues)\n  - [Performance Tips](#performance-tips)\n  - [Security Best Practices](#security-best-practices)\n- [Stargazers over time](#stargazers-over-time)\n- [License](#license)\n\n## Support Platform\n\n📱 Platform codes: `1` = iOS (APNS), `2` = Android (FCM), `3` = Huawei (HMS)\n\n- [APNS](https://developer.apple.com/documentation/usernotifications)\n- [FCM](https://firebase.google.com/)\n- [HMS](https://developer.huawei.com/consumer/en/hms/)\n\n[A live server on Netlify](https://gorush.netlify.app/) and get notification token on [Firebase Cloud Messaging web](https://fcm-demo-88b40.web.app/). You can use the token to send a notification to the device.\n\n```bash\ncurl -X POST \\\n     -H \"Content-Type: application/json\" \\\n     -d '{\n  \"notifications\": [\n    {\n      \"tokens\": [\n        \"your_device_token\"\n      ],\n      \"platform\": 2,\n      \"title\": \"Test Title\",\n      \"message\": \"Test Message\"\n    }\n  ]\n}' \\\n  https://gorush.netlify.app/api/push\n```\n\n## Features\n\n- Support [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) using [go-fcm](https://github.com/appleboy/go-fcm) library for Android.\n- Support [HTTP/2](https://http2.github.io/) Apple Push Notification Service using [apns2](https://github.com/sideshow/apns2) library.\n- Support [HMS Push Service](https://developer.huawei.com/consumer/en/hms/huawei-pushkit) using [go-hms-push](https://github.com/msalihkarakasli/go-hms-push) library for Huawei Devices.\n- Support [YAML](https://github.com/go-yaml/yaml) configuration.\n- Support command line to send single Android or iOS notification.\n- Support Web API to send push notification.\n- Support [HTTP/2](https://http2.github.io/) or HTTP/1.1 protocol.\n- Support notification queue and multiple workers.\n- Support `/api/stat/app` show notification success and failure counts.\n- Support `/api/config` show your [YAML](https://en.wikipedia.org/wiki/YAML) config.\n- Support store app stat to memory, [Redis](http://redis.io/), [BoltDB](https://github.com/boltdb/bolt), [BuntDB](https://github.com/tidwall/buntdb), [LevelDB](https://github.com/syndtr/goleveldb) or [BadgerDB](https://github.com/dgraph-io/badger).\n- Support `p8`, `p12` or `pem` format of iOS certificate file.\n- Support `/sys/stats` show response time, status code count, etc.\n- Support for HTTP, HTTPS or SOCKS5 proxy.\n- Support retry send notification if server response is fail.\n- Support expose [prometheus](https://prometheus.io/) metrics.\n- Support install TLS certificates from [Let's Encrypt](https://letsencrypt.org/) automatically.\n- Support send notification through [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) protocol, we use [gRPC](https://grpc.io/) as default framework.\n- Support running in Docker, [Kubernetes](https://kubernetes.io/) or [AWS Lambda](https://aws.amazon.com/lambda) ([Native Support in Golang](https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/))\n- Support graceful shutdown that workers and queue have been sent to APNs/FCM before shutdown service.\n- Support different Queue as backend like [NSQ](https://nsq.io/), [NATS](https://nats.io/) or [Redis streams](https://redis.io/docs/manual/data-types/streams/), default engine is local [Channel](https://tour.golang.org/concurrency/2).\n\n**Performance**: Average memory usage ~28MB. Supports high-throughput notification delivery with configurable workers and queue systems.\n\n## Configuration\n\nGorush uses YAML configuration. Create a `config.yml` file with your settings:\n\n### Basic Configuration\n\n```yaml\ncore:\n  port: \"8088\" # HTTP server port\n  worker_num: 0 # Workers (0 = CPU cores)\n  queue_num: 8192 # Queue size\n  mode: \"release\" # or \"debug\"\n\n# Enable platforms you need\nandroid:\n  enabled: true\n  key_path: \"fcm-key.json\" # FCM service account key\n\nios:\n  enabled: true\n  key_path: \"apns-key.pem\" # APNS certificate\n  production: true # Use production APNS\n\nhuawei:\n  enabled: false\n  appid: \"YOUR_APP_ID\"\n  appsecret: \"YOUR_APP_SECRET\"\n```\n\n### Advanced Configuration\n\n<details>\n<summary>Click to expand full configuration options</summary>\n\n```yaml\ncore:\n  enabled: true\n  address: \"\"\n  shutdown_timeout: 30\n  port: \"8088\"\n  worker_num: 0\n  queue_num: 0\n  max_notification: 100\n  sync: false\n  feedback_hook_url: \"\"\n  feedback_timeout: 10\n  feedback_header:\n  mode: \"release\"\n  ssl: false\n  cert_path: \"cert.pem\"\n  key_path: \"key.pem\"\n  cert_base64: \"\"\n  key_base64: \"\"\n  http_proxy: \"\"\n  pid:\n    enabled: false\n    path: \"gorush.pid\"\n    override: true\n  auto_tls:\n    enabled: false\n    folder: \".cache\"\n    host: \"\"\n\ngrpc:\n  enabled: false\n  port: 9000\n\napi:\n  push_uri: \"/api/push\"\n  stat_go_uri: \"/api/stat/go\"\n  stat_app_uri: \"/api/stat/app\"\n  config_uri: \"/api/config\"\n  sys_stat_uri: \"/sys/stats\"\n  metric_uri: \"/metrics\"\n  health_uri: \"/healthz\"\n\nandroid:\n  enabled: true\n  key_path: \"\"\n  credential: \"\"\n  max_retry: 0\n\nhuawei:\n  enabled: false\n  appsecret: \"YOUR_APP_SECRET\"\n  appid: \"YOUR_APP_ID\"\n  max_retry: 0\n\nqueue:\n  engine: \"local\"\n  nsq:\n    addr: 127.0.0.1:4150\n    topic: gorush\n    channel: gorush\n  nats:\n    addr: 127.0.0.1:4222\n    subj: gorush\n    queue: gorush\n  redis:\n    addr: 127.0.0.1:6379\n    group: gorush\n    consumer: gorush\n    stream_name: gorush\n    with_tls: false\n    username: \"\"\n    password: \"\"\n    db: 0\n\nios:\n  enabled: false\n  key_path: \"\"\n  key_base64: \"\"\n  key_type: \"pem\"\n  password: \"\"\n  production: false\n  max_concurrent_pushes: 100\n  max_retry: 0\n  key_id: \"\"\n  team_id: \"\"\n\nlog:\n  format: \"string\"\n  access_log: \"stdout\"\n  access_level: \"debug\"\n  error_log: \"stderr\"\n  error_level: \"error\"\n  hide_token: true\n  hide_messages: false\n\nstat:\n  engine: \"memory\"\n  redis:\n    cluster: false\n    addr: \"localhost:6379\"\n    username: \"\"\n    password: \"\"\n    db: 0\n  boltdb:\n    path: \"bolt.db\"\n    bucket: \"gorush\"\n  buntdb:\n    path: \"bunt.db\"\n  leveldb:\n    path: \"level.db\"\n  badgerdb:\n    path: \"badger.db\"\n```\n\nSee the complete [example config file](config/testdata/config.yml).\n\n</details>\n\n## Installation\n\n### Recommended: Install Script\n\nThe easiest way to install gorush is using the install script:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/appleboy/gorush/master/install.sh | bash\n```\n\nThis will automatically:\n\n- Detect your OS and architecture\n- Download the latest version\n- Install to `~/.gorush/bin`\n- Add to your PATH\n\n#### Options\n\n```bash\n# Install specific version (replace X.Y.Z with the desired version, e.g., 1.19.2)\nVERSION=X.Y.Z curl -fsSL https://raw.githubusercontent.com/appleboy/gorush/master/install.sh | bash\n\n# Custom install directory\nINSTALL_DIR=/usr/local/bin curl -fsSL https://raw.githubusercontent.com/appleboy/gorush/master/install.sh | bash\n\n# Skip SSL verification (not recommended)\nINSECURE=1 curl -fsSL https://raw.githubusercontent.com/appleboy/gorush/master/install.sh | bash\n```\n\n### Manual Download\n\nDownload from [releases page](https://github.com/appleboy/gorush/releases):\n\n### Package Managers\n\n#### Homebrew (macOS/Linux)\n\n```bash\nbrew tap appleboy/tap\nbrew install gorush\n```\n\n#### Go Install\n\n```bash\n# Latest stable version\ngo install github.com/appleboy/gorush@latest\n\n# Development version\ngo install github.com/appleboy/gorush@master\n```\n\n### Build from Source\n\n**Requirements**: [Go 1.25+](https://go.dev/dl/), [Git](http://git-scm.com/)\n\n```bash\ngit clone https://github.com/appleboy/gorush.git\ncd gorush\nmake build\n# Binary will be in the root directory\n```\n\n### Docker\n\n```bash\n# Run directly\ndocker run --rm -p 8088:8088 appleboy/gorush\n\n# With custom config\ndocker run --rm -p 8088:8088 -v $(pwd)/config.yml:/home/gorush/config.yml appleboy/gorush\n```\n\n## Usage\n\n### Starting the Server\n\n```bash\n# Use default config (port 8088)\n./gorush\n\n# Use custom config file\n./gorush -c config.yml\n\n# Set specific options\n./gorush -p 9000 -c config.yml\n```\n\n### Command Line Notifications\n\n#### Android (FCM) Command Line\n\n**Prerequisites**: Generate FCM service account key from [Firebase Console](https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk) → Settings → Service Accounts → Generate New Private Key.\n\n```bash\n# Android: Single notification\ngorush -android -m \"Hello Android!\" --fcm-key \"path/to/fcm-key.json\" -t \"device_token\"\n\n# Android: Using environment variable (recommended)\nexport GOOGLE_APPLICATION_CREDENTIALS=\"path/to/fcm-key.json\"\ngorush -android -m \"Hello Android!\" -t \"device_token\"\n\n# Android: Topic message\ngorush --android --topic \"news\" -m \"Breaking News!\" --fcm-key \"path/to/fcm-key.json\"\n```\n\n#### iOS (APNS) Command Line\n\n```bash\n# iOS: Development environment\ngorush -ios -m \"Hello iOS!\" -i \"cert.pem\" -t \"device_token\" --topic \"com.example.app\"\n\n# iOS: Production environment\ngorush -ios -m \"Hello iOS!\" -i \"cert.pem\" -t \"device_token\" --topic \"com.example.app\" -production\n\n# iOS: With password-protected certificate\ngorush -ios -m \"Hello iOS!\" -i \"cert.p12\" -P \"cert_password\" -t \"device_token\"\n```\n\n#### Huawei (HMS) Command Line\n\n```bash\n# Huawei: Single notification\ngorush -huawei -title \"Hello\" -m \"Hello Huawei!\" -hk \"APP_SECRET\" -hid \"APP_ID\" -t \"device_token\"\n\n# Huawei: Topic message\ngorush --huawei --topic \"updates\" -title \"Update\" -m \"New version available\" -hk \"APP_SECRET\" -hid \"APP_ID\"\n```\n\n### REST API Usage\n\n#### Health Check\n\n```bash\ncurl http://localhost:8088/healthz\n```\n\n#### Send Notifications\n\n```bash\ncurl -X POST http://localhost:8088/api/push \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"notifications\": [{\n      \"tokens\": [\"device_token_1\", \"device_token_2\"],\n      \"platform\": 2,\n      \"title\": \"Hello World\",\n      \"message\": \"This is a test notification\"\n    }]\n  }'\n```\n\n#### Get Statistics\n\n```bash\n# Application stats\ncurl http://localhost:8088/api/stat/app\n\n# Go runtime stats\ncurl http://localhost:8088/api/stat/go\n\n# System stats\ncurl http://localhost:8088/sys/stats\n\n# Prometheus metrics\ncurl http://localhost:8088/metrics\n```\n\n### CLI Options Reference\n\n<details>\n<summary>Click to expand all CLI options</summary>\n\n```bash\nServer Options:\n    -A, --address <address>          Address to bind (default: any)\n    -p, --port <port>                Use port for clients (default: 8088)\n    -c, --config <file>              Configuration file path\n    -m, --message <message>          Notification message\n    -t, --token <token>              Notification token\n    -e, --engine <engine>            Storage engine (memory, redis ...)\n    --title <title>                  Notification title\n    --proxy <proxy>                  Proxy URL\n    --pid <pid path>                 Process identifier path\n    --redis-addr <redis addr>        Redis addr (default: localhost:6379)\n    --ping                           healthy check command for container\n\niOS Options:\n    -i, --key <file>                 certificate key file path\n    -P, --password <password>        certificate key password\n    --ios                            enabled iOS (default: false)\n    --production                     iOS production mode (default: false)\n\nAndroid Options:\n    --fcm-key <fcm_key_path>         FCM Key Path\n    --android                        enabled android (default: false)\n\nHuawei Options:\n    -hk, --hmskey <hms_key>          HMS App Secret\n    -hid, --hmsid <hms_id>           HMS App ID\n    --huawei                         enabled huawei (default: false)\n\nCommon Options:\n    --topic <topic>                  iOS, Android or Huawei topic message\n    -h, --help                       Show this message\n    -V, --version                    Show version\n```\n\n</details>\n\n## Web API\n\n### Overview\n\nGorush provides RESTful APIs for sending notifications and monitoring system status:\n\n| Endpoint        | Method | Description                |\n| --------------- | ------ | -------------------------- |\n| `/api/push`     | POST   | Send push notifications    |\n| `/api/stat/app` | GET    | Application statistics     |\n| `/api/stat/go`  | GET    | Go runtime statistics      |\n| `/sys/stats`    | GET    | System performance metrics |\n| `/metrics`      | GET    | Prometheus metrics         |\n| `/healthz`      | GET    | Health check               |\n| `/api/config`   | GET    | Current configuration      |\n\n### Send Notifications - `POST /api/push`\n\n#### Basic Examples\n\n##### iOS (APNS) API\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"ios_device_token\"],\n      \"platform\": 1,\n      \"title\": \"Hello iOS\",\n      \"message\": \"Hello World iOS!\"\n    }\n  ]\n}\n```\n\n##### Android (FCM) API\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"android_device_token\"],\n      \"platform\": 2,\n      \"title\": \"Hello Android\",\n      \"message\": \"Hello World Android!\"\n    }\n  ]\n}\n```\n\n##### Huawei (HMS) API\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"huawei_device_token\"],\n      \"platform\": 3,\n      \"title\": \"Hello Huawei\",\n      \"message\": \"Hello World Huawei!\"\n    }\n  ]\n}\n```\n\n#### Advanced Examples\n\n##### iOS with Custom Sound\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"ios_device_token\"],\n      \"platform\": 1,\n      \"title\": \"Important Alert\",\n      \"message\": \"Critical notification\",\n      \"apns\": {\n        \"payload\": {\n          \"aps\": {\n            \"sound\": {\n              \"name\": \"custom.wav\",\n              \"critical\": 1,\n              \"volume\": 0.8\n            }\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n##### Multiple Platforms\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"ios_token\"],\n      \"platform\": 1,\n      \"message\": \"Hello iOS!\"\n    },\n    {\n      \"tokens\": [\"android_token\"],\n      \"platform\": 2,\n      \"message\": \"Hello Android!\"\n    }\n  ]\n}\n```\n\n### Statistics APIs\n\n#### Application Stats - `GET /api/stat/app`\n\n```json\n{\n  \"version\": \"v1.18.9\",\n  \"busy_workers\": 0,\n  \"success_tasks\": 150,\n  \"failure_tasks\": 5,\n  \"submitted_tasks\": 155,\n  \"total_count\": 155,\n  \"ios\": {\n    \"push_success\": 80,\n    \"push_error\": 2\n  },\n  \"android\": {\n    \"push_success\": 65,\n    \"push_error\": 3\n  },\n  \"huawei\": {\n    \"push_success\": 5,\n    \"push_error\": 0\n  }\n}\n```\n\n#### System Performance - `GET /sys/stats`\n\n```json\n{\n  \"pid\": 12345,\n  \"uptime\": \"2h30m15s\",\n  \"total_response_time\": \"45.2ms\",\n  \"average_response_time\": \"1.2ms\",\n  \"total_status_code_count\": {\n    \"200\": 1450,\n    \"400\": 12,\n    \"500\": 3\n  }\n}\n```\n\n### Request Parameters\n\n<details>\n<summary>Complete API request parameters</summary>\n\n#### Request body\n\nThe Request body must have a notifications array. The following is a parameter table for each notification.\n\n| name                    | type         | description                                                                                       | required | note                                                          |\n| ----------------------- | ------------ | ------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------- |\n| notif_id                | string       | A unique string that identifies the notification for async feedback                               | -        |                                                               |\n| tokens                  | string array | device tokens                                                                                     | o        |                                                               |\n| platform                | int          | platform(iOS,Android)                                                                             | o        | 1=iOS, 2=Android (Firebase), 3=Huawei (HMS)                   |\n| message                 | string       | message for notification                                                                          | -        |                                                               |\n| title                   | string       | notification title                                                                                | -        |                                                               |\n| priority                | string       | Sets the priority of the message.                                                                 | -        | `normal` or `high`                                            |\n| content_available       | bool         | data messages wake the app by default.                                                            | -        |                                                               |\n| sound                   | interface{}  | sound type                                                                                        | -        |                                                               |\n| data                    | string array | extensible partition                                                                              | -        | only Android and IOS                                          |\n| huawei_data             | string       | JSON object as string to extensible partition partition                                           | -        | only Huawei. See the [detail](#huawei-notification)           |\n| retry                   | int          | retry send notification if fail response from server. Value must be small than `max_retry` field. | -        |                                                               |\n| topic                   | string       | send messages to topics                                                                           |          |                                                               |\n| image                   | string       | image url to show in notification                                                                 | -        | only Android and Huawei                                       |\n| to                      | string       | The value must be a registration token, notification key, or topic.                               | -        | only Android                                                  |\n| collapse_key            | string       | a key for collapsing notifications                                                                | -        | only Android                                                  |\n| huawei_collapse_key     | int          | a key integer for collapsing notifications                                                        | -        | only Huawei See the [detail](#huawei-notification)            |\n| delay_while_idle        | bool         | a flag for device idling                                                                          | -        | only Android                                                  |\n| time_to_live            | uint         | expiration of message kept on FCM storage                                                         | -        | only Android                                                  |\n| huawei_ttl              | string       | expiration of message kept on HMS storage                                                         | -        | only Huawei See the [detail](#huawei-notification)            |\n| restricted_package_name | string       | the package name of the application                                                               | -        | only Android                                                  |\n| dry_run                 | bool         | allows developers to test a request without actually sending a message                            | -        | only Android                                                  |\n| notification            | string array | payload of a FCM message                                                                          | -        | only Android. See the [detail](#android-notification-payload) |\n| huawei_notification     | string array | payload of a HMS message                                                                          | -        | only Huawei. See the [detail](#huawei-notification)           |\n| app_id                  | string       | hms app id                                                                                        | -        | only Huawei. See the [detail](#huawei-notification)           |\n| bi_tag                  | string       | Tag of a message in a batch delivery task                                                         | -        | only Huawei. See the [detail](#huawei-notification)           |\n| fast_app_target         | int          | State of a mini program when a quick app sends a data message.                                    | -        | only Huawei. See the [detail](#huawei-notification)           |\n| expiration              | int          | expiration for notification                                                                       | -        | only iOS                                                      |\n| apns_id                 | string       | A canonical UUID that identifies the notification                                                 | -        | only iOS                                                      |\n| collapse_id             | string       | An identifier you use to coalesce multiple notifications into a single notification for the user  | -        | only iOS                                                      |\n| push_type               | string       | The type of the notification. The value of this header is alert or background.                    | -        | only iOS                                                      |\n| badge                   | int          | badge count                                                                                       | -        | only iOS                                                      |\n| category                | string       | the UIMutableUserNotificationCategory object                                                      | -        | only iOS                                                      |\n| alert                   | string array | payload of a iOS message                                                                          | -        | only iOS. See the [detail](#ios-alert-payload)                |\n| mutable_content         | bool         | enable Notification Service app extension.                                                        | -        | only iOS(10.0+).                                              |\n| name                    | string       | sets the name value on the aps sound dictionary.                                                  | -        | only iOS                                                      |\n| volume                  | float32      | sets the volume value on the aps sound dictionary.                                                | -        | only iOS                                                      |\n| interruption_level      | string       | defines the interruption level for the push notification.                                         | -        | only iOS(15.0+)                                               |\n| content-state           | string array | dynamic and custom content for live-activity notification.                                        | -        | only iOS(16.1+)                                               |\n| timestamp               | int          | the UNIX time when sending the remote notification that updates or ends a Live Activity           | -        | only iOS(16.1+)                                               |\n| event                   | string       | describes whether you update or end an ongoing Live Activity                                      | -        | only iOS(16.1+)                                               |\n| stale-date              | int          | the date which a Live Activity becomes stale, or out of date                                      | -        | only iOS(16.1+)                                               |\n| dismissal-date          | int          | the UNIX time -timestamp- which a Live Activity will end and will be removed                      | -        | only iOS(16.1+)                                               |\n\n### iOS alert payload\n\n| name           | type             | description                                                                                      | required | note |\n| -------------- | ---------------- | ------------------------------------------------------------------------------------------------ | -------- | ---- |\n| title          | string           | Apple Watch & Safari display this string as part of the notification interface.                  | -        |      |\n| body           | string           | The text of the alert message.                                                                   | -        |      |\n| subtitle       | string           | Apple Watch & Safari display this string as part of the notification interface.                  | -        |      |\n| action         | string           | The label of the action button. This one is required for Safari Push Notifications.              | -        |      |\n| action-loc-key | string           | If a string is specified, the system displays an alert that includes the Close and View buttons. | -        |      |\n| launch-image   | string           | The filename of an image file in the app bundle, with or without the filename extension.         | -        |      |\n| loc-args       | array of strings | Variable string values to appear in place of the format specifiers in loc-key.                   | -        |      |\n| loc-key        | string           | A key to an alert-message string in a Localizable.strings file for the current localization.     | -        |      |\n| title-loc-args | array of strings | Variable string values to appear in place of the format specifiers in title-loc-key.             | -        |      |\n| title-loc-key  | string           | The key to a title string in the Localizable.strings file for the current localization.          | -        |      |\n\nSee more detail about [APNs Remote Notification Payload](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html).\n\n### iOS sound payload\n\n| name     | type    | description                                          | required | note |\n| -------- | ------- | ---------------------------------------------------- | -------- | ---- |\n| name     | string  | sets the name value on the aps sound dictionary.     | -        |      |\n| volume   | float32 | sets the volume value on the aps sound dictionary.   | -        |      |\n| critical | int     | sets the critical value on the aps sound dictionary. | -        |      |\n\nrequest format:\n\n```json\n{\n  \"sound\": {\n    \"critical\": 1,\n    \"name\": \"default\",\n    \"volume\": 2.0\n  }\n}\n```\n\n### Android notification payload\n\n| name           | type   | description                                                                                               | required | note |\n| -------------- | ------ | --------------------------------------------------------------------------------------------------------- | -------- | ---- |\n| icon           | string | Indicates notification icon.                                                                              | -        |      |\n| tag            | string | Indicates whether each notification message results in a new entry on the notification center on Android. | -        |      |\n| color          | string | Indicates color of the icon, expressed in #rrggbb format                                                  | -        |      |\n| click_action   | string | The action associated with a user click on the notification.                                              | -        |      |\n| body_loc_key   | string | Indicates the key to the body string for localization.                                                    | -        |      |\n| body_loc_args  | string | Indicates the string value to replace format specifiers in body string for localization.                  | -        |      |\n| title_loc_key  | string | Indicates the key to the title string for localization.                                                   | -        |      |\n| title_loc_args | string | Indicates the string value to replace format specifiers in title string for localization.                 | -        |      |\n\nSee more detail about [Firebase Cloud Messaging HTTP Protocol reference](https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream).\n\n### Huawei notification\n\n1. app_id: app id from huawei developer console\n2. bi_tag:\n3. fast_app_target:\n4. huawei_data: mapped to data\n5. huawei_notification: mapped to notification\n6. huawei_ttl: mapped to ttl\n7. huawei_collapse_key: mapped to collapse_key\n\nSee more detail about [Huawei Mobulse Services Push API reference](https://developer.huawei.com/consumer/en/doc/development/HMS-References/push-sendapi).\n\n### iOS Example\n\nSend normal notification.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n      \"message\": \"Hello World iOS!\"\n    }\n  ]\n}\n```\n\nThe following payload asks the system to display an alert with a Close button and a single action button.The title and body keys provide the contents of the alert. The “PLAY” string is used to retrieve a localized string from the appropriate Localizable.strings file of the app. The resulting string is used by the alert as the title of an action button. This payload also asks the system to badge the app’s icon with the number 5.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n      \"badge\": 5,\n      \"alert\": {\n        \"title\": \"Game Request\",\n        \"body\": \"Bob wants to play poker\",\n        \"action-loc-key\": \"PLAY\"\n      }\n    }\n  ]\n}\n```\n\nThe following payload specifies that the device should display an alert message, plays a sound, and badges the app’s icon.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n      \"message\": \"You got your emails.\",\n      \"badge\": 9,\n      \"sound\": {\n        \"critical\": 1,\n        \"name\": \"default\",\n        \"volume\": 1.0\n      }\n    }\n  ]\n}\n```\n\nAdd other fields which user defined via `data` field.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n      \"message\": \"Hello World iOS!\",\n      \"data\": {\n        \"key1\": \"welcome\",\n        \"key2\": 2\n      }\n    }\n  ]\n}\n```\n\nSupport send notification from different environment. See the detail of [issue](https://github.com/appleboy/gorush/issues/246).\n\n```diff\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n+     \"production\": true,\n      \"message\": \"Hello World iOS Production!\"\n    },\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n+     \"development\": true,\n      \"message\": \"Hello World iOS Sandbox!\"\n    }\n  ]\n}\n```\n\n### Android Example\n\nSend normal notification.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 2,\n      \"message\": \"Hello World Android!\",\n      \"title\": \"You got message\"\n    }\n  ]\n}\n```\n\nLabel associated with the message's analytics data.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 2,\n      \"message\": \"Hello World Android!\",\n      \"title\": \"You got message\",\n      \"fcm_options\": {\n        \"analytics_label\": \"example\"\n      }\n    }\n  ]\n}\n```\n\nAdd `notification` payload.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 2,\n      \"message\": \"Hello World Android!\",\n      \"title\": \"You got message\",\n      \"notification\": {\n        \"icon\": \"myicon\",\n        \"color\": \"#112244\"\n      }\n    }\n  ]\n}\n```\n\nAdd other fields which user defined via `data` field.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 2,\n      \"message\": \"Hello World Android!\",\n      \"title\": \"You got message\",\n      \"data\": {\n        \"Nick\": \"Mario\",\n        \"body\": \"great match!\",\n        \"Room\": \"PortugalVSDenmark\"\n      }\n    }\n  ]\n}\n```\n\nSend messages to topic\n\n```json\n{\n  \"notifications\": [\n    {\n      \"topic\": \"highScores\",\n      \"platform\": 2,\n      \"message\": \"This is a Firebase Cloud Messaging Topic Message\"\n    }\n  ]\n}\n```\n\n### Huawei Example\n\nSend normal notification.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 3,\n      \"message\": \"Hello World Huawei!\",\n      \"title\": \"You got message\"\n    }\n  ]\n}\n```\n\nAdd `notification` payload.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 3,\n      \"message\": \"Hello World Huawei!\",\n      \"title\": \"You got message\",\n      \"huawei_notification\": {\n        \"icon\": \"myicon\",\n        \"color\": \"#112244\"\n      }\n    }\n  ]\n}\n```\n\nAdd other fields which user defined via `huawei_data` field.\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 3,\n      \"huawei_data\": \"{'title' : 'Mario','message' : 'great match!', 'Room' : 'PortugalVSDenmark'}\"\n    }\n  ]\n}\n```\n\nSend messages to topics\n\n```json\n{\n  \"notifications\": [\n    {\n      \"topic\": \"foo-bar\",\n      \"platform\": 3,\n      \"message\": \"This is a Huawei Mobile Services Topic Message\",\n      \"title\": \"You got message\"\n    }\n  ]\n}\n```\n\n### Response body\n\nError response message table:\n\n| status code | message                                    |\n| ----------- | ------------------------------------------ |\n| 400         | Missing `notifications` field.             |\n| 400         | Notifications field is empty.              |\n| 400         | Number of notifications(50) over limit(10) |\n\nSuccess response:\n\n```json\n{\n  \"counts\": 60,\n  \"logs\": [],\n  \"success\": \"ok\"\n}\n```\n\nIf you need error logs from sending fail notifications, please set a `feedback_hook_url` and `feedback_header` for custom header. The server with send the failing logs asynchronously to your API as `POST` requests.\n\n```diff\ncore:\n  port: \"8088\" # ignore this port number if auto_tls is enabled (listen 443).\n  worker_num: 0 # default worker number is runtime.NumCPU()\n  queue_num: 0 # default queue number is 8192\n  max_notification: 100\n  sync: false\n- feedback_hook_url: \"\"\n+ feedback_hook_url: \"https://example.com/api/hook\"\n+ feedback_header:\n+   - x-gorush-token:4e989115e09680f44a645519fed6a976\n```\n\nYou can also switch to **sync** mode by setting the `sync` value as `true` on yaml config. It only works when the queue engine is local.\n\n```diff\ncore:\n  port: \"8088\" # ignore this port number if auto_tls is enabled (listen 443).\n  worker_num: 0 # default worker number is runtime.NumCPU()\n  queue_num: 0 # default queue number is 8192\n  max_notification: 100\n- sync: false\n+ sync: true\n```\n\nSee the following error format.\n\n```json\n{\n  \"counts\": 60,\n  \"logs\": [\n    {\n      \"type\": \"failed-push\",\n      \"platform\": \"android\",\n      \"token\": \"*******\",\n      \"message\": \"Hello World Android!\",\n      \"error\": \"InvalidRegistration\"\n    },\n    {\n      \"type\": \"failed-push\",\n      \"platform\": \"ios\",\n      \"token\": \"*****\",\n      \"message\": \"Hello World iOS1111!\",\n      \"error\": \"Post https://api.push.apple.com/3/device/bbbbb: remote error: tls: revoked certificate\"\n    },\n    {\n      \"type\": \"failed-push\",\n      \"platform\": \"ios\",\n      \"token\": \"*******\",\n      \"message\": \"Hello World iOS222!\",\n      \"error\": \"Post https://api.push.apple.com/3/device/token_b: remote error: tls: revoked certificate\"\n    }\n  ],\n  \"success\": \"ok\"\n}\n```\n\n## Deployment\n\n### Docker Deployment\n\n#### Docker Quick Start\n\n```bash\n# Run with default config\ndocker run --rm -p 8088:8088 appleboy/gorush\n\n# Run with custom config\ndocker run --rm -p 8088:8088 \\\n  -v $(pwd)/config.yml:/home/gorush/config.yml \\\n  appleboy/gorush\n\n# Run in background\ndocker run -d --name gorush -p 8088:8088 appleboy/gorush\n```\n\n#### Docker Compose\n\n```yaml\nversion: \"3\"\nservices:\n  gorush:\n    image: appleboy/gorush\n    ports:\n      - \"8088:8088\"\n    volumes:\n      - ./config.yml:/home/gorush/config.yml\n  redis:\n    image: redis:alpine\n    ports:\n      - \"6379:6379\"\n```\n\n### Kubernetes\n\n#### Quick Deploy\n\n```bash\n# Create namespace and config\nkubectl create -f k8s/gorush-namespace.yaml\nkubectl create -f k8s/gorush-configmap.yaml\n\n# Deploy Redis (optional, for queue/stats)\nkubectl create -f k8s/gorush-redis-deployment.yaml\nkubectl create -f k8s/gorush-redis-service.yaml\n\n# Deploy Gorush\nkubectl create -f k8s/gorush-deployment.yaml\nkubectl create -f k8s/gorush-service.yaml\n```\n\n#### AWS Load Balancer\n\nFor AWS ELB:\n\n```bash\nkubectl create -f k8s/gorush-service.yaml\n```\n\nFor AWS ALB, modify service type:\n\n```yaml\n# k8s/gorush-service.yaml\nspec:\n  type: NodePort # Change from LoadBalancer\n```\n\nThen deploy ingress:\n\n```bash\nkubectl create -f k8s/gorush-aws-alb-ingress.yaml\n```\n\n#### Cleanup\n\n```bash\nkubectl delete -f k8s/\n```\n\n### AWS Lambda\n\n#### Build and Deploy\n\n```bash\n# Build Lambda binary\ngit clone https://github.com/appleboy/gorush.git\ncd gorush\nmake build_linux_lambda\n\n# Create deployment package\nzip deployment.zip release/linux/lambda/gorush\n\n# Deploy with AWS CLI\naws lambda update-function-code \\\n  --function-name gorush \\\n  --zip-file fileb://deployment.zip\n```\n\n#### Automated Deployment\n\nUsing [drone-lambda](https://github.com/appleboy/drone-lambda):\n\n```bash\nAWS_ACCESS_KEY_ID=your_key \\\nAWS_SECRET_ACCESS_KEY=your_secret \\\ndrone-lambda --region us-west-2 \\\n  --function-name gorush \\\n  --source release/linux/lambda/gorush\n```\n\n### Netlify Functions\n\nAlternative serverless deployment without AWS:\n\n```toml\n# netlify.toml\n[build]\ncommand = \"make build_linux_lambda\"\nfunctions = \"release/linux/lambda\"\n\n[build.environment]\nGO_VERSION = \"1.24\"\n\n[[redirects]]\nfrom = \"/*\"\nstatus = 200\nto = \"/.netlify/functions/gorush/:splat\"\n```\n\n### gRPC Service\n\nEnable gRPC server for high-performance applications:\n\n```yaml\n# config.yml\ngrpc:\n  enabled: true\n  port: 9000\n```\n\nOr via environment:\n\n```bash\nGORUSH_GRPC_ENABLED=true GORUSH_GRPC_PORT=9000 gorush\n```\n\n#### gRPC Client Example (Go)\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"log\"\n    \"github.com/appleboy/gorush/rpc/proto\"\n    \"google.golang.org/grpc\"\n    \"google.golang.org/grpc/credentials/insecure\"\n)\n\nfunc main() {\n    conn, err := grpc.NewClient(\"localhost:9000\", grpc.WithTransportCredentials(insecure.NewCredentials()))\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer conn.Close()\n\n    client := proto.NewGorushClient(conn)\n    resp, err := client.Send(context.Background(), &proto.NotificationRequest{\n        Platform: 2,\n        Tokens:   []string{\"device_token\"},\n        Message:  \"Hello gRPC!\",\n        Title:    \"Test Notification\",\n    })\n\n    if err != nil {\n        log.Fatal(err)\n    }\n    log.Printf(\"Success: %v, Count: %d\", resp.Success, resp.Counts)\n}\n```\n\n## FAQ\n\n### Common Issues\n\n#### Q: How do I get FCM credentials?\n\nA: Go to [Firebase Console](https://console.firebase.google.com/) → Project Settings → Service Accounts → Generate New Private Key. Download the JSON file.\n\n#### Q: iOS notifications not working in production?\n\nA: Make sure you:\n\n1. Use production APNS certificates (`production: true`)\n2. Set correct bundle ID in certificate\n3. Test with production app build\n\n#### Q: Getting \"certificate verify failed\" error?\n\nA: This usually means:\n\n- Wrong certificate format (use `.pem` or `.p12`)\n- Certificate expired\n- Wrong environment (dev vs production)\n\n#### Q: How to handle large notification volumes?\n\nA: Configure workers and queue settings:\n\n```yaml\ncore:\n  worker_num: 8 # Increase workers\n  queue_num: 16384 # Increase queue size\nqueue:\n  engine: \"redis\" # Use external queue\n```\n\n#### Q: Can I send to multiple platforms at once?\n\nA: Yes, include multiple notification objects in the request:\n\n```json\n{\n  \"notifications\": [\n    { \"platform\": 1, \"tokens\": [\"ios_token\"], \"message\": \"iOS\" },\n    { \"platform\": 2, \"tokens\": [\"android_token\"], \"message\": \"Android\" }\n  ]\n}\n```\n\n#### Q: How to monitor notification failures?\n\nA: Enable sync mode or feedback webhook:\n\n```yaml\ncore:\n  sync: true # Get immediate response\n  feedback_hook_url: \"https://your-api\" # Async webhook\n```\n\n#### Q: What's the difference between platforms?\n\nA: Platform codes: `1` = iOS (APNS), `2` = Android (FCM), `3` = Huawei (HMS)\n\n### Performance Tips\n\n- Use Redis for queue and stats storage in production\n- Enable gRPC for better performance\n- Set appropriate worker numbers based on CPU cores\n- Use connection pooling for high-volume scenarios\n\n### Security Best Practices\n\n- Store credentials as files, not in config\n- Use environment variables for sensitive data\n- Enable SSL/TLS in production\n- Rotate certificates before expiration\n- Monitor failed notifications for security issues\n\n## Stargazers over time\n\n[![Stargazers over time](https://starchart.cc/appleboy/gorush.svg)](https://starchart.cc/appleboy/gorush)\n\n## License\n\nCopyright 2026 Bo-Yi Wu [@appleboy](https://github.com/appleboy).\n\nLicensed under the MIT License.\n"
  },
  {
    "path": "app/config.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/appleboy/gorush/config\"\n)\n\n// MergeConfig merges CLI options into the configuration.\n// CLI options take precedence over config file values.\nfunc MergeConfig(cfg *config.ConfYaml, opts *Options) error {\n\t// iOS options\n\tif opts.Conf.Ios.KeyPath != \"\" {\n\t\tcfg.Ios.KeyPath = opts.Conf.Ios.KeyPath\n\t}\n\tif opts.Conf.Ios.KeyID != \"\" {\n\t\tcfg.Ios.KeyID = opts.Conf.Ios.KeyID\n\t}\n\tif opts.Conf.Ios.TeamID != \"\" {\n\t\tcfg.Ios.TeamID = opts.Conf.Ios.TeamID\n\t}\n\tif opts.Conf.Ios.Password != \"\" {\n\t\tcfg.Ios.Password = opts.Conf.Ios.Password\n\t}\n\tif opts.Conf.Ios.Production {\n\t\tcfg.Ios.Production = opts.Conf.Ios.Production\n\t}\n\n\t// Android options\n\tif opts.Conf.Android.KeyPath != \"\" {\n\t\tcfg.Android.KeyPath = opts.Conf.Android.KeyPath\n\t}\n\n\t// Huawei options\n\tif opts.Conf.Huawei.AppSecret != \"\" {\n\t\tcfg.Huawei.AppSecret = opts.Conf.Huawei.AppSecret\n\t}\n\tif opts.Conf.Huawei.AppID != \"\" {\n\t\tcfg.Huawei.AppID = opts.Conf.Huawei.AppID\n\t}\n\n\t// Storage options\n\tif opts.Conf.Stat.Engine != \"\" {\n\t\tcfg.Stat.Engine = opts.Conf.Stat.Engine\n\t}\n\tif opts.Conf.Stat.Redis.Addr != \"\" {\n\t\tcfg.Stat.Redis.Addr = opts.Conf.Stat.Redis.Addr\n\t}\n\n\t// Server options with validation\n\tif opts.Conf.Core.Port != \"\" {\n\t\tif err := config.ValidatePort(opts.Conf.Core.Port); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid port from command line: %w\", err)\n\t\t}\n\t\tcfg.Core.Port = opts.Conf.Core.Port\n\t}\n\tif opts.Conf.Core.Address != \"\" {\n\t\tif err := config.ValidateAddress(opts.Conf.Core.Address); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid address from command line: %w\", err)\n\t\t}\n\t\tcfg.Core.Address = opts.Conf.Core.Address\n\t}\n\tif opts.Conf.Core.HTTPProxy != \"\" {\n\t\tcfg.Core.HTTPProxy = opts.Conf.Core.HTTPProxy\n\t}\n\n\t// PID options\n\tif opts.Conf.Core.PID.Path != \"\" {\n\t\tif err := config.ValidatePIDPath(opts.Conf.Core.PID.Path); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid PID path from command line: %w\", err)\n\t\t}\n\t\tcfg.Core.PID.Path = opts.Conf.Core.PID.Path\n\t\tcfg.Core.PID.Enabled = true\n\t\tcfg.Core.PID.Override = true\n\t}\n\n\treturn nil\n}\n\n// ValidateAndMerge loads config, merges CLI options, and validates.\nfunc ValidateAndMerge(opts *Options) (*config.ConfYaml, error) {\n\tcfg, err := config.LoadConf(opts.ConfigFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"load yaml config file error: %w\", err)\n\t}\n\n\tif err := MergeConfig(cfg, opts); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := config.ValidateConfig(cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"configuration validation failed: %w\", err)\n\t}\n\n\treturn cfg, nil\n}\n"
  },
  {
    "path": "app/config_test.go",
    "content": "package app\n\nimport (\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/config\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMergeConfig_IOSOptions(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\topts := NewOptions()\n\topts.Conf.Ios.KeyPath = \"/path/to/key\"\n\topts.Conf.Ios.KeyID = \"key-id-123\"\n\topts.Conf.Ios.TeamID = \"team-id-456\"\n\topts.Conf.Ios.Password = \"secret\"\n\topts.Conf.Ios.Production = true\n\n\terr := MergeConfig(cfg, opts)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"/path/to/key\", cfg.Ios.KeyPath)\n\tassert.Equal(t, \"key-id-123\", cfg.Ios.KeyID)\n\tassert.Equal(t, \"team-id-456\", cfg.Ios.TeamID)\n\tassert.Equal(t, \"secret\", cfg.Ios.Password)\n\tassert.True(t, cfg.Ios.Production)\n}\n\nfunc TestMergeConfig_AndroidOptions(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\topts := NewOptions()\n\topts.Conf.Android.KeyPath = \"/path/to/fcm/key.json\"\n\n\terr := MergeConfig(cfg, opts)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"/path/to/fcm/key.json\", cfg.Android.KeyPath)\n}\n\nfunc TestMergeConfig_HuaweiOptions(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\topts := NewOptions()\n\topts.Conf.Huawei.AppSecret = \"hms-secret\"\n\topts.Conf.Huawei.AppID = \"hms-id\"\n\n\terr := MergeConfig(cfg, opts)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"hms-secret\", cfg.Huawei.AppSecret)\n\tassert.Equal(t, \"hms-id\", cfg.Huawei.AppID)\n}\n\nfunc TestMergeConfig_StorageOptions(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\topts := NewOptions()\n\topts.Conf.Stat.Engine = \"redis\"\n\topts.Conf.Stat.Redis.Addr = \"localhost:6379\"\n\n\terr := MergeConfig(cfg, opts)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"redis\", cfg.Stat.Engine)\n\tassert.Equal(t, \"localhost:6379\", cfg.Stat.Redis.Addr)\n}\n\nfunc TestMergeConfig_ServerOptions(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\topts := NewOptions()\n\topts.Conf.Core.Port = \"9000\"\n\topts.Conf.Core.Address = \"127.0.0.1\"\n\topts.Conf.Core.HTTPProxy = \"http://proxy:8080\"\n\n\terr := MergeConfig(cfg, opts)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"9000\", cfg.Core.Port)\n\tassert.Equal(t, \"127.0.0.1\", cfg.Core.Address)\n\tassert.Equal(t, \"http://proxy:8080\", cfg.Core.HTTPProxy)\n}\n\nfunc TestMergeConfig_InvalidPort(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\topts := NewOptions()\n\topts.Conf.Core.Port = \"invalid\"\n\n\terr := MergeConfig(cfg, opts)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"invalid port\")\n}\n\nfunc TestMergeConfig_NoOverrideEmpty(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\toriginalPort := cfg.Core.Port\n\n\topts := NewOptions()\n\t// Empty values should not override\n\n\terr := MergeConfig(cfg, opts)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, originalPort, cfg.Core.Port)\n}\n\nfunc TestValidateAndMerge(t *testing.T) {\n\topts := NewOptions()\n\t// Use empty config file to get defaults\n\n\tcfg, err := ValidateAndMerge(opts)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, cfg)\n}\n"
  },
  {
    "path": "app/options.go",
    "content": "package app\n\nimport (\n\t\"flag\"\n\n\t\"github.com/appleboy/gorush/config\"\n)\n\n// Options holds all CLI flag values.\ntype Options struct {\n\tShowVersion bool\n\tPing        bool\n\tConfigFile  string\n\n\t// Notification options (for CLI mode)\n\tToken   string\n\tMessage string\n\tTitle   string\n\tTopic   string\n\n\t// Config overrides\n\tConf config.ConfYaml\n}\n\n// NewOptions creates a new Options instance with default values.\nfunc NewOptions() *Options {\n\treturn &Options{}\n}\n\n// BindFlags binds CLI flags to the Options struct.\n// Call this before flag.Parse().\nfunc (o *Options) BindFlags() {\n\t// Version flags\n\tflag.BoolVar(&o.ShowVersion, \"version\", false, \"Print version information.\")\n\tflag.BoolVar(&o.ShowVersion, \"V\", false, \"Print version information.\")\n\n\t// Config file\n\tflag.StringVar(&o.ConfigFile, \"c\", \"\", \"Configuration file path.\")\n\tflag.StringVar(&o.ConfigFile, \"config\", \"\", \"Configuration file path.\")\n\n\t// PID file\n\tflag.StringVar(&o.Conf.Core.PID.Path, \"pid\", \"\", \"PID file path.\")\n\n\t// iOS options\n\tflag.StringVar(&o.Conf.Ios.KeyPath, \"i\", \"\", \"iOS certificate key file path\")\n\tflag.StringVar(&o.Conf.Ios.KeyPath, \"key\", \"\", \"iOS certificate key file path\")\n\tflag.StringVar(&o.Conf.Ios.KeyID, \"key-id\", \"\", \"iOS Key ID for P8 token\")\n\tflag.StringVar(&o.Conf.Ios.TeamID, \"team-id\", \"\", \"iOS Team ID for P8 token\")\n\tflag.StringVar(&o.Conf.Ios.Password, \"P\", \"\", \"iOS certificate password for gorush\")\n\tflag.StringVar(&o.Conf.Ios.Password, \"password\", \"\", \"iOS certificate password for gorush\")\n\tflag.BoolVar(&o.Conf.Ios.Enabled, \"ios\", false, \"send ios notification\")\n\tflag.BoolVar(&o.Conf.Ios.Production, \"production\", false, \"production mode in iOS\")\n\n\t// Android options\n\tflag.StringVar(&o.Conf.Android.KeyPath, \"fcm-key\", \"\", \"FCM key path configuration for gorush\")\n\tflag.BoolVar(&o.Conf.Android.Enabled, \"android\", false, \"send android notification\")\n\n\t// Huawei options\n\tflag.StringVar(&o.Conf.Huawei.AppSecret, \"hk\", \"\", \"Huawei api key configuration for gorush\")\n\tflag.StringVar(\n\t\t&o.Conf.Huawei.AppSecret,\n\t\t\"hmskey\",\n\t\t\"\",\n\t\t\"Huawei api key configuration for gorush\",\n\t)\n\tflag.StringVar(&o.Conf.Huawei.AppID, \"hid\", \"\", \"HMS app id configuration for gorush\")\n\tflag.StringVar(&o.Conf.Huawei.AppID, \"hmsid\", \"\", \"HMS app id configuration for gorush\")\n\tflag.BoolVar(&o.Conf.Huawei.Enabled, \"huawei\", false, \"send huawei notification\")\n\n\t// Server options\n\tflag.StringVar(&o.Conf.Core.Address, \"A\", \"\", \"address to bind\")\n\tflag.StringVar(&o.Conf.Core.Address, \"address\", \"\", \"address to bind\")\n\tflag.StringVar(&o.Conf.Core.Port, \"p\", \"\", \"port number for gorush\")\n\tflag.StringVar(&o.Conf.Core.Port, \"port\", \"\", \"port number for gorush\")\n\tflag.StringVar(&o.Conf.Core.HTTPProxy, \"proxy\", \"\", \"http proxy url\")\n\n\t// Storage options\n\tflag.StringVar(&o.Conf.Stat.Engine, \"e\", \"\", \"store engine\")\n\tflag.StringVar(&o.Conf.Stat.Engine, \"engine\", \"\", \"store engine\")\n\tflag.StringVar(&o.Conf.Stat.Redis.Addr, \"redis-addr\", \"\", \"redis addr\")\n\n\t// Notification options (CLI mode)\n\tflag.StringVar(&o.Token, \"t\", \"\", \"token string\")\n\tflag.StringVar(&o.Token, \"token\", \"\", \"token string\")\n\tflag.StringVar(&o.Message, \"m\", \"\", \"notification message\")\n\tflag.StringVar(&o.Message, \"message\", \"\", \"notification message\")\n\tflag.StringVar(&o.Title, \"title\", \"\", \"notification title\")\n\tflag.StringVar(&o.Topic, \"topic\", \"\", \"apns topic in iOS\")\n\n\t// Health check\n\tflag.BoolVar(&o.Ping, \"ping\", false, \"ping server\")\n}\n\n// CLISendOptions returns CLI send options for notification sending.\nfunc (o *Options) CLISendOptions() CLISendOptions {\n\treturn CLISendOptions{\n\t\tToken:   o.Token,\n\t\tMessage: o.Message,\n\t\tTitle:   o.Title,\n\t\tTopic:   o.Topic,\n\t}\n}\n\n// IsCLIMode returns true if running in CLI notification mode.\nfunc (o *Options) IsCLIMode() bool {\n\treturn o.Conf.Android.Enabled || o.Conf.Ios.Enabled || o.Conf.Huawei.Enabled\n}\n"
  },
  {
    "path": "app/options_test.go",
    "content": "package app\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewOptions(t *testing.T) {\n\topts := NewOptions()\n\tassert.NotNil(t, opts)\n\tassert.False(t, opts.ShowVersion)\n\tassert.False(t, opts.Ping)\n\tassert.Empty(t, opts.ConfigFile)\n\tassert.Empty(t, opts.Token)\n\tassert.Empty(t, opts.Message)\n}\n\nfunc TestOptions_CLISendOptions(t *testing.T) {\n\topts := &Options{\n\t\tToken:   \"test-token\",\n\t\tMessage: \"test-message\",\n\t\tTitle:   \"test-title\",\n\t\tTopic:   \"test-topic\",\n\t}\n\n\tsendOpts := opts.CLISendOptions()\n\tassert.Equal(t, \"test-token\", sendOpts.Token)\n\tassert.Equal(t, \"test-message\", sendOpts.Message)\n\tassert.Equal(t, \"test-title\", sendOpts.Title)\n\tassert.Equal(t, \"test-topic\", sendOpts.Topic)\n}\n\nfunc TestOptions_IsCLIMode(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\topts     *Options\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"no platform enabled\",\n\t\t\topts:     &Options{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"android enabled\",\n\t\t\topts: func() *Options {\n\t\t\t\to := NewOptions()\n\t\t\t\to.Conf.Android.Enabled = true\n\t\t\t\treturn o\n\t\t\t}(),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ios enabled\",\n\t\t\topts: func() *Options {\n\t\t\t\to := NewOptions()\n\t\t\t\to.Conf.Ios.Enabled = true\n\t\t\t\treturn o\n\t\t\t}(),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"huawei enabled\",\n\t\t\topts: func() *Options {\n\t\t\t\to := NewOptions()\n\t\t\t\to.Conf.Huawei.Enabled = true\n\t\t\t\treturn o\n\t\t\t}(),\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, tt.opts.IsCLIMode())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/sender.go",
    "content": "package app\n\nimport (\n\t\"context\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/notify\"\n\t\"github.com/appleboy/gorush/status\"\n)\n\n// CLISendOptions contains options for sending notifications via CLI.\ntype CLISendOptions struct {\n\tToken   string\n\tMessage string\n\tTitle   string\n\tTopic   string\n}\n\n// SendAndroidNotification sends an Android notification via CLI.\nfunc SendAndroidNotification(ctx context.Context, cfg *config.ConfYaml, opts CLISendOptions) error {\n\tcfg.Android.Enabled = true\n\n\treq := &notify.PushNotification{\n\t\tPlatform: core.PlatFormAndroid,\n\t\tMessage:  opts.Message,\n\t\tTitle:    opts.Title,\n\t}\n\n\tif opts.Token != \"\" {\n\t\treq.To = opts.Token\n\t}\n\n\tif opts.Topic != \"\" {\n\t\treq.Topic = opts.Topic\n\t}\n\n\tif err := status.InitAppStatus(cfg); err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := notify.PushToAndroid(ctx, req, cfg); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// SendHuaweiNotification sends a Huawei notification via CLI.\nfunc SendHuaweiNotification(ctx context.Context, cfg *config.ConfYaml, opts CLISendOptions) error {\n\tcfg.Huawei.Enabled = true\n\n\treq := &notify.PushNotification{\n\t\tPlatform: core.PlatFormHuawei,\n\t\tMessage:  opts.Message,\n\t\tTitle:    opts.Title,\n\t}\n\n\tif opts.Token != \"\" {\n\t\treq.Tokens = []string{opts.Token}\n\t}\n\n\tif opts.Topic != \"\" {\n\t\treq.To = opts.Topic\n\t}\n\n\tif err := notify.CheckMessage(req); err != nil {\n\t\treturn err\n\t}\n\n\tif err := status.InitAppStatus(cfg); err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := notify.PushToHuawei(ctx, req, cfg); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// SendIOSNotification sends an iOS notification via CLI.\nfunc SendIOSNotification(ctx context.Context, cfg *config.ConfYaml, opts CLISendOptions) error {\n\tcfg.Ios.Enabled = true\n\n\treq := &notify.PushNotification{\n\t\tPlatform: core.PlatFormIos,\n\t\tMessage:  opts.Message,\n\t\tTitle:    opts.Title,\n\t}\n\n\tif opts.Token != \"\" {\n\t\treq.Tokens = []string{opts.Token}\n\t}\n\n\tif opts.Topic != \"\" {\n\t\treq.Topic = opts.Topic\n\t}\n\n\tif err := notify.CheckMessage(req); err != nil {\n\t\treturn err\n\t}\n\n\tif err := status.InitAppStatus(cfg); err != nil {\n\t\treturn err\n\t}\n\n\tif err := notify.InitAPNSClient(ctx, cfg); err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := notify.PushToIOS(ctx, req, cfg); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// SendNotification sends a notification based on platform type.\nfunc SendNotification(\n\tctx context.Context,\n\tplatform int,\n\tcfg *config.ConfYaml,\n\topts CLISendOptions,\n) error {\n\tswitch platform {\n\tcase core.PlatFormAndroid:\n\t\treturn SendAndroidNotification(ctx, cfg, opts)\n\tcase core.PlatFormHuawei:\n\t\treturn SendHuaweiNotification(ctx, cfg, opts)\n\tcase core.PlatFormIos:\n\t\treturn SendIOSNotification(ctx, cfg, opts)\n\tdefault:\n\t\tlogx.LogError.Fatalf(\"unsupported platform: %d\", platform)\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "app/sender_test.go",
    "content": "package app\n\nimport (\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCLISendOptions(t *testing.T) {\n\topts := CLISendOptions{\n\t\tToken:   \"test-token\",\n\t\tMessage: \"test-message\",\n\t\tTitle:   \"test-title\",\n\t\tTopic:   \"test-topic\",\n\t}\n\n\tassert.Equal(t, \"test-token\", opts.Token)\n\tassert.Equal(t, \"test-message\", opts.Message)\n\tassert.Equal(t, \"test-title\", opts.Title)\n\tassert.Equal(t, \"test-topic\", opts.Topic)\n}\n\nfunc TestSendNotification_UnsupportedPlatform(t *testing.T) {\n\t// Test that unsupported platform is handled\n\t// Note: This would call logx.LogError.Fatalf in production,\n\t// so we can't easily test it without mocking.\n\t// This test mainly documents the expected behavior.\n\tassert.Equal(t, 1, core.PlatFormIos)\n\tassert.Equal(t, 2, core.PlatFormAndroid)\n\tassert.Equal(t, 3, core.PlatFormHuawei)\n}\n"
  },
  {
    "path": "app/worker.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/notify\"\n\n\t\"github.com/golang-queue/nats\"\n\t\"github.com/golang-queue/nsq\"\n\t\"github.com/golang-queue/queue\"\n\tqcore \"github.com/golang-queue/queue/core\"\n\tredisdb \"github.com/golang-queue/redisdb-stream\"\n)\n\n// NewQueueWorker creates a queue worker based on the configured queue engine.\n// Supported engines: local, nsq, nats, redis.\nfunc NewQueueWorker(cfg *config.ConfYaml) (qcore.Worker, error) {\n\tswitch core.Queue(cfg.Queue.Engine) {\n\tcase core.LocalQueue:\n\t\treturn queue.NewRing(\n\t\t\tqueue.WithQueueSize(int(cfg.Core.QueueNum)),\n\t\t\tqueue.WithFn(notify.Run(cfg)),\n\t\t\tqueue.WithLogger(logx.QueueLogger()),\n\t\t), nil\n\n\tcase core.NSQ:\n\t\treturn nsq.NewWorker(\n\t\t\tnsq.WithAddr(cfg.Queue.NSQ.Addr),\n\t\t\tnsq.WithTopic(cfg.Queue.NSQ.Topic),\n\t\t\tnsq.WithChannel(cfg.Queue.NSQ.Channel),\n\t\t\tnsq.WithMaxInFlight(int(cfg.Core.WorkerNum)),\n\t\t\tnsq.WithRunFunc(notify.Run(cfg)),\n\t\t\tnsq.WithLogger(logx.QueueLogger()),\n\t\t), nil\n\n\tcase core.NATS:\n\t\treturn nats.NewWorker(\n\t\t\tnats.WithAddr(cfg.Queue.NATS.Addr),\n\t\t\tnats.WithSubj(cfg.Queue.NATS.Subj),\n\t\t\tnats.WithQueue(cfg.Queue.NATS.Queue),\n\t\t\tnats.WithRunFunc(notify.Run(cfg)),\n\t\t\tnats.WithLogger(logx.QueueLogger()),\n\t\t), nil\n\n\tcase core.Redis:\n\t\topts := []redisdb.Option{\n\t\t\tredisdb.WithAddr(cfg.Queue.Redis.Addr),\n\t\t\tredisdb.WithUsername(cfg.Queue.Redis.Username),\n\t\t\tredisdb.WithPassword(cfg.Queue.Redis.Password),\n\t\t\tredisdb.WithDB(cfg.Queue.Redis.DB),\n\t\t\tredisdb.WithStreamName(cfg.Queue.Redis.StreamName),\n\t\t\tredisdb.WithGroup(cfg.Queue.Redis.Group),\n\t\t\tredisdb.WithConsumer(cfg.Queue.Redis.Consumer),\n\t\t\tredisdb.WithMaxLength(cfg.Core.QueueNum),\n\t\t\tredisdb.WithRunFunc(notify.Run(cfg)),\n\t\t\tredisdb.WithLogger(logx.QueueLogger()),\n\t\t}\n\t\tif cfg.Queue.Redis.WithTLS {\n\t\t\topts = append(opts, redisdb.WithTLS())\n\t\t}\n\t\treturn redisdb.NewWorker(opts...), nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported queue engine: %s\", cfg.Queue.Engine)\n\t}\n}\n\n// NewQueuePool creates a queue pool with the configured number of workers.\nfunc NewQueuePool(cfg *config.ConfYaml, w qcore.Worker) *queue.Queue {\n\treturn queue.NewPool(\n\t\tcfg.Core.WorkerNum,\n\t\tqueue.WithWorker(w),\n\t\tqueue.WithLogger(logx.QueueLogger()),\n\t)\n}\n"
  },
  {
    "path": "app/worker_test.go",
    "content": "package app\n\nimport (\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/config\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewQueueWorker_LocalQueue(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\tcfg.Queue.Engine = \"local\"\n\n\tw, err := NewQueueWorker(cfg)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, w)\n}\n\nfunc TestNewQueueWorker_UnsupportedEngine(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\tcfg.Queue.Engine = \"unsupported\"\n\n\tw, err := NewQueueWorker(cfg)\n\trequire.Error(t, err)\n\tassert.Nil(t, w)\n\tassert.Contains(t, err.Error(), \"unsupported queue engine\")\n}\n\nfunc TestNewQueuePool(t *testing.T) {\n\tcfg, _ := config.LoadConf(\"\")\n\tcfg.Queue.Engine = \"local\"\n\n\tw, err := NewQueueWorker(cfg)\n\trequire.NoError(t, err)\n\n\tq := NewQueuePool(cfg, w)\n\tassert.NotNil(t, q)\n\n\t// Clean up\n\tq.Release()\n}\n"
  },
  {
    "path": "certificate/authkey-invalid.p8",
    "content": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE\nZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI\ntB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfZZZ\n"
  },
  {
    "path": "certificate/authkey-valid.p8",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE\nZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI\ntB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfeQl\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certificate/certificate-valid.pem",
    "content": "Bag Attributes\n    localKeyID: 8C 1A 9F 00 66 BD 24 42 B9 5D 1E EB FE 5E 8B CA 04 3D 73 83 \n    friendlyName: APNS/2 Private Key\nsubject=/C=NZ/ST=Wellington/L=Wellington/O=Internet Widgits Pty Ltd/OU=9ZEH62KRVV/CN=APNS/2 Development IOS Push Services: com.sideshow.Apns2\nissuer=/C=NZ/ST=Wellington/L=Wellington/O=APNS/2 Inc./OU=APNS/2 Worldwide Developer Relations/CN=APNS/2 Worldwide Developer Relations Certification Authority\n-----BEGIN CERTIFICATE-----\nMIID6zCCAtMCAQIwDQYJKoZIhvcNAQELBQAwgcMxCzAJBgNVBAYTAk5aMRMwEQYD\nVQQIEwpXZWxsaW5ndG9uMRMwEQYDVQQHEwpXZWxsaW5ndG9uMRQwEgYDVQQKEwtB\nUE5TLzIgSW5jLjEtMCsGA1UECxMkQVBOUy8yIFdvcmxkd2lkZSBEZXZlbG9wZXIg\nUmVsYXRpb25zMUUwQwYDVQQDEzxBUE5TLzIgV29ybGR3aWRlIERldmVsb3BlciBS\nZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTYwMTA4MDgzNDMw\nWhcNMjYwMTA1MDgzNDMwWjCBsjELMAkGA1UEBhMCTloxEzARBgNVBAgTCldlbGxp\nbmd0b24xEzARBgNVBAcTCldlbGxpbmd0b24xITAfBgNVBAoTGEludGVybmV0IFdp\nZGdpdHMgUHR5IEx0ZDETMBEGA1UECxMKOVpFSDYyS1JWVjFBMD8GA1UEAxM4QVBO\nUy8yIERldmVsb3BtZW50IElPUyBQdXNoIFNlcnZpY2VzOiBjb20uc2lkZXNob3cu\nQXBuczIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY0c1TKB5oZPwQ\n7t1CwMIrvqB6GIU3tPy6RhckZXTkOB8YeBWJ7UKfCz8HGHFVomBP0T5OUbeqQzqW\nYJbQzZ8a6ZMszbL0lO4X9++3Oi5/TtAwOUOK8rOFN25m2KfsayHQZ/4vWStK2Fwm\n5aJbGLlpH/b/7z1D4vhmMgoBuT1IuyhGiyFxlZ9EtTloFvsqM1E5fYZOSZACyXTa\nK4vdgbQMgUVsI714FAgLTlK0UeiRkmKm3pdbtfVbrthzI+IHXKItUIy+Fn20PRMh\ndSnaztSz7tgBWCIx22qvcYogHWiOgUYIM772zE2y8UVOr8DsiRlsOHSA7EI4MJcQ\nG2FUq2Z/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGyfyO2HMgcdeBcz3bt5BILX\nf7RA2/UmVIwcKR1qotTsF+PnBmcILeyOQgDe9tGU5cRc79kDt3JRmMYROFIMgFRf\nWf22uOKtho7GQQaKvG+bkgMVdYFRlBHnF+KeqKH81qb9p+CT4Iw0GehIL1DijFLR\nVIAIBYpz4oBPCIE1ISVT+Fgaf3JAh59kbPbNw9AIDxaBtP8EuzSTNwfbxoGbCobS\nWi1U8IsCwQFt8tM1m4ZXD1CcZIrGdryeAhVkvKIJRiU5QYWI2nqZN+JqQucm9ad0\nmYO5mJkIobUa4+ZJhCPKEdmgpFbRGk0wVuaDM9Cv6P2srsYAjaO4y3VP0GvNKRI=\n-----END CERTIFICATE-----\nBag Attributes\n    localKeyID: 8C 1A 9F 00 66 BD 24 42 B9 5D 1E EB FE 5E 8B CA 04 3D 73 83 \n    friendlyName: APNS/2 Private Key\nKey Attributes: <No Attributes>\n-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA2NHNUygeaGT8EO7dQsDCK76gehiFN7T8ukYXJGV05DgfGHgV\nie1Cnws/BxhxVaJgT9E+TlG3qkM6lmCW0M2fGumTLM2y9JTuF/fvtzouf07QMDlD\nivKzhTduZtin7Gsh0Gf+L1krSthcJuWiWxi5aR/2/+89Q+L4ZjIKAbk9SLsoRosh\ncZWfRLU5aBb7KjNROX2GTkmQAsl02iuL3YG0DIFFbCO9eBQIC05StFHokZJipt6X\nW7X1W67YcyPiB1yiLVCMvhZ9tD0TIXUp2s7Us+7YAVgiMdtqr3GKIB1ojoFGCDO+\n9sxNsvFFTq/A7IkZbDh0gOxCODCXEBthVKtmfwIDAQABAoIBAQCW8ZCI+OAae1tE\nipZ9F2bWP3LHLXTo8FYVdCA+VWeITk3PoiIUkJmV0aWCUhDstgto5doDej5sCTur\nXvj/ynaerMeqJFYWkewjwZcgLyAZvwuO1v7fp9E0x/9TGDfnjjnPNeaundxW0cNt\nzOY3l0HVHsy9Jpe3QDcAJovy4Tv5+hFY4kDxUBGsyjvhScVgKg5tLkJclm3sOu/L\nGyLqpwNI3OJAdMIuVD4N2BZ1aOEap6mp2y8Ie0/R4YWcaZ5A4Pw7xUPl6SXc9uua\n/78QTERtPC6ejyCBiE05a8m3Q3iud3Xtnlyws2KwhgBAfE6M4zR/f3OQB7ZIXMhy\nZpmZZw5xAoGBAPYn84IrlIQetWQfvPdM7Kzgh6UDHCugnlCDghwYpRJGi8hMfuZV\nxNIrYAJzLYDQ01lFJRJgWXTcbqz9NBz1nhg+cNOz1/KY+38eudee6DNYmztP7jDP\n2jnaS+dtjC8hAXObnFqG+NilMDLLu6aRmrJaImbjSrfyLiE6mvJ7u81nAoGBAOF9\ng93wZ0mL1rk2s5WwHGTNU/HaOtmWS4z7kA7f4QaRub+MwppZmmDZPHpiZX7BPcZz\niOPQh+xn7IqRGoQWBLykBVt8zZFoLZJoCR3n63lex5A4p/0Pp1gFZrR+xX8PYVos\n3yeeiWyPKsXXNc0s5QwHZcX6Wb8EHThTXGCBetcpAoGAMeQJC9IPaPPcae2w3CLA\nOY3MkFpgBEuqqsDsxwsLsfeQb0lp0v+BQ+O8suJrT5eDrq1ABUh3+SKQYAl13YS+\nxUUqkw35b9cn6iztF9HCWF3WIKBjs4r9PQqMpdxjNE4pQChC+Wov16ErcrAuWWVb\niFiSbm4U/9FbHisFqq3/c3MCgYB+vzSuPgFw37+0oEDVtQZgyuGSop5NzCNvfb/9\n/G3aaXNFbnO8mv0hzzoleMWgODLnJ+4cUAz3H3tgcCu9bzr+Zhv0zvQl9a8YCo6F\nVuWPdW0rbg1PO8tOuMqATnno79ZC/9H3zS9l7BuY1V2SlNeyqT3VyOFFc6SREpps\nTJul8QKBgAxnQB8MA7zPULu1clyaJLdtEdRPkKWN7lKYptc0e/VHfSsKxseWkfqi\nzgXZ51kQTrT6Zb6HYRfwC1mMXHWRKRyYjAnCxVim6YQd+KVT49iRDDAiIFoMGA4i\nvvcIlneqOZZPDIoKJ60IjO/DZHWkw5mLjaIrT+qQ3XAGdJA13hcm\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "certificate/localhost.cert",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC+zCCAeOgAwIBAgIJALbZEDvUQrFKMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV\nBAMMCWxvY2FsaG9zdDAeFw0xNjAzMjgwMzMwNDFaFw0yNjAzMjYwMzMwNDFaMBQx\nEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAMj1+xg4jVLzVnB5j7n1ul30WEE4BCzcNFxg5AOB5H5q+wje0YYiVFg6PQyv\nGCipqIRXVRdVQ1hHSeunYGKe8lq3Sb1X8PUJ12v9uRbpS9DK1Owqk8rsPDu6sVTL\nqKKgH1Z8yazzaS0AbXuA5e9gO/RzijbnpEP+quM4dueiMPVEJyLq+EoIQY+MM8MP\n8dZzL4XZl7wL4UsCN7rPcO6W3tlnT0iO3h9c/Ym2hFhz+KNJ9KRRCvtPGZESigtK\nbHsXH099WDo8v/Wp5/evBw/+JD0opxmCfHIBALHt9v53RvvsDZ1t33Rpu5C8znEY\nY2Ay7NgxhqjqoWJqA48lJeA0clsCAwEAAaNQME4wHQYDVR0OBBYEFC0bTU1Xofeh\nNKIelashIsqKidDYMB8GA1UdIwQYMBaAFC0bTU1XofehNKIelashIsqKidDYMAwG\nA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAiJL8IMTwNX9XqQWYDFgkG4\nAnrVwQhreAqC9rSxDCjqqnMHPHGzcCeDMLAMoh0kOy20nowUGNtCZ0uBvnX2q1bN\ng1jt+GBcLJDR3LL4CpNOlm3YhOycuNfWMxTA7BXkmnSrZD/7KhArsBEY8aulxwKJ\nHRgNlIwe1oFD1YdX1BS5pp4t25B6Vq4A3FMMUkVoWE688nE168hvQgwjrHkgHhwe\neN8lGE2DhFraXnWmDMdwaHD3HRFGhyppIFN+f7BqbWX9gM+T2YRTfObIXLWbqJLD\n3Mk/NkxqVcg4eY54wJ1ufCUGAYAIaY6fQqiNUz8nhwK3t45NBVT9y/uJXqnTLyY=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certificate/localhost.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAyPX7GDiNUvNWcHmPufW6XfRYQTgELNw0XGDkA4Hkfmr7CN7R\nhiJUWDo9DK8YKKmohFdVF1VDWEdJ66dgYp7yWrdJvVfw9QnXa/25FulL0MrU7CqT\nyuw8O7qxVMuooqAfVnzJrPNpLQBte4Dl72A79HOKNuekQ/6q4zh256Iw9UQnIur4\nSghBj4wzww/x1nMvhdmXvAvhSwI3us9w7pbe2WdPSI7eH1z9ibaEWHP4o0n0pFEK\n+08ZkRKKC0psexcfT31YOjy/9ann968HD/4kPSinGYJ8cgEAse32/ndG++wNnW3f\ndGm7kLzOcRhjYDLs2DGGqOqhYmoDjyUl4DRyWwIDAQABAoIBAGTKqsN9KbSfA42q\nCqI0UuLouJMNa1qsnz5uAi6YKWgWdA4A44mpEjCmFRSVhUJvxWuK+cyYIQzXxIWD\nD16nZdqF72AeCWZ9JySsvvZ00GfKM3y35iRy08sJWgOzmcLnGJCiSeyKsQe3HTJC\ndhDXbXqvsHTVPZg01LTeDxUiTffU8NMKqR2AecQ2sTDwXEhAnTyAtnzl/XaBgFzu\nU6G7FzGM5y9bxkfQVkvy+DEJkHGNOjzwcVfByyVl610ixmG1vmxVj9PbWmIPsUV8\nySmjhvDQbOfoxW0h9vTlTqGtQcBw962osnDDMWFCdM7lzO0T7RRnPVGIRpCJOKhq\nkeqHKwECgYEA8wwI/iZughoTXTNG9LnQQ/WAtsqO80EjMTUheo5I1kOzmUz09pyh\niAsUDoN0/26tZ5WNjlnyZu7dvTc/x3dTZpmNnoo8gcVbQNECDRzqfuQ9PPXm1SN5\n6peBqAvBv78hjV05aXzPG/VBbeig7l299EarEA+a/oH3KrgDoqVqE0ECgYEA06vA\nYJmgg4fZRucAYoaYsLz9Z9rCFjTe1PBTmUJkbOR8vFIHHTTEWi/SuxXL0wDSeoE2\n7BQm86gCC7/KgRdrzoBqZ5qS9Mv2dsLgY635VSgjjfZkVLiH1VRRpSQObYnfoysg\ngatcHSKMExd4SLQByAuImXP+L5ayDBcEJfbqSpsCgYB78Is1b0uzNLDjOh7Y9Vhr\nD2qPzEORcIoNsdZctOoXuXaAmmngyIbm5R9ZN1gWWc47oFwLV3rxWqXgs6fmg8cX\n7v309vFcC9Q4/Vxaa4B5LNK9n3gTAIBPTOtlUnl+2my1tfBtBqRm0W6IKbTHWS5g\nvxjEm/CiEIyGUEgqTMgHAQKBgBKuXdQoutng63QufwIzDtbKVzMLQ4XiNKhmbXph\nOavCnp+gPbB+L7Yl8ltAmTSOJgVZ0hcT0DxA361Zx+2Mu58GBl4OblnchmwE1vj1\nKcQyPrEQxdoUTyiswGfqvrs8J9imvb+z9/U6T1KAB8Wi3WViXzPr4MsiaaRXg642\nFIdxAoGAZ7/735dkhJcyOfs+LKsLr68JSstoorXOYvdMu1+JGa9iLuhnHEcMVWC8\nIuihzPfloZtMbGYkZJn8l3BeGd8hmfFtgTgZGPoVRetft2LDFLnPxp2sEH5OFLsQ\nR+K/kAOul8eStWuMXOFA9pMzGkGEgIFJMJOyaJON3kedQI8deCM=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "config/config.go",
    "content": "package config\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/spf13/viper\"\n)\n\nvar defaultConf = []byte(`\ncore:\n  enabled: true # enable httpd server\n  address: \"\" # ip address to bind (default: any)\n  shutdown_timeout: 30 # default is 30 second\n  port: \"8088\" # ignore this port number if auto_tls is enabled (listen 443).\n  worker_num: 0 # default worker number is runtime.NumCPU()\n  queue_num: 0 # default queue number is 8192\n  max_notification: 100\n  # set true if you need get error message from fail push notification in API response.\n  # It only works when the queue engine is local.\n  sync: false\n  # set webhook url if you need get error message asynchronously from fail push notification in API response.\n  feedback_hook_url: \"\"\n  feedback_timeout: 10 # default is 10 second\n  feedback_header:\n  mode: \"release\"\n  ssl: false\n  cert_path: \"cert.pem\"\n  key_path: \"key.pem\"\n  cert_base64: \"\"\n  key_base64: \"\"\n  http_proxy: \"\"\n  pid:\n    enabled: false\n    path: \"gorush.pid\"\n    override: true\n  auto_tls:\n    enabled: false # Automatically install TLS certificates from Let's Encrypt.\n    folder: \".cache\" # folder for storing TLS certificates\n    host: \"\" # which domains the Let's Encrypt will attempt\n\ngrpc:\n  enabled: false # enable gRPC server\n  port: 9000\n\napi:\n  push_uri: \"/api/push\"\n  stat_go_uri: \"/api/stat/go\"\n  stat_app_uri: \"/api/stat/app\"\n  config_uri: \"/api/config\"\n  sys_stat_uri: \"/sys/stats\"\n  metric_uri: \"/metrics\"\n  health_uri: \"/healthz\"\n\nandroid:\n  enabled: true\n  key_path: \"\" # path to fcm key file\n  credential: \"\" # fcm credential data\n  max_retry: 0 # resend fail notification, default value zero is disabled\n\nhuawei:\n  enabled: false\n  appsecret: \"YOUR_APP_SECRET\"\n  appid: \"YOUR_APP_ID\"\n  max_retry: 0 # resend fail notification, default value zero is disabled\n\nqueue:\n  engine: \"local\" # support \"local\", \"nsq\", \"nats\" and \"redis\" default value is \"local\"\n  nsq:\n    addr: 127.0.0.1:4150\n    topic: gorush\n    channel: gorush\n  nats:\n    addr: 127.0.0.1:4222\n    subj: gorush\n    queue: gorush\n  redis:\n    addr: 127.0.0.1:6379\n    group: gorush\n    consumer: gorush\n    stream_name: gorush\n    with_tls: false\n    username: \"\"\n    password: \"\"\n    db: 0\n\nios:\n  enabled: false\n  key_path: \"\"\n  key_base64: \"\" # load iOS key from base64 input\n  key_type: \"pem\" # could be pem, p12 or p8 type\n  password: \"\" # certificate password, default as empty string.\n  production: false\n  max_concurrent_pushes: 100 # just for push ios notification\n  max_retry: 0 # resend fail notification, default value zero is disabled\n  key_id: \"\" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)\n  team_id: \"\" # TeamID from developer account (View Account -> Membership)\n\nlog:\n  format: \"string\" # string or json\n  access_log: \"stdout\" # stdout: output to console, or define log path like \"log/access_log\"\n  access_level: \"debug\"\n  error_log: \"stderr\" # stderr: output to console, or define log path like \"log/error_log\"\n  error_level: \"error\"\n  hide_token: true\n  hide_messages: false\n\nstat:\n  engine: \"memory\" # support memory, redis, boltdb, buntdb or leveldb\n  redis:\n    cluster: false\n    addr: \"localhost:6379\" # if cluster is true, you may set this to \"localhost:6379,localhost:6380,localhost:6381\"\n    username: \"\"\n    password: \"\"\n    db: 0\n  boltdb:\n    path: \"bolt.db\"\n    bucket: \"gorush\"\n  buntdb:\n    path: \"bunt.db\"\n  leveldb:\n    path: \"level.db\"\n  badgerdb:\n    path: \"badger.db\"\n`)\n\n// ConfYaml is config structure.\ntype ConfYaml struct {\n\tCore    SectionCore    `yaml:\"core\"`\n\tAPI     SectionAPI     `yaml:\"api\"`\n\tAndroid SectionAndroid `yaml:\"android\"`\n\tHuawei  SectionHuawei  `yaml:\"huawei\"`\n\tIos     SectionIos     `yaml:\"ios\"`\n\tQueue   SectionQueue   `yaml:\"queue\"`\n\tLog     SectionLog     `yaml:\"log\"`\n\tStat    SectionStat    `yaml:\"stat\"`\n\tGRPC    SectionGRPC    `yaml:\"grpc\"`\n}\n\n// SectionCore is sub section of config.\ntype SectionCore struct {\n\tEnabled         bool           `yaml:\"enabled\"`\n\tAddress         string         `yaml:\"address\"`\n\tShutdownTimeout int64          `yaml:\"shutdown_timeout\"`\n\tPort            string         `yaml:\"port\"`\n\tMaxNotification int64          `yaml:\"max_notification\"`\n\tWorkerNum       int64          `yaml:\"worker_num\"`\n\tQueueNum        int64          `yaml:\"queue_num\"`\n\tMode            string         `yaml:\"mode\"`\n\tSync            bool           `yaml:\"sync\"`\n\tSSL             bool           `yaml:\"ssl\"`\n\tCertPath        string         `yaml:\"cert_path\"`\n\tKeyPath         string         `yaml:\"key_path\"`\n\tCertBase64      string         `yaml:\"cert_base64\"`\n\tKeyBase64       string         `yaml:\"key_base64\"`\n\tHTTPProxy       string         `yaml:\"http_proxy\"`\n\tPID             SectionPID     `yaml:\"pid\"`\n\tAutoTLS         SectionAutoTLS `yaml:\"auto_tls\"`\n\n\tFeedbackURL     string   `yaml:\"feedback_hook_url\"`\n\tFeedbackTimeout int64    `yaml:\"feedback_timeout\"`\n\tFeedbackHeader  []string `yaml:\"feedback_header\"`\n}\n\n// SectionAutoTLS support Let's Encrypt setting.\ntype SectionAutoTLS struct {\n\tEnabled bool   `yaml:\"enabled\"`\n\tFolder  string `yaml:\"folder\"`\n\tHost    string `yaml:\"host\"`\n}\n\n// SectionAPI is sub section of config.\ntype SectionAPI struct {\n\tPushURI    string `yaml:\"push_uri\"`\n\tStatGoURI  string `yaml:\"stat_go_uri\"`\n\tStatAppURI string `yaml:\"stat_app_uri\"`\n\tConfigURI  string `yaml:\"config_uri\"`\n\tSysStatURI string `yaml:\"sys_stat_uri\"`\n\tMetricURI  string `yaml:\"metric_uri\"`\n\tHealthURI  string `yaml:\"health_uri\"`\n}\n\n// SectionAndroid is sub section of config.\ntype SectionAndroid struct {\n\tEnabled    bool   `yaml:\"enabled\"`\n\tKeyPath    string `yaml:\"key_path\"`\n\tCredential string `yaml:\"credential\"`\n\tMaxRetry   int    `yaml:\"max_retry\"`\n}\n\n// SectionHuawei is sub section of config.\ntype SectionHuawei struct {\n\tEnabled   bool   `yaml:\"enabled\"`\n\tAppSecret string `yaml:\"appsecret\"`\n\tAppID     string `yaml:\"appid\"`\n\tMaxRetry  int    `yaml:\"max_retry\"`\n}\n\n// SectionIos is sub section of config.\ntype SectionIos struct {\n\tEnabled             bool   `yaml:\"enabled\"`\n\tKeyPath             string `yaml:\"key_path\"`\n\tKeyBase64           string `yaml:\"key_base64\"`\n\tKeyType             string `yaml:\"key_type\"`\n\tPassword            string `yaml:\"password\"`\n\tProduction          bool   `yaml:\"production\"`\n\tMaxConcurrentPushes uint   `yaml:\"max_concurrent_pushes\"`\n\tMaxRetry            int    `yaml:\"max_retry\"`\n\tKeyID               string `yaml:\"key_id\"`\n\tTeamID              string `yaml:\"team_id\"`\n}\n\n// SectionLog is sub section of config.\ntype SectionLog struct {\n\tFormat       string `yaml:\"format\"`\n\tAccessLog    string `yaml:\"access_log\"`\n\tAccessLevel  string `yaml:\"access_level\"`\n\tErrorLog     string `yaml:\"error_log\"`\n\tErrorLevel   string `yaml:\"error_level\"`\n\tHideToken    bool   `yaml:\"hide_token\"`\n\tHideMessages bool   `yaml:\"hide_messages\"`\n}\n\n// SectionStat is sub section of config.\ntype SectionStat struct {\n\tEngine   string          `yaml:\"engine\"`\n\tRedis    SectionRedis    `yaml:\"redis\"`\n\tBoltDB   SectionBoltDB   `yaml:\"boltdb\"`\n\tBuntDB   SectionBuntDB   `yaml:\"buntdb\"`\n\tLevelDB  SectionLevelDB  `yaml:\"leveldb\"`\n\tBadgerDB SectionBadgerDB `yaml:\"badgerdb\"`\n}\n\n// SectionQueue is sub section of config.\ntype SectionQueue struct {\n\tEngine string            `yaml:\"engine\"`\n\tNSQ    SectionNSQ        `yaml:\"nsq\"`\n\tNATS   SectionNATS       `yaml:\"nats\"`\n\tRedis  SectionRedisQueue `yaml:\"redis\"`\n}\n\n// SectionNSQ is sub section of config.\ntype SectionNSQ struct {\n\tAddr    string `yaml:\"addr\"`\n\tTopic   string `yaml:\"topic\"`\n\tChannel string `yaml:\"channel\"`\n}\n\n// SectionNATS is sub section of config.\ntype SectionNATS struct {\n\tAddr  string `yaml:\"addr\"`\n\tSubj  string `yaml:\"subj\"`\n\tQueue string `yaml:\"queue\"`\n}\n\n// SectionRedisQueue is sub section of config.\ntype SectionRedisQueue struct {\n\tAddr       string `yaml:\"addr\"`\n\tUsername   string `yaml:\"username\"`\n\tPassword   string `yaml:\"password\"`\n\tDB         int    `yaml:\"db\"`\n\tStreamName string `yaml:\"stream_name\"`\n\tGroup      string `yaml:\"group\"`\n\tConsumer   string `yaml:\"consumer\"`\n\tWithTLS    bool   `yaml:\"with_tls\"`\n}\n\n// SectionRedis is sub section of config.\ntype SectionRedis struct {\n\tCluster  bool   `yaml:\"cluster\"`\n\tAddr     string `yaml:\"addr\"`\n\tUsername string `yaml:\"username\"`\n\tPassword string `yaml:\"password\"`\n\tDB       int    `yaml:\"db\"`\n}\n\n// SectionBoltDB is sub section of config.\ntype SectionBoltDB struct {\n\tPath   string `yaml:\"path\"`\n\tBucket string `yaml:\"bucket\"`\n}\n\n// SectionBuntDB is sub section of config.\ntype SectionBuntDB struct {\n\tPath string `yaml:\"path\"`\n}\n\n// SectionLevelDB is sub section of config.\ntype SectionLevelDB struct {\n\tPath string `yaml:\"path\"`\n}\n\n// SectionBadgerDB is sub section of config.\ntype SectionBadgerDB struct {\n\tPath string `yaml:\"path\"`\n}\n\n// SectionPID is sub section of config.\ntype SectionPID struct {\n\tEnabled  bool   `yaml:\"enabled\"`\n\tPath     string `yaml:\"path\"`\n\tOverride bool   `yaml:\"override\"`\n}\n\n// SectionGRPC is sub section of config.\ntype SectionGRPC struct {\n\tEnabled bool   `yaml:\"enabled\"`\n\tPort    string `yaml:\"port\"`\n}\n\nfunc setDefaults() {\n\t// Core defaults\n\tviper.SetDefault(\"core.enabled\", true)\n\tviper.SetDefault(\"core.address\", \"\")\n\tviper.SetDefault(\"core.shutdown_timeout\", 30)\n\tviper.SetDefault(\"core.port\", \"8088\")\n\tviper.SetDefault(\"core.worker_num\", 0)\n\tviper.SetDefault(\"core.queue_num\", 0)\n\tviper.SetDefault(\"core.max_notification\", 100)\n\tviper.SetDefault(\"core.sync\", false)\n\tviper.SetDefault(\"core.feedback_timeout\", 10)\n\tviper.SetDefault(\"core.mode\", \"release\")\n\tviper.SetDefault(\"core.ssl\", false)\n\tviper.SetDefault(\"core.cert_path\", \"cert.pem\")\n\tviper.SetDefault(\"core.key_path\", \"key.pem\")\n\tviper.SetDefault(\"core.pid.enabled\", false)\n\tviper.SetDefault(\"core.pid.path\", \"gorush.pid\")\n\tviper.SetDefault(\"core.pid.override\", true)\n\tviper.SetDefault(\"core.auto_tls.enabled\", false)\n\tviper.SetDefault(\"core.auto_tls.folder\", \".cache\")\n\n\t// iOS defaults\n\tviper.SetDefault(\"ios.enabled\", false)\n\tviper.SetDefault(\"ios.key_type\", \"pem\")\n\tviper.SetDefault(\"ios.production\", false)\n\tviper.SetDefault(\"ios.max_concurrent_pushes\", uint(100))\n\tviper.SetDefault(\"ios.max_retry\", 0)\n\n\t// Android defaults\n\tviper.SetDefault(\"android.enabled\", true)\n\tviper.SetDefault(\"android.max_retry\", 0)\n\n\t// API defaults\n\tviper.SetDefault(\"api.push_uri\", \"/api/push\")\n\tviper.SetDefault(\"api.stat_go_uri\", \"/api/stat/go\")\n\tviper.SetDefault(\"api.stat_app_uri\", \"/api/stat/app\")\n\tviper.SetDefault(\"api.config_uri\", \"/api/config\")\n\tviper.SetDefault(\"api.sys_stat_uri\", \"/sys/stats\")\n\tviper.SetDefault(\"api.metric_uri\", \"/metrics\")\n\tviper.SetDefault(\"api.health_uri\", \"/healthz\")\n\n\t// gRPC defaults\n\tviper.SetDefault(\"grpc.enabled\", false)\n\tviper.SetDefault(\"grpc.port\", \"9000\")\n\n\t// Queue defaults\n\tviper.SetDefault(\"queue.engine\", \"local\")\n\n\t// Stat defaults\n\tviper.SetDefault(\"stat.engine\", \"memory\")\n\n\t// Log defaults\n\tviper.SetDefault(\"log.format\", \"string\")\n\tviper.SetDefault(\"log.access_log\", \"stdout\")\n\tviper.SetDefault(\"log.access_level\", \"debug\")\n\tviper.SetDefault(\"log.error_log\", \"stderr\")\n\tviper.SetDefault(\"log.error_level\", \"error\")\n\tviper.SetDefault(\"log.hide_token\", true)\n\tviper.SetDefault(\"log.hide_messages\", false)\n}\n\n// LoadConf load config from file and read in environment variables that match\nfunc LoadConf(confPath ...string) (*ConfYaml, error) {\n\t// load default values\n\tsetDefaults()\n\n\tviper.SetConfigType(\"yaml\")\n\tviper.AutomaticEnv()         // read in environment variables that match\n\tviper.SetEnvPrefix(\"gorush\") // will be uppercased automatically\n\tviper.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))\n\n\t// If config path is provided, load from file\n\tif len(confPath) > 0 && confPath[0] != \"\" {\n\t\tcontent, err := os.ReadFile(confPath[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read config file %s: %w\", confPath[0], err)\n\t\t}\n\n\t\tif err := viper.ReadConfig(bytes.NewBuffer(content)); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse config file %s: %w\", confPath[0], err)\n\t\t}\n\n\t\t// Early return after successfully loading config from file\n\t\treturn loadConfigFromViper()\n\t}\n\n\t// Search config in home directory with name \".gorush\" (without extension).\n\tviper.AddConfigPath(\"/etc/gorush/\")\n\tviper.AddConfigPath(\"$HOME/.gorush\")\n\tviper.AddConfigPath(\".\")\n\tviper.SetConfigName(\"config\")\n\n\t// If a config file is found, read it in.\n\tif err := viper.ReadInConfig(); err == nil {\n\t\tlog.Println(\"Using config file:\", viper.ConfigFileUsed())\n\t\treturn loadConfigFromViper()\n\t}\n\n\t// Try to load default config as fallback\n\tif err := viper.ReadConfig(bytes.NewBuffer(defaultConf)); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to load default config and no config file found: %w\", err)\n\t}\n\n\treturn loadConfigFromViper()\n}\n\n// loadConfigFromViper extracts config loading logic to avoid code duplication\nfunc loadConfigFromViper() (*ConfYaml, error) {\n\tconf := &ConfYaml{}\n\n\t// Core\n\tconf.Core.Address = viper.GetString(\"core.address\")\n\tconf.Core.Port = viper.GetString(\"core.port\")\n\tconf.Core.ShutdownTimeout = int64(viper.GetInt(\"core.shutdown_timeout\"))\n\tconf.Core.Enabled = viper.GetBool(\"core.enabled\")\n\tconf.Core.WorkerNum = int64(viper.GetInt(\"core.worker_num\"))\n\tconf.Core.QueueNum = int64(viper.GetInt(\"core.queue_num\"))\n\tconf.Core.Mode = viper.GetString(\"core.mode\")\n\tconf.Core.Sync = viper.GetBool(\"core.sync\")\n\tconf.Core.FeedbackURL = viper.GetString(\"core.feedback_hook_url\")\n\tconf.Core.FeedbackTimeout = int64(viper.GetInt(\"core.feedback_timeout\"))\n\tconf.Core.FeedbackHeader = viper.GetStringSlice(\"core.feedback_header\")\n\tconf.Core.SSL = viper.GetBool(\"core.ssl\")\n\tconf.Core.CertPath = viper.GetString(\"core.cert_path\")\n\tconf.Core.KeyPath = viper.GetString(\"core.key_path\")\n\tconf.Core.CertBase64 = viper.GetString(\"core.cert_base64\")\n\tconf.Core.KeyBase64 = viper.GetString(\"core.key_base64\")\n\tconf.Core.MaxNotification = int64(viper.GetInt(\"core.max_notification\"))\n\tconf.Core.HTTPProxy = viper.GetString(\"core.http_proxy\")\n\tconf.Core.PID.Enabled = viper.GetBool(\"core.pid.enabled\")\n\tconf.Core.PID.Path = viper.GetString(\"core.pid.path\")\n\tconf.Core.PID.Override = viper.GetBool(\"core.pid.override\")\n\tconf.Core.AutoTLS.Enabled = viper.GetBool(\"core.auto_tls.enabled\")\n\tconf.Core.AutoTLS.Folder = viper.GetString(\"core.auto_tls.folder\")\n\tconf.Core.AutoTLS.Host = viper.GetString(\"core.auto_tls.host\")\n\n\t// Api\n\tconf.API.PushURI = viper.GetString(\"api.push_uri\")\n\tconf.API.StatGoURI = viper.GetString(\"api.stat_go_uri\")\n\tconf.API.StatAppURI = viper.GetString(\"api.stat_app_uri\")\n\tconf.API.ConfigURI = viper.GetString(\"api.config_uri\")\n\tconf.API.SysStatURI = viper.GetString(\"api.sys_stat_uri\")\n\tconf.API.MetricURI = viper.GetString(\"api.metric_uri\")\n\tconf.API.HealthURI = viper.GetString(\"api.health_uri\")\n\n\t// Android\n\tconf.Android.Enabled = viper.GetBool(\"android.enabled\")\n\tconf.Android.KeyPath = viper.GetString(\"android.key_path\")\n\tconf.Android.Credential = viper.GetString(\"android.credential\")\n\tconf.Android.MaxRetry = viper.GetInt(\"android.max_retry\")\n\n\t// Huawei\n\tconf.Huawei.Enabled = viper.GetBool(\"huawei.enabled\")\n\tconf.Huawei.AppSecret = viper.GetString(\"huawei.appsecret\")\n\tconf.Huawei.AppID = viper.GetString(\"huawei.appid\")\n\tconf.Huawei.MaxRetry = viper.GetInt(\"huawei.max_retry\")\n\n\t// iOS\n\tconf.Ios.Enabled = viper.GetBool(\"ios.enabled\")\n\tconf.Ios.KeyPath = viper.GetString(\"ios.key_path\")\n\tconf.Ios.KeyBase64 = viper.GetString(\"ios.key_base64\")\n\tconf.Ios.KeyType = viper.GetString(\"ios.key_type\")\n\tconf.Ios.Password = viper.GetString(\"ios.password\")\n\tconf.Ios.Production = viper.GetBool(\"ios.production\")\n\tconf.Ios.MaxConcurrentPushes = viper.GetUint(\"ios.max_concurrent_pushes\")\n\tconf.Ios.MaxRetry = viper.GetInt(\"ios.max_retry\")\n\tconf.Ios.KeyID = viper.GetString(\"ios.key_id\")\n\tconf.Ios.TeamID = viper.GetString(\"ios.team_id\")\n\n\t// log\n\tconf.Log.Format = viper.GetString(\"log.format\")\n\tconf.Log.AccessLog = viper.GetString(\"log.access_log\")\n\tconf.Log.AccessLevel = viper.GetString(\"log.access_level\")\n\tconf.Log.ErrorLog = viper.GetString(\"log.error_log\")\n\tconf.Log.ErrorLevel = viper.GetString(\"log.error_level\")\n\tconf.Log.HideToken = viper.GetBool(\"log.hide_token\")\n\tconf.Log.HideMessages = viper.GetBool(\"log.hide_messages\")\n\n\t// Queue Engine\n\tconf.Queue.Engine = viper.GetString(\"queue.engine\")\n\tconf.Queue.NSQ.Addr = viper.GetString(\"queue.nsq.addr\")\n\tconf.Queue.NSQ.Topic = viper.GetString(\"queue.nsq.topic\")\n\tconf.Queue.NSQ.Channel = viper.GetString(\"queue.nsq.channel\")\n\tconf.Queue.NATS.Addr = viper.GetString(\"queue.nats.addr\")\n\tconf.Queue.NATS.Subj = viper.GetString(\"queue.nats.subj\")\n\tconf.Queue.NATS.Queue = viper.GetString(\"queue.nats.queue\")\n\tconf.Queue.Redis.Addr = viper.GetString(\"queue.redis.addr\")\n\tconf.Queue.Redis.StreamName = viper.GetString(\"queue.redis.stream_name\")\n\tconf.Queue.Redis.Group = viper.GetString(\"queue.redis.group\")\n\tconf.Queue.Redis.Consumer = viper.GetString(\"queue.redis.consumer\")\n\tconf.Queue.Redis.WithTLS = viper.GetBool(\"queue.redis.with_tls\")\n\tconf.Queue.Redis.Username = viper.GetString(\"queue.redis.username\")\n\tconf.Queue.Redis.Password = viper.GetString(\"queue.redis.password\")\n\tconf.Queue.Redis.DB = viper.GetInt(\"queue.redis.db\")\n\n\t// Stat Engine\n\tconf.Stat.Engine = viper.GetString(\"stat.engine\")\n\tconf.Stat.Redis.Cluster = viper.GetBool(\"stat.redis.cluster\")\n\tconf.Stat.Redis.Addr = viper.GetString(\"stat.redis.addr\")\n\tconf.Stat.Redis.Username = viper.GetString(\"stat.redis.username\")\n\tconf.Stat.Redis.Password = viper.GetString(\"stat.redis.password\")\n\tconf.Stat.Redis.DB = viper.GetInt(\"stat.redis.db\")\n\tconf.Stat.BoltDB.Path = viper.GetString(\"stat.boltdb.path\")\n\tconf.Stat.BoltDB.Bucket = viper.GetString(\"stat.boltdb.bucket\")\n\tconf.Stat.BuntDB.Path = viper.GetString(\"stat.buntdb.path\")\n\tconf.Stat.LevelDB.Path = viper.GetString(\"stat.leveldb.path\")\n\tconf.Stat.BadgerDB.Path = viper.GetString(\"stat.badgerdb.path\")\n\n\t// gRPC Server\n\tconf.GRPC.Enabled = viper.GetBool(\"grpc.enabled\")\n\tconf.GRPC.Port = viper.GetString(\"grpc.port\")\n\n\tif conf.Core.WorkerNum == int64(0) {\n\t\tconf.Core.WorkerNum = int64(runtime.NumCPU())\n\t}\n\n\tif conf.Core.QueueNum == int64(0) {\n\t\tconf.Core.QueueNum = int64(8192)\n\t}\n\n\treturn conf, nil\n}\n\n// ValidatePort validates that a port string is within valid range\nfunc ValidatePort(port string) error {\n\tif port == \"\" {\n\t\treturn nil // empty port is allowed, will use default\n\t}\n\tp, err := strconv.Atoi(port)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid port format: %s\", port)\n\t}\n\tif p < 1 || p > 65535 {\n\t\treturn fmt.Errorf(\"port out of range (1-65535): %d\", p)\n\t}\n\treturn nil\n}\n\n// ValidateAddress validates that an address is not empty and contains valid characters\nfunc ValidateAddress(addr string) error {\n\tif addr == \"\" {\n\t\treturn nil // empty address is allowed, will bind to all interfaces\n\t}\n\t// Basic validation - check if it's a valid IP or hostname format\n\tif net.ParseIP(addr) == nil {\n\t\t// If not a valid IP, check if it could be a hostname (basic check)\n\t\tif len(addr) > 253 || strings.Contains(addr, \"..\") {\n\t\t\treturn fmt.Errorf(\"invalid address format: %s\", addr)\n\t\t}\n\t}\n\treturn nil\n}\n\n// ValidatePIDPath validates and sanitizes PID file path to prevent path traversal\nfunc ValidatePIDPath(pidPath string) error {\n\tif pidPath == \"\" {\n\t\treturn nil\n\t}\n\n\t// Clean the path to resolve any . or .. elements\n\tcleanPath := filepath.Clean(pidPath)\n\n\t// Check for path traversal attempts by looking for \"..\" in the cleaned path\n\t// If a relative path attempts to traverse upwards, it will still have a \"..\" prefix after cleaning\n\tif !filepath.IsAbs(cleanPath) && strings.HasPrefix(cleanPath, \"..\") {\n\t\treturn fmt.Errorf(\"path traversal detected in PID path: %s\", pidPath)\n\t}\n\n\t// Ensure the path is not trying to write to sensitive system directories\n\tsensitive := []string{\"/etc/\", \"/usr/\", \"/var/\", \"/sys/\", \"/proc/\"}\n\tfor _, prefix := range sensitive {\n\t\tif strings.HasPrefix(cleanPath, prefix) {\n\t\t\treturn fmt.Errorf(\"cannot write PID file to sensitive directory: %s\", cleanPath)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ValidateConfig validates critical configuration parameters\nfunc ValidateConfig(cfg *ConfYaml) error {\n\tif err := ValidatePort(cfg.Core.Port); err != nil {\n\t\treturn fmt.Errorf(\"invalid core port: %w\", err)\n\t}\n\n\tif err := ValidateAddress(cfg.Core.Address); err != nil {\n\t\treturn fmt.Errorf(\"invalid core address: %w\", err)\n\t}\n\n\tif err := ValidatePIDPath(cfg.Core.PID.Path); err != nil {\n\t\treturn fmt.Errorf(\"invalid PID path: %w\", err)\n\t}\n\n\t// Validate Redis address if Redis is enabled\n\tif cfg.Stat.Engine == \"redis\" && cfg.Stat.Redis.Addr != \"\" {\n\t\thost, port, err := net.SplitHostPort(cfg.Stat.Redis.Addr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid Redis address format: %s\", cfg.Stat.Redis.Addr)\n\t\t}\n\t\tif err := ValidateAddress(host); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid Redis host: %w\", err)\n\t\t}\n\t\tif err := ValidatePort(port); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid Redis port: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// SanitizedCopy returns a copy of the config with sensitive fields redacted.\nfunc (c *ConfYaml) SanitizedCopy() *ConfYaml {\n\tsanitized := *c\n\n\t// Core TLS & proxy\n\tsanitized.Core.CertBase64 = redact(c.Core.CertBase64)\n\tsanitized.Core.KeyBase64 = redact(c.Core.KeyBase64)\n\tsanitized.Core.HTTPProxy = redact(c.Core.HTTPProxy)\n\tsanitized.Core.CertPath = redact(c.Core.CertPath)\n\tsanitized.Core.KeyPath = redact(c.Core.KeyPath)\n\tif len(c.Core.FeedbackHeader) > 0 {\n\t\tsanitized.Core.FeedbackHeader = make([]string, len(c.Core.FeedbackHeader))\n\t\tfor i, v := range c.Core.FeedbackHeader {\n\t\t\tif v != \"\" {\n\t\t\t\tsanitized.Core.FeedbackHeader[i] = \"[REDACTED]\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// Android\n\tsanitized.Android.KeyPath = redact(c.Android.KeyPath)\n\tsanitized.Android.Credential = redact(c.Android.Credential)\n\n\t// Huawei\n\tsanitized.Huawei.AppSecret = redact(c.Huawei.AppSecret)\n\n\t// iOS\n\tsanitized.Ios.KeyPath = redact(c.Ios.KeyPath)\n\tsanitized.Ios.KeyBase64 = redact(c.Ios.KeyBase64)\n\tsanitized.Ios.Password = redact(c.Ios.Password)\n\tsanitized.Ios.KeyID = redact(c.Ios.KeyID)\n\tsanitized.Ios.TeamID = redact(c.Ios.TeamID)\n\n\t// Queue Redis\n\tsanitized.Queue.Redis.Username = redact(c.Queue.Redis.Username)\n\tsanitized.Queue.Redis.Password = redact(c.Queue.Redis.Password)\n\n\t// Stat Redis\n\tsanitized.Stat.Redis.Username = redact(c.Stat.Redis.Username)\n\tsanitized.Stat.Redis.Password = redact(c.Stat.Redis.Password)\n\n\treturn &sanitized\n}\n\nfunc redact(s string) string {\n\tif s != \"\" {\n\t\treturn \"[REDACTED]\"\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "config/config_test.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/stretchr/testify/suite\"\n)\n\n// Test file is missing\nfunc TestMissingFile(t *testing.T) {\n\tfilename := \"nonexistent_file.yml\"\n\tconf, err := LoadConf(filename)\n\n\tassert.Nil(t, conf)\n\trequire.Error(t, err)\n\t// Check that the error message includes the filename and describes the issue\n\tassert.Contains(t, err.Error(), \"failed to read config file\")\n\tassert.Contains(t, err.Error(), filename)\n}\n\n// Test invalid YAML content\nfunc TestInvalidYAMLFile(t *testing.T) {\n\t// Create a temporary file with invalid YAML\n\ttmpFile := \"test_invalid.yml\"\n\tcontent := []byte(\"invalid: yaml: content: [unclosed\")\n\n\t// Write invalid content to a temporary file\n\terr := os.WriteFile(tmpFile, content, 0o600)\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpFile) // Clean up\n\n\tconf, err := LoadConf(tmpFile)\n\n\tassert.Nil(t, conf)\n\trequire.Error(t, err)\n\t// Check that the error message includes the filename and describes parsing failure\n\tassert.Contains(t, err.Error(), \"failed to parse config file\")\n\tassert.Contains(t, err.Error(), tmpFile)\n}\n\n// Test improved error messages for default config loading\nfunc TestDefaultConfigLoadFailure(t *testing.T) {\n\t// Backup original defaultConf\n\toriginalDefaultConf := defaultConf\n\tdefer func() {\n\t\tdefaultConf = originalDefaultConf\n\t}()\n\n\t// Set invalid default config\n\tdefaultConf = []byte(`invalid: yaml: [unclosed`)\n\n\tconf, err := LoadConf()\n\n\tassert.Nil(t, conf)\n\trequire.Error(t, err)\n\t// Check that the error message describes the default config loading failure\n\tassert.Contains(t, err.Error(), \"failed to load default config and no config file found\")\n}\n\nfunc TestEmptyConfig(t *testing.T) {\n\tconf, err := LoadConf(\"testdata/empty.yml\")\n\tif err != nil {\n\t\tpanic(\"failed to load config.yml from file\")\n\t}\n\n\tassert.Equal(t, uint(100), conf.Ios.MaxConcurrentPushes)\n}\n\ntype ConfigTestSuite struct {\n\tsuite.Suite\n\tConfGorushDefault *ConfYaml\n\tConfGorush        *ConfYaml\n}\n\nfunc (suite *ConfigTestSuite) SetupTest() {\n\tvar err error\n\tsuite.ConfGorushDefault, err = LoadConf()\n\tif err != nil {\n\t\tpanic(\"failed to load default config.yml\")\n\t}\n\tsuite.ConfGorush, err = LoadConf(\"testdata/config.yml\")\n\tif err != nil {\n\t\tpanic(\"failed to load config.yml from file\")\n\t}\n}\n\nfunc (suite *ConfigTestSuite) TestValidateConfDefault() {\n\t// Core\n\tsuite.Empty(suite.ConfGorushDefault.Core.Address)\n\tsuite.Equal(\"8088\", suite.ConfGorushDefault.Core.Port)\n\tsuite.Equal(int64(30), suite.ConfGorushDefault.Core.ShutdownTimeout)\n\tsuite.True(suite.ConfGorushDefault.Core.Enabled)\n\tsuite.Equal(int64(runtime.NumCPU()), suite.ConfGorushDefault.Core.WorkerNum)\n\tsuite.Equal(int64(8192), suite.ConfGorushDefault.Core.QueueNum)\n\tsuite.Equal(\"release\", suite.ConfGorushDefault.Core.Mode)\n\tsuite.False(suite.ConfGorushDefault.Core.Sync)\n\tsuite.Empty(suite.ConfGorushDefault.Core.FeedbackURL)\n\tsuite.Empty(suite.ConfGorushDefault.Core.FeedbackHeader)\n\tsuite.Equal(int64(10), suite.ConfGorushDefault.Core.FeedbackTimeout)\n\tsuite.False(suite.ConfGorushDefault.Core.SSL)\n\tsuite.Equal(\"cert.pem\", suite.ConfGorushDefault.Core.CertPath)\n\tsuite.Equal(\"key.pem\", suite.ConfGorushDefault.Core.KeyPath)\n\tsuite.Empty(suite.ConfGorushDefault.Core.KeyBase64)\n\tsuite.Empty(suite.ConfGorushDefault.Core.CertBase64)\n\tsuite.Equal(int64(100), suite.ConfGorushDefault.Core.MaxNotification)\n\tsuite.Empty(suite.ConfGorushDefault.Core.HTTPProxy)\n\t// Pid\n\tsuite.False(suite.ConfGorushDefault.Core.PID.Enabled)\n\tsuite.Equal(\"gorush.pid\", suite.ConfGorushDefault.Core.PID.Path)\n\tsuite.True(suite.ConfGorushDefault.Core.PID.Override)\n\tsuite.False(suite.ConfGorushDefault.Core.AutoTLS.Enabled)\n\tsuite.Equal(\".cache\", suite.ConfGorushDefault.Core.AutoTLS.Folder)\n\tsuite.Empty(suite.ConfGorushDefault.Core.AutoTLS.Host)\n\n\t// Api\n\tsuite.Equal(\"/api/push\", suite.ConfGorushDefault.API.PushURI)\n\tsuite.Equal(\"/api/stat/go\", suite.ConfGorushDefault.API.StatGoURI)\n\tsuite.Equal(\"/api/stat/app\", suite.ConfGorushDefault.API.StatAppURI)\n\tsuite.Equal(\"/api/config\", suite.ConfGorushDefault.API.ConfigURI)\n\tsuite.Equal(\"/sys/stats\", suite.ConfGorushDefault.API.SysStatURI)\n\tsuite.Equal(\"/metrics\", suite.ConfGorushDefault.API.MetricURI)\n\tsuite.Equal(\"/healthz\", suite.ConfGorushDefault.API.HealthURI)\n\n\t// Android\n\tsuite.True(suite.ConfGorushDefault.Android.Enabled)\n\tsuite.Empty(suite.ConfGorushDefault.Android.KeyPath)\n\tsuite.Empty(suite.ConfGorushDefault.Android.Credential)\n\tsuite.Equal(0, suite.ConfGorushDefault.Android.MaxRetry)\n\n\t// iOS\n\tsuite.False(suite.ConfGorushDefault.Ios.Enabled)\n\tsuite.Empty(suite.ConfGorushDefault.Ios.KeyPath)\n\tsuite.Empty(suite.ConfGorushDefault.Ios.KeyBase64)\n\tsuite.Equal(\"pem\", suite.ConfGorushDefault.Ios.KeyType)\n\tsuite.Empty(suite.ConfGorushDefault.Ios.Password)\n\tsuite.False(suite.ConfGorushDefault.Ios.Production)\n\tsuite.Equal(uint(100), suite.ConfGorushDefault.Ios.MaxConcurrentPushes)\n\tsuite.Equal(0, suite.ConfGorushDefault.Ios.MaxRetry)\n\tsuite.Empty(suite.ConfGorushDefault.Ios.KeyID)\n\tsuite.Empty(suite.ConfGorushDefault.Ios.TeamID)\n\n\t// queue\n\tsuite.Equal(\"local\", suite.ConfGorushDefault.Queue.Engine)\n\tsuite.Equal(\"127.0.0.1:4150\", suite.ConfGorushDefault.Queue.NSQ.Addr)\n\tsuite.Equal(\"gorush\", suite.ConfGorushDefault.Queue.NSQ.Topic)\n\tsuite.Equal(\"gorush\", suite.ConfGorushDefault.Queue.NSQ.Channel)\n\n\tsuite.Equal(\"127.0.0.1:4222\", suite.ConfGorushDefault.Queue.NATS.Addr)\n\tsuite.Equal(\"gorush\", suite.ConfGorushDefault.Queue.NATS.Subj)\n\tsuite.Equal(\"gorush\", suite.ConfGorushDefault.Queue.NATS.Queue)\n\n\tsuite.Equal(\"127.0.0.1:6379\", suite.ConfGorushDefault.Queue.Redis.Addr)\n\tsuite.Equal(\"gorush\", suite.ConfGorushDefault.Queue.Redis.StreamName)\n\tsuite.Equal(\"gorush\", suite.ConfGorushDefault.Queue.Redis.Group)\n\tsuite.Equal(\"gorush\", suite.ConfGorushDefault.Queue.Redis.Consumer)\n\tsuite.Empty(suite.ConfGorushDefault.Queue.Redis.Username)\n\tsuite.Empty(suite.ConfGorushDefault.Queue.Redis.Password)\n\tsuite.False(suite.ConfGorushDefault.Queue.Redis.WithTLS)\n\tsuite.Equal(0, suite.ConfGorushDefault.Queue.Redis.DB)\n\n\t// log\n\tsuite.Equal(\"string\", suite.ConfGorushDefault.Log.Format)\n\tsuite.Equal(\"stdout\", suite.ConfGorushDefault.Log.AccessLog)\n\tsuite.Equal(\"debug\", suite.ConfGorushDefault.Log.AccessLevel)\n\tsuite.Equal(\"stderr\", suite.ConfGorushDefault.Log.ErrorLog)\n\tsuite.Equal(\"error\", suite.ConfGorushDefault.Log.ErrorLevel)\n\tsuite.True(suite.ConfGorushDefault.Log.HideToken)\n\tsuite.False(suite.ConfGorushDefault.Log.HideMessages)\n\n\tsuite.Equal(\"memory\", suite.ConfGorushDefault.Stat.Engine)\n\tsuite.False(suite.ConfGorushDefault.Stat.Redis.Cluster)\n\tsuite.Equal(\"localhost:6379\", suite.ConfGorushDefault.Stat.Redis.Addr)\n\tsuite.Empty(suite.ConfGorushDefault.Stat.Redis.Username)\n\tsuite.Empty(suite.ConfGorushDefault.Stat.Redis.Password)\n\tsuite.Equal(0, suite.ConfGorushDefault.Stat.Redis.DB)\n\n\tsuite.Equal(\"bolt.db\", suite.ConfGorushDefault.Stat.BoltDB.Path)\n\tsuite.Equal(\"gorush\", suite.ConfGorushDefault.Stat.BoltDB.Bucket)\n\n\tsuite.Equal(\"bunt.db\", suite.ConfGorushDefault.Stat.BuntDB.Path)\n\tsuite.Equal(\"level.db\", suite.ConfGorushDefault.Stat.LevelDB.Path)\n\tsuite.Equal(\"badger.db\", suite.ConfGorushDefault.Stat.BadgerDB.Path)\n\n\t// gRPC\n\tsuite.False(suite.ConfGorushDefault.GRPC.Enabled)\n\tsuite.Equal(\"9000\", suite.ConfGorushDefault.GRPC.Port)\n}\n\nfunc (suite *ConfigTestSuite) TestValidateConf() {\n\t// Core\n\tsuite.Equal(\"8088\", suite.ConfGorush.Core.Port)\n\tsuite.Equal(int64(30), suite.ConfGorush.Core.ShutdownTimeout)\n\tsuite.True(suite.ConfGorush.Core.Enabled)\n\tsuite.Equal(int64(runtime.NumCPU()), suite.ConfGorush.Core.WorkerNum)\n\tsuite.Equal(int64(8192), suite.ConfGorush.Core.QueueNum)\n\tsuite.Equal(\"release\", suite.ConfGorush.Core.Mode)\n\tsuite.False(suite.ConfGorush.Core.Sync)\n\tsuite.Empty(suite.ConfGorush.Core.FeedbackURL)\n\tsuite.Equal(int64(10), suite.ConfGorush.Core.FeedbackTimeout)\n\tsuite.Len(suite.ConfGorush.Core.FeedbackHeader, 1)\n\tsuite.Equal(\n\t\t\"x-gorush-token:4e989115e09680f44a645519fed6a976\",\n\t\tsuite.ConfGorush.Core.FeedbackHeader[0],\n\t)\n\tsuite.False(suite.ConfGorush.Core.SSL)\n\tsuite.Equal(\"cert.pem\", suite.ConfGorush.Core.CertPath)\n\tsuite.Equal(\"key.pem\", suite.ConfGorush.Core.KeyPath)\n\tsuite.Empty(suite.ConfGorush.Core.CertBase64)\n\tsuite.Empty(suite.ConfGorush.Core.KeyBase64)\n\tsuite.Equal(int64(100), suite.ConfGorush.Core.MaxNotification)\n\tsuite.Empty(suite.ConfGorush.Core.HTTPProxy)\n\t// Pid\n\tsuite.False(suite.ConfGorush.Core.PID.Enabled)\n\tsuite.Equal(\"gorush.pid\", suite.ConfGorush.Core.PID.Path)\n\tsuite.True(suite.ConfGorush.Core.PID.Override)\n\tsuite.False(suite.ConfGorush.Core.AutoTLS.Enabled)\n\tsuite.Equal(\".cache\", suite.ConfGorush.Core.AutoTLS.Folder)\n\tsuite.Empty(suite.ConfGorush.Core.AutoTLS.Host)\n\n\t// Api\n\tsuite.Equal(\"/api/push\", suite.ConfGorush.API.PushURI)\n\tsuite.Equal(\"/api/stat/go\", suite.ConfGorush.API.StatGoURI)\n\tsuite.Equal(\"/api/stat/app\", suite.ConfGorush.API.StatAppURI)\n\tsuite.Equal(\"/api/config\", suite.ConfGorush.API.ConfigURI)\n\tsuite.Equal(\"/sys/stats\", suite.ConfGorush.API.SysStatURI)\n\tsuite.Equal(\"/metrics\", suite.ConfGorush.API.MetricURI)\n\tsuite.Equal(\"/healthz\", suite.ConfGorush.API.HealthURI)\n\n\t// Android\n\tsuite.True(suite.ConfGorush.Android.Enabled)\n\tsuite.Equal(\"key.json\", suite.ConfGorush.Android.KeyPath)\n\tsuite.Equal(\"CREDENTIAL_JSON_DATA\", suite.ConfGorush.Android.Credential)\n\tsuite.Equal(0, suite.ConfGorush.Android.MaxRetry)\n\n\t// iOS\n\tsuite.False(suite.ConfGorush.Ios.Enabled)\n\tsuite.Equal(\"key.pem\", suite.ConfGorush.Ios.KeyPath)\n\tsuite.Empty(suite.ConfGorush.Ios.KeyBase64)\n\tsuite.Equal(\"pem\", suite.ConfGorush.Ios.KeyType)\n\tsuite.Empty(suite.ConfGorush.Ios.Password)\n\tsuite.False(suite.ConfGorush.Ios.Production)\n\tsuite.Equal(uint(100), suite.ConfGorush.Ios.MaxConcurrentPushes)\n\tsuite.Equal(0, suite.ConfGorush.Ios.MaxRetry)\n\tsuite.Empty(suite.ConfGorush.Ios.KeyID)\n\tsuite.Empty(suite.ConfGorush.Ios.TeamID)\n\n\t// log\n\tsuite.Equal(\"string\", suite.ConfGorush.Log.Format)\n\tsuite.Equal(\"stdout\", suite.ConfGorush.Log.AccessLog)\n\tsuite.Equal(\"debug\", suite.ConfGorush.Log.AccessLevel)\n\tsuite.Equal(\"stderr\", suite.ConfGorush.Log.ErrorLog)\n\tsuite.Equal(\"error\", suite.ConfGorush.Log.ErrorLevel)\n\tsuite.True(suite.ConfGorush.Log.HideToken)\n\n\tsuite.Equal(\"memory\", suite.ConfGorush.Stat.Engine)\n\tsuite.False(suite.ConfGorush.Stat.Redis.Cluster)\n\tsuite.Equal(\"localhost:6379\", suite.ConfGorush.Stat.Redis.Addr)\n\tsuite.Empty(suite.ConfGorush.Stat.Redis.Username)\n\tsuite.Empty(suite.ConfGorush.Stat.Redis.Password)\n\tsuite.Equal(0, suite.ConfGorush.Stat.Redis.DB)\n\n\tsuite.Equal(\"bolt.db\", suite.ConfGorush.Stat.BoltDB.Path)\n\tsuite.Equal(\"gorush\", suite.ConfGorush.Stat.BoltDB.Bucket)\n\n\tsuite.Equal(\"bunt.db\", suite.ConfGorush.Stat.BuntDB.Path)\n\tsuite.Equal(\"level.db\", suite.ConfGorush.Stat.LevelDB.Path)\n\tsuite.Equal(\"badger.db\", suite.ConfGorush.Stat.BadgerDB.Path)\n\n\t// gRPC\n\tsuite.False(suite.ConfGorush.GRPC.Enabled)\n\tsuite.Equal(\"9000\", suite.ConfGorush.GRPC.Port)\n}\n\nfunc TestConfigTestSuite(t *testing.T) {\n\tsuite.Run(t, new(ConfigTestSuite))\n}\n\nfunc TestLoadConfigFromEnv(t *testing.T) {\n\tt.Setenv(\"GORUSH_CORE_PORT\", \"9001\")\n\tt.Setenv(\"GORUSH_GRPC_ENABLED\", \"true\")\n\tt.Setenv(\"GORUSH_CORE_MAX_NOTIFICATION\", \"200\")\n\tt.Setenv(\"GORUSH_IOS_KEY_ID\", \"ABC123DEFG\")\n\tt.Setenv(\"GORUSH_IOS_TEAM_ID\", \"DEF123GHIJ\")\n\tt.Setenv(\"GORUSH_API_HEALTH_URI\", \"/healthz\")\n\tt.Setenv(\"GORUSH_CORE_FEEDBACK_HOOK_URL\", \"http://example.com\")\n\tt.Setenv(\"GORUSH_CORE_FEEDBACK_HEADER\", \"x-api-key:1234567890 x-auth-key:0987654321\")\n\n\tConfGorush, err := LoadConf(\"testdata/config.yml\")\n\tif err != nil {\n\t\tpanic(\"failed to load config.yml from file\")\n\t}\n\tassert.Equal(t, \"9001\", ConfGorush.Core.Port)\n\tassert.Equal(t, int64(200), ConfGorush.Core.MaxNotification)\n\tassert.True(t, ConfGorush.GRPC.Enabled)\n\tassert.Equal(t, \"ABC123DEFG\", ConfGorush.Ios.KeyID)\n\tassert.Equal(t, \"DEF123GHIJ\", ConfGorush.Ios.TeamID)\n\tassert.Equal(t, \"/healthz\", ConfGorush.API.HealthURI)\n\tassert.Equal(t, \"http://example.com\", ConfGorush.Core.FeedbackURL)\n\tassert.Equal(t, \"x-api-key:1234567890\", ConfGorush.Core.FeedbackHeader[0])\n\tassert.Equal(t, \"x-auth-key:0987654321\", ConfGorush.Core.FeedbackHeader[1])\n}\n\nfunc TestRedisDBConfiguration(t *testing.T) {\n\t// Test loading Redis DB configuration from file\n\tconf, err := LoadConf(\"testdata/redis_db_config.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to load redis_db_config.yml: %v\", err)\n\t}\n\n\t// Test queue.redis.db is properly loaded\n\tassert.Equal(t, \"redis\", conf.Queue.Engine)\n\tassert.Equal(t, 5, conf.Queue.Redis.DB)\n\n\t// Test stat.redis.db is properly loaded\n\tassert.Equal(t, \"redis\", conf.Stat.Engine)\n\tassert.Equal(t, 3, conf.Stat.Redis.DB)\n}\n\nfunc TestRedisDBConfigurationFromEnv(t *testing.T) {\n\t// Test loading Redis DB configuration from environment variables\n\tt.Setenv(\"GORUSH_QUEUE_REDIS_DB\", \"7\")\n\tt.Setenv(\"GORUSH_STAT_REDIS_DB\", \"9\")\n\n\tconf, err := LoadConf()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to load config: %v\", err)\n\t}\n\n\t// Test queue.redis.db is properly loaded from env\n\tassert.Equal(t, 7, conf.Queue.Redis.DB)\n\n\t// Test stat.redis.db is properly loaded from env\n\tassert.Equal(t, 9, conf.Stat.Redis.DB)\n}\n\nfunc TestLoadWrongDefaultYAMLConfig(t *testing.T) {\n\t// Backup original defaultConf\n\toriginalDefaultConf := defaultConf\n\tdefer func() {\n\t\tdefaultConf = originalDefaultConf\n\t}()\n\n\tdefaultConf = []byte(`a`)\n\t_, err := LoadConf()\n\tassert.Error(t, err)\n}\n\nfunc TestValidatePort(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tport    string\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname:    \"empty port should be valid\",\n\t\t\tport:    \"\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid port 80\",\n\t\t\tport:    \"80\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid port 8080\",\n\t\t\tport:    \"8080\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid port 65535\",\n\t\t\tport:    \"65535\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid port 1\",\n\t\t\tport:    \"1\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid port 0\",\n\t\t\tport:    \"0\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"port out of range\",\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid port 65536\",\n\t\t\tport:    \"65536\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"port out of range\",\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid port format\",\n\t\t\tport:    \"abc\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"invalid port format\",\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid port with injection\",\n\t\t\tport:    \"80;rm -rf /\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"invalid port format\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := ValidatePort(tt.port)\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"ValidatePort() expected error but got none\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif tt.errMsg != \"\" && !strings.Contains(err.Error(), tt.errMsg) {\n\t\t\t\t\tt.Errorf(\"ValidatePort() error = %v, want error containing %v\", err, tt.errMsg)\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"ValidatePort() error = %v, want nil\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateAddress(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\taddr    string\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname:    \"empty address should be valid\",\n\t\t\taddr:    \"\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid IPv4 localhost\",\n\t\t\taddr:    \"127.0.0.1\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid IPv4 all interfaces\",\n\t\t\taddr:    \"0.0.0.0\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid IPv6 localhost\",\n\t\t\taddr:    \"::1\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid hostname\",\n\t\t\taddr:    \"localhost\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid address too long\",\n\t\t\taddr:    strings.Repeat(\"a\", 254),\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"invalid address format\",\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid address with double dots\",\n\t\t\taddr:    \"test..example.com\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"invalid address format\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := ValidateAddress(tt.addr)\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"ValidateAddress() expected error but got none\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif tt.errMsg != \"\" && !strings.Contains(err.Error(), tt.errMsg) {\n\t\t\t\t\tt.Errorf(\n\t\t\t\t\t\t\"ValidateAddress() error = %v, want error containing %v\",\n\t\t\t\t\t\terr,\n\t\t\t\t\t\ttt.errMsg,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"ValidateAddress() error = %v, want nil\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidatePIDPath(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tpidPath string\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname:    \"empty path should be valid\",\n\t\t\tpidPath: \"\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid relative path\",\n\t\t\tpidPath: \"gorush.pid\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid absolute path in tmp\",\n\t\t\tpidPath: \"/tmp/gorush.pid\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid absolute path with .. components (cleaned)\",\n\t\t\tpidPath: \"/tmp/foo/../gorush.pid\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid absolute path with multiple .. components\",\n\t\t\tpidPath: \"/home/user/subdir/../gorush.pid\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"relative path traversal attack\",\n\t\t\tpidPath: \"../../../etc/passwd\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"path traversal detected\",\n\t\t},\n\t\t{\n\t\t\tname:    \"simple relative traversal\",\n\t\t\tpidPath: \"../gorush.pid\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"path traversal detected\",\n\t\t},\n\t\t{\n\t\t\tname:    \"complex relative traversal\",\n\t\t\tpidPath: \"subdir/../../gorush.pid\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"path traversal detected\",\n\t\t},\n\t\t{\n\t\t\tname:    \"attempt to write to /etc\",\n\t\t\tpidPath: \"/etc/gorush.pid\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"sensitive directory\",\n\t\t},\n\t\t{\n\t\t\tname:    \"attempt to write to /usr\",\n\t\t\tpidPath: \"/usr/bin/gorush.pid\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"sensitive directory\",\n\t\t},\n\t\t{\n\t\t\tname:    \"attempt to write to /var\",\n\t\t\tpidPath: \"/var/log/gorush.pid\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"sensitive directory\",\n\t\t},\n\t\t{\n\t\t\tname:    \"attempt to write to /sys\",\n\t\t\tpidPath: \"/sys/gorush.pid\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"sensitive directory\",\n\t\t},\n\t\t{\n\t\t\tname:    \"attempt to write to /proc\",\n\t\t\tpidPath: \"/proc/gorush.pid\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"sensitive directory\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := ValidatePIDPath(tt.pidPath)\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"ValidatePIDPath() expected error but got none\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif tt.errMsg != \"\" && !strings.Contains(err.Error(), tt.errMsg) {\n\t\t\t\t\tt.Errorf(\n\t\t\t\t\t\t\"ValidatePIDPath() error = %v, want error containing %v\",\n\t\t\t\t\t\terr,\n\t\t\t\t\t\ttt.errMsg,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"ValidatePIDPath() error = %v, want nil\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tcfg     *ConfYaml\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname: \"valid config\",\n\t\t\tcfg: &ConfYaml{\n\t\t\t\tCore: SectionCore{\n\t\t\t\t\tPort:    \"8088\",\n\t\t\t\t\tAddress: \"0.0.0.0\",\n\t\t\t\t},\n\t\t\t\tStat: SectionStat{\n\t\t\t\t\tEngine: \"memory\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid port in config\",\n\t\t\tcfg: &ConfYaml{\n\t\t\t\tCore: SectionCore{\n\t\t\t\t\tPort: \"99999\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"invalid core port\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := ValidateConfig(tt.cfg)\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"ValidateConfig() expected error but got none\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif tt.errMsg != \"\" && !strings.Contains(err.Error(), tt.errMsg) {\n\t\t\t\t\tt.Errorf(\n\t\t\t\t\t\t\"ValidateConfig() error = %v, want error containing %v\",\n\t\t\t\t\t\terr,\n\t\t\t\t\t\ttt.errMsg,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"ValidateConfig() error = %v, want nil\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Benchmark tests for security validation functions\nfunc BenchmarkValidatePort(b *testing.B) {\n\tfor b.Loop() {\n\t\t_ = ValidatePort(\"8080\")\n\t}\n}\n\nfunc BenchmarkValidateAddress(b *testing.B) {\n\tfor b.Loop() {\n\t\t_ = ValidateAddress(\"127.0.0.1\")\n\t}\n}\n\nfunc BenchmarkValidatePIDPath(b *testing.B) {\n\tfor b.Loop() {\n\t\t_ = ValidatePIDPath(\"/tmp/gorush.pid\")\n\t}\n}\n\n// Integration test for security validation\nfunc TestSecurityValidationIntegration(t *testing.T) {\n\t// Test that all validation functions work together\n\tt.Run(\"complete security validation\", func(t *testing.T) {\n\t\t// Test valid inputs\n\t\tif err := ValidatePort(\"8088\"); err != nil {\n\t\t\tt.Errorf(\"Valid port should not error: %v\", err)\n\t\t}\n\n\t\tif err := ValidateAddress(\"127.0.0.1\"); err != nil {\n\t\t\tt.Errorf(\"Valid address should not error: %v\", err)\n\t\t}\n\n\t\tif err := ValidatePIDPath(\"/tmp/test.pid\"); err != nil {\n\t\t\tt.Errorf(\"Valid PID path should not error: %v\", err)\n\t\t}\n\n\t\t// Test malicious inputs\n\t\tif err := ValidatePort(\"8080; rm -rf /\"); err == nil {\n\t\t\tt.Error(\"Malicious port should error\")\n\t\t}\n\n\t\tif err := ValidatePIDPath(\"../../../etc/passwd\"); err == nil {\n\t\t\tt.Error(\"Path traversal should error\")\n\t\t}\n\n\t\tif err := ValidatePIDPath(\"/etc/malicious.pid\"); err == nil {\n\t\t\tt.Error(\"Sensitive directory write should error\")\n\t\t}\n\t})\n}\n\nfunc TestAPIDefaultsFromEnv(t *testing.T) {\n\t// Test that API endpoint defaults can be overridden by environment variables\n\tt.Setenv(\"GORUSH_API_PUSH_URI\", \"/custom/push\")\n\tt.Setenv(\"GORUSH_API_STAT_GO_URI\", \"/custom/stat/go\")\n\tt.Setenv(\"GORUSH_API_METRIC_URI\", \"/custom/metrics\")\n\n\tconf, err := LoadConf()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to load config: %v\", err)\n\t}\n\n\tassert.Equal(t, \"/custom/push\", conf.API.PushURI)\n\tassert.Equal(t, \"/custom/stat/go\", conf.API.StatGoURI)\n\tassert.Equal(t, \"/custom/metrics\", conf.API.MetricURI)\n}\n\nfunc TestLogDefaultsFromEnv(t *testing.T) {\n\t// Test that log level defaults can be overridden by environment variables\n\tt.Setenv(\"GORUSH_LOG_ACCESS_LEVEL\", \"info\")\n\tt.Setenv(\"GORUSH_LOG_ERROR_LEVEL\", \"warn\")\n\tt.Setenv(\"GORUSH_LOG_FORMAT\", \"json\")\n\n\tconf, err := LoadConf()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to load config: %v\", err)\n\t}\n\n\tassert.Equal(t, \"info\", conf.Log.AccessLevel)\n\tassert.Equal(t, \"warn\", conf.Log.ErrorLevel)\n\tassert.Equal(t, \"json\", conf.Log.Format)\n}\n\nfunc TestLogLevelDefaultsWhenEmpty(t *testing.T) {\n\t// Test that when no log level is specified, defaults are used\n\t// This was the original bug - empty log levels caused initialization to fail\n\tconf, err := LoadConf()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to load config: %v\", err)\n\t}\n\n\t// Verify defaults are set\n\tassert.NotEmpty(t, conf.Log.AccessLevel, \"access level should have default value\")\n\tassert.NotEmpty(t, conf.Log.ErrorLevel, \"error level should have default value\")\n\tassert.Equal(t, \"debug\", conf.Log.AccessLevel)\n\tassert.Equal(t, \"error\", conf.Log.ErrorLevel)\n}\n\nfunc TestAllAPIEndpointsHaveDefaults(t *testing.T) {\n\t// Test that all API endpoints have proper defaults\n\tconf, err := LoadConf()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to load config: %v\", err)\n\t}\n\n\t// Verify all API endpoints have non-empty defaults\n\tassert.NotEmpty(t, conf.API.PushURI, \"push_uri should have default\")\n\tassert.NotEmpty(t, conf.API.StatGoURI, \"stat_go_uri should have default\")\n\tassert.NotEmpty(t, conf.API.StatAppURI, \"stat_app_uri should have default\")\n\tassert.NotEmpty(t, conf.API.ConfigURI, \"config_uri should have default\")\n\tassert.NotEmpty(t, conf.API.SysStatURI, \"sys_stat_uri should have default\")\n\tassert.NotEmpty(t, conf.API.MetricURI, \"metric_uri should have default\")\n\tassert.NotEmpty(t, conf.API.HealthURI, \"health_uri should have default\")\n\n\t// Verify correct default values\n\tassert.Equal(t, \"/api/push\", conf.API.PushURI)\n\tassert.Equal(t, \"/api/stat/go\", conf.API.StatGoURI)\n\tassert.Equal(t, \"/api/stat/app\", conf.API.StatAppURI)\n\tassert.Equal(t, \"/api/config\", conf.API.ConfigURI)\n\tassert.Equal(t, \"/sys/stats\", conf.API.SysStatURI)\n\tassert.Equal(t, \"/metrics\", conf.API.MetricURI)\n\tassert.Equal(t, \"/healthz\", conf.API.HealthURI)\n}\n\nfunc TestSanitizedCopy(t *testing.T) {\n\tcfg := &ConfYaml{}\n\tcfg.Core.CertBase64 = \"cert-data\"\n\tcfg.Core.KeyBase64 = \"key-data\"\n\tcfg.Core.HTTPProxy = \"http://proxy:8080\"\n\tcfg.Core.CertPath = \"/path/to/cert.pem\"\n\tcfg.Core.KeyPath = \"/path/to/key.pem\"\n\tcfg.Core.FeedbackHeader = []string{\"x-api-key:secret123\", \"x-auth-key:secret456\"}\n\tcfg.Android.KeyPath = \"/path/to/key.json\"\n\tcfg.Android.Credential = \"fcm-credential\"\n\tcfg.Huawei.AppSecret = \"hms-secret\"\n\tcfg.Ios.KeyPath = \"/path/to/ios.p8\"\n\tcfg.Ios.KeyBase64 = \"ios-key-data\"\n\tcfg.Ios.Password = \"ios-password\"\n\tcfg.Ios.KeyID = \"ABCDE12345\"\n\tcfg.Ios.TeamID = \"TEAM123456\"\n\tcfg.Queue.Redis.Username = \"queue-user\"\n\tcfg.Queue.Redis.Password = \"queue-pass\"\n\tcfg.Stat.Redis.Username = \"stat-user\"\n\tcfg.Stat.Redis.Password = \"stat-pass\"\n\n\t// Set a non-sensitive field to verify it's preserved\n\tcfg.Core.Port = \"8088\"\n\n\tsanitized := cfg.SanitizedCopy()\n\n\t// All sensitive fields should be redacted\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Core.CertBase64)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Core.KeyBase64)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Core.HTTPProxy)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Core.CertPath)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Core.KeyPath)\n\tassert.Len(t, sanitized.Core.FeedbackHeader, 2)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Core.FeedbackHeader[0])\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Core.FeedbackHeader[1])\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Android.KeyPath)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Android.Credential)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Huawei.AppSecret)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Ios.KeyPath)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Ios.KeyBase64)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Ios.Password)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Ios.KeyID)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Ios.TeamID)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Queue.Redis.Username)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Queue.Redis.Password)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Stat.Redis.Username)\n\tassert.Equal(t, \"[REDACTED]\", sanitized.Stat.Redis.Password)\n\n\t// Non-sensitive fields should be preserved\n\tassert.Equal(t, \"8088\", sanitized.Core.Port)\n\n\t// Original config must NOT be modified\n\tassert.Equal(t, \"/path/to/cert.pem\", cfg.Core.CertPath)\n\tassert.Equal(t, \"/path/to/key.pem\", cfg.Core.KeyPath)\n\tassert.Equal(t, \"x-api-key:secret123\", cfg.Core.FeedbackHeader[0])\n\tassert.Equal(t, \"cert-data\", cfg.Core.CertBase64)\n\tassert.Equal(t, \"fcm-credential\", cfg.Android.Credential)\n\tassert.Equal(t, \"ios-password\", cfg.Ios.Password)\n\tassert.Equal(t, \"stat-pass\", cfg.Stat.Redis.Password)\n}\n\nfunc TestSanitizedCopyEmptyFields(t *testing.T) {\n\tcfg := &ConfYaml{}\n\t// All sensitive fields are empty by default\n\n\tsanitized := cfg.SanitizedCopy()\n\n\t// Empty fields should remain empty, not become \"[REDACTED]\"\n\tassert.Empty(t, sanitized.Core.CertBase64)\n\tassert.Empty(t, sanitized.Core.KeyBase64)\n\tassert.Empty(t, sanitized.Core.HTTPProxy)\n\tassert.Empty(t, sanitized.Core.CertPath)\n\tassert.Empty(t, sanitized.Core.KeyPath)\n\tassert.Nil(t, sanitized.Core.FeedbackHeader)\n\tassert.Empty(t, sanitized.Android.KeyPath)\n\tassert.Empty(t, sanitized.Android.Credential)\n\tassert.Empty(t, sanitized.Huawei.AppSecret)\n\tassert.Empty(t, sanitized.Ios.KeyPath)\n\tassert.Empty(t, sanitized.Ios.KeyBase64)\n\tassert.Empty(t, sanitized.Ios.Password)\n\tassert.Empty(t, sanitized.Ios.KeyID)\n\tassert.Empty(t, sanitized.Ios.TeamID)\n\tassert.Empty(t, sanitized.Queue.Redis.Username)\n\tassert.Empty(t, sanitized.Queue.Redis.Password)\n\tassert.Empty(t, sanitized.Stat.Redis.Username)\n\tassert.Empty(t, sanitized.Stat.Redis.Password)\n}\n"
  },
  {
    "path": "config/testdata/empty.yml",
    "content": ""
  },
  {
    "path": "config/testdata/redis_db_config.yml",
    "content": "queue:\n  engine: \"redis\"\n  redis:\n    addr: 127.0.0.1:6379\n    group: gorush\n    consumer: gorush\n    stream_name: gorush\n    username: \"\"\n    password: \"\"\n    with_tls: false\n    db: 5\n\nstat:\n  engine: \"redis\"\n  redis:\n    cluster: false\n    addr: \"localhost:6379\"\n    username: \"\"\n    password: \"\"\n    db: 3\n"
  },
  {
    "path": "contrib/init/debian/README.md",
    "content": "# Run gorush in Debian/Ubuntu\n\n## Installation\n\nPut `gorush` binary into `/usr/bin` folder.\n\n```sh\ncp gorush /usr/bin/\nchmod +x /usr/bin/gorush\n```\n\nput `gorush` init script into `/etc/rc.d`\n\n```sh\ncp contrib/init/debian/gorush /etc.rc.d/\n```\n\ninstall and remove System-V style init script links\n\n```sh\nupdate-rc.d gorush start 20 2 3 4 5 . stop 80 0 1 6 .\n```\n\n## Start service\n\ncreate gorush configuration file.\n\n```sh\nmkdir -p /etc/gorush\ncp config/testdata/config.yml /etc/gorush/\n```\n\nstart gorush service.\n\n```sh\n/etc/init.d/gorush start\n```\n"
  },
  {
    "path": "contrib/init/debian/gorush",
    "content": "#!/bin/sh\n\n### BEGIN INIT INFO\n# Provides:          gorush\n# Required-Start:    $syslog $network\n# Required-Stop:     $syslog $network\n# Default-Start:     2 3 4 5\n# Default-Stop:      0 1 6\n# Short-Description: starts the gorush web server\n# Description:       starts gorush using start-stop-daemon\n### END INIT INFO\n\n# Original Author: Bo-Yi Wu (appleboy)\n\n# Do NOT \"set -e\"\nPATH=/sbin:/usr/sbin:/bin:/usr/bin\nDESC=\"the gorush web server\"\nNAME=gorush\nDAEMON=$(which gorush)\n\nDAEMONUSER=www-data\nPIDFILE=/var/run/$NAME.pid\nCONFIGFILE=/etc/gorush/config.yml\nDAEMONOPTS=\"-c $CONFIGFILE\"\n\nUSERBIND=\"setcap cap_net_bind_service=+ep\"\nSTOP_SCHEDULE=\"${STOP_SCHEDULE:-QUIT/5/TERM/5/KILL/5}\"\n\n# Read configuration variable file if it is present\n[ -r /etc/default/$NAME ] && . /etc/default/$NAME\n\n# Exit if the package is not installed\n[ -x \"$DAEMON\" ] || exit 0\n\n# Set the ulimits\nulimit -n 8192\n\ndo_start()\n{\n  $USERBIND $DAEMON\n  sh -c \"USER=$DAEMONUSER start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \\\\\n    --background --chuid $DAEMONUSER --exec $DAEMON -- $DAEMONOPTS\"\n}\n\ndo_stop()\n{\n  start-stop-daemon --stop --quiet --retry=$STOP_SCHEDULE --pidfile $PIDFILE --name $NAME --oknodo\n  rm -f $PIDFILE\n}\n\ndo_status()\n{\n  if [ -f $PIDFILE ]; then\n    if kill -0 $(cat \"$PIDFILE\"); then\n      echo \"$NAME is running, PID is $(cat $PIDFILE)\"\n    else\n      echo \"$NAME process is dead, but pidfile exists\"\n    fi\n  else\n    echo \"$NAME is not running\"\n  fi\n}\n\ncase \"$1\" in\n  start)\n    echo \"Starting $DESC\" \"$NAME\"\n    do_start\n    ;;\n  stop)\n    echo \"Stopping $DESC\" \"$NAME\"\n    do_stop\n    ;;\n  status)\n    do_status\n    ;;\n  restart)\n    echo \"Restarting $DESC\" \"$NAME\"\n    do_stop\n    do_start\n    ;;\n  *)\n    echo \"Usage: $SCRIPTNAME {start|stop|status|restart}\" >&2\n    exit 2\n    ;;\nesac\n\nexit 0\n"
  },
  {
    "path": "core/core.go",
    "content": "package core\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strings\"\n)\n\n// Backward-compatible integer constants kept as-is.\nconst (\n\t// PlatFormIos constant is 1 for iOS\n\tPlatFormIos = iota + 1\n\t// PlatFormAndroid constant is 2 for Android\n\tPlatFormAndroid\n\t// PlatFormHuawei constant is 3 for Huawei\n\tPlatFormHuawei\n)\n\n// Log block string constants (backward-compatible)\nconst (\n\t// SucceededPush is log block\n\tSucceededPush = \"succeeded-push\"\n\t// FailedPush is log block\n\tFailedPush = \"failed-push\"\n)\n\n// Platform is a typed enum for push target platforms.\n// This complements the existing integer constants.\ntype Platform uint8\n\nconst (\n\t// Typed equivalents of platform values.\n\tPlatformIOS     Platform = Platform(PlatFormIos)\n\tPlatformAndroid Platform = Platform(PlatFormAndroid)\n\tPlatformHuawei  Platform = Platform(PlatFormHuawei)\n)\n\n// String returns a stable lowercase name for the platform.\nfunc (p Platform) String() string {\n\tswitch p {\n\tcase PlatformIOS:\n\t\treturn \"ios\"\n\tcase PlatformAndroid:\n\t\treturn \"android\"\n\tcase PlatformHuawei:\n\t\treturn \"huawei\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// IsValid reports whether the platform value is supported.\nfunc (p Platform) IsValid() bool {\n\treturn p == PlatformIOS || p == PlatformAndroid || p == PlatformHuawei\n}\n\n// ParsePlatform parses a string (case-insensitive) into a Platform.\nfunc ParsePlatform(s string) (Platform, error) {\n\tswitch strings.ToLower(strings.TrimSpace(s)) {\n\tcase \"ios\":\n\t\treturn PlatformIOS, nil\n\tcase \"android\":\n\t\treturn PlatformAndroid, nil\n\tcase \"huawei\":\n\t\treturn PlatformHuawei, nil\n\tdefault:\n\t\treturn 0, errors.New(\"unknown platform: \" + s)\n\t}\n}\n\n// MarshalText encodes Platform as its string form.\nfunc (p Platform) MarshalText() ([]byte, error) {\n\tif !p.IsValid() {\n\t\treturn nil, errors.New(\"invalid platform\")\n\t}\n\treturn []byte(p.String()), nil\n}\n\n// UnmarshalText decodes Platform from its string form.\nfunc (p *Platform) UnmarshalText(text []byte) error {\n\tv, err := ParsePlatform(string(text))\n\tif err != nil {\n\t\treturn err\n\t}\n\t*p = v\n\treturn nil\n}\n\n// MarshalJSON encodes Platform as a JSON string.\nfunc (p Platform) MarshalJSON() ([]byte, error) {\n\tif !p.IsValid() {\n\t\treturn nil, errors.New(\"invalid platform\")\n\t}\n\treturn json.Marshal(p.String())\n}\n\n// UnmarshalJSON decodes Platform from a JSON string or legacy number.\nfunc (p *Platform) UnmarshalJSON(data []byte) error {\n\t// Try string first.\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err == nil {\n\t\treturn p.UnmarshalText([]byte(s))\n\t}\n\t// Fallback to legacy numeric values 1/2/3.\n\tvar n int\n\tif err := json.Unmarshal(data, &n); err == nil {\n\t\tswitch n {\n\t\tcase PlatFormIos:\n\t\t\t*p = PlatformIOS\n\t\t\treturn nil\n\t\tcase PlatFormAndroid:\n\t\t\t*p = PlatformAndroid\n\t\t\treturn nil\n\t\tcase PlatFormHuawei:\n\t\t\t*p = PlatformHuawei\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn errors.New(\"invalid platform JSON\")\n}\n\n// LogBlock is a typed alias for log block kinds.\ntype LogBlock string\n\nconst (\n\tLogSucceededPush LogBlock = LogBlock(SucceededPush)\n\tLogFailedPush    LogBlock = LogBlock(FailedPush)\n)\n\n// IsValid checks a LogBlock value.\nfunc (l LogBlock) IsValid() bool {\n\treturn l == LogSucceededPush || l == LogFailedPush\n}\n"
  },
  {
    "path": "core/core_test.go",
    "content": "package core\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n)\n\nfunc TestPlatformStringAndValidity(t *testing.T) {\n\tcases := []struct {\n\t\tp        Platform\n\t\texpected string\n\t}{\n\t\t{PlatformIOS, PlatformIOS.String()},\n\t\t{PlatformAndroid, PlatformAndroid.String()},\n\t\t{PlatformHuawei, PlatformHuawei.String()},\n\t}\n\n\tfor _, c := range cases {\n\t\tif !c.p.IsValid() {\n\t\t\tt.Fatalf(\"expected %v to be valid\", c.p)\n\t\t}\n\t\tif got := c.p.String(); got != c.expected {\n\t\t\tt.Fatalf(\"String() mismatch: got %q want %q\", got, c.expected)\n\t\t}\n\t}\n\n\tif Platform(0).IsValid() {\n\t\tt.Fatalf(\"expected zero value to be invalid\")\n\t}\n}\n\nfunc TestParsePlatform(t *testing.T) {\n\tp, err := ParsePlatform(\" IOS \")\n\tif err != nil || p != PlatformIOS {\n\t\tt.Fatalf(\"ParsePlatform ios failed: p=%v err=%v\", p, err)\n\t}\n\tp, err = ParsePlatform(PlatformAndroid.String())\n\tif err != nil || p != PlatformAndroid {\n\t\tt.Fatalf(\"ParsePlatform android failed: p=%v err=%v\", p, err)\n\t}\n\tp, err = ParsePlatform(\"HuAwEi\")\n\tif err != nil || p != PlatformHuawei {\n\t\tt.Fatalf(\"ParsePlatform huawei failed: p=%v err=%v\", p, err)\n\t}\n\tif _, err = ParsePlatform(\"unknown\"); err == nil {\n\t\tt.Fatalf(\"expected error for unknown platform\")\n\t}\n}\n\nfunc TestPlatformTextMarshaling(t *testing.T) {\n\tb, err := PlatformIOS.MarshalText()\n\tif err != nil || string(b) != PlatformIOS.String() {\n\t\tt.Fatalf(\"MarshalText: got %q err=%v\", string(b), err)\n\t}\n\tvar p Platform\n\tandroidText := []byte(PlatformAndroid.String())\n\tif err := p.UnmarshalText(androidText); err != nil || p != PlatformAndroid {\n\t\tt.Fatalf(\"UnmarshalText: p=%v err=%v\", p, err)\n\t}\n}\n\nfunc TestPlatformJSONMarshaling(t *testing.T) {\n\tb, err := json.Marshal(PlatformHuawei)\n\texpected := `\"` + PlatformHuawei.String() + `\"`\n\tif err != nil || string(b) != expected {\n\t\tt.Fatalf(\"MarshalJSON: got %s err=%v\", string(b), err)\n\t}\n\tvar p Platform\n\tiosJSON := []byte(`\"` + PlatformIOS.String() + `\"`)\n\tif err := json.Unmarshal(iosJSON, &p); err != nil || p != PlatformIOS {\n\t\tt.Fatalf(\"UnmarshalJSON string: p=%v err=%v\", p, err)\n\t}\n\t// legacy numeric values\n\tif err := json.Unmarshal([]byte(\"2\"), &p); err != nil || p != PlatformAndroid {\n\t\tt.Fatalf(\"UnmarshalJSON legacy number: p=%v err=%v\", p, err)\n\t}\n\tif err := json.Unmarshal([]byte(\"99\"), &p); err == nil {\n\t\tt.Fatalf(\"expected error for invalid numeric JSON\")\n\t}\n}\n\nfunc TestLogBlock(t *testing.T) {\n\tif !LogSucceededPush.IsValid() || !LogFailedPush.IsValid() {\n\t\tt.Fatalf(\"expected log blocks to be valid\")\n\t}\n\tif LogBlock(\"x\").IsValid() {\n\t\tt.Fatalf(\"expected arbitrary value to be invalid\")\n\t}\n}\n"
  },
  {
    "path": "core/health.go",
    "content": "package core\n\nimport \"context\"\n\n// Health defines a health-check connection.\ntype Health interface {\n\t// Check returns if server is healthy or not\n\tCheck(c context.Context) (bool, error)\n}\n"
  },
  {
    "path": "core/queue.go",
    "content": "package core\n\n// Queue as backend\ntype Queue string\n\nvar (\n\t// LocalQueue for channel in Go\n\tLocalQueue Queue = \"local\"\n\t// NSQ a realtime distributed messaging platform\n\tNSQ Queue = \"nsq\"\n\t// NATS Connective Technology for Adaptive Edge & Distributed Systems\n\tNATS Queue = \"nats\"\n\t// Redis Pub/Sub\n\tRedis Queue = \"redis\"\n)\n\n// IsLocalQueue check is Local Queue\nfunc IsLocalQueue(q Queue) bool {\n\treturn q == LocalQueue\n}\n"
  },
  {
    "path": "core/storage.go",
    "content": "package core\n\nconst (\n\t// TotalCountKey is key name for total count of storage\n\tTotalCountKey = \"gorush-total-count\"\n\n\t// IosSuccessKey is key name or ios success count of storage\n\t/* #nosec */\n\tIosSuccessKey = \"gorush-ios-success-count\"\n\n\t// IosErrorKey is key name or ios success error of storage\n\tIosErrorKey = \"gorush-ios-error-count\"\n\n\t// AndroidSuccessKey is key name for android success count of storage\n\tAndroidSuccessKey = \"gorush-android-success-count\"\n\n\t// AndroidErrorKey is key name for android error count of storage\n\tAndroidErrorKey = \"gorush-android-error-count\"\n\n\t// HuaweiSuccessKey is key name for huawei success count of storage\n\tHuaweiSuccessKey = \"gorush-huawei-success-count\"\n\n\t// HuaweiErrorKey is key name for huawei error count of storage\n\tHuaweiErrorKey = \"gorush-huawei-error-count\"\n)\n\n// Storage interface\ntype Storage interface {\n\tInit() error\n\tAdd(key string, count int64)\n\tSet(key string, count int64)\n\tGet(key string) int64\n\tClose() error\n}\n"
  },
  {
    "path": "doc.go",
    "content": "// A push notification server using Gin framework written in Go (Golang).\n//\n// Details about the gorush project are found in github page:\n//\n//\thttps://github.com/appleboy/gorush\n//\n// The pre-compiled binaries can be downloaded from release page.\n//\n// Send Android notification\n//\n//\t$ gorush -android -m=\"your message\" --fcm-key=\"FCM Key Path\" -t=\"Device token\"\n//\n// Send iOS notification\n//\n//\t$ gorush -ios -m=\"your message\" -i=\"API Key\" -t=\"Device token\"\n//\n// The default endpoint is APNs development. Please add -production flag for APNs production push endpoint.\n//\n//\t$ gorush -ios -m=\"your message\" -i=\"API Key\" -t=\"Device token\" -production\n//\n// Run gorush web server\n//\n//\t$ gorush -c config.yml\n//\n// Get go status of api server using httpie tool:\n//\n//\t$ http -v --verify=no --json GET https://localhost:8088/api/stat/go\n//\n// Simple send iOS notification example, the platform value is 1:\n//\n//\t{\n//\t  \"notifications\": [\n//\t    {\n//\t      \"tokens\": [\"token_a\", \"token_b\"],\n//\t      \"platform\": 1,\n//\t      \"message\": \"Hello World iOS!\"\n//\t    }\n//\t  ]\n//\t}\n//\n// Simple send Android notification example, the platform value is 2:\n//\n//\t{\n//\t  \"notifications\": [\n//\t    {\n//\t      \"tokens\": [\"token_a\", \"token_b\"],\n//\t      \"platform\": 2,\n//\t      \"message\": \"Hello World Android!\"\n//\t    }\n//\t  ]\n//\t}\n//\n// For more details, see the documentation and example.\npackage main\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "FROM alpine:3.22\n\nARG TARGETOS\nARG TARGETARCH\nARG USER=gorush\nENV HOME /home/$USER\n\nLABEL maintainer=\"Bo-Yi Wu <appleboy.tw@gmail.com>\" \\\n  org.label-schema.name=\"Gorush\" \\\n  org.label-schema.vendor=\"Bo-Yi Wu\" \\\n  org.label-schema.schema-version=\"1.0\"\n\n# add new user\nRUN adduser -D $USER\nRUN apk add --no-cache ca-certificates mailcap && \\\n  rm -rf /var/cache/apk/*\n\nCOPY release/${TARGETOS}/${TARGETARCH}/gorush /bin/\n\nUSER $USER\nWORKDIR $HOME\n\nEXPOSE 8088 9000\nHEALTHCHECK --start-period=1s --interval=10s --timeout=5s \\\n  CMD [\"/bin/gorush\", \"--ping\"]\n\nENTRYPOINT [\"/bin/gorush\"]\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  gorush:\n    image: appleboy/gorush\n    restart: always\n    ports:\n      - \"8088:8088\"\n      - \"9000:9000\"\n    logging:\n      options:\n        max-size: \"100k\"\n        max-file: \"3\"\n    environment:\n      - GORUSH_CORE_QUEUE_NUM=512\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/appleboy/gorush\n\ngo 1.25.0\n\nreplace github.com/msalihkarakasli/go-hms-push => github.com/spawn2kill/go-hms-push v0.0.0-20211125124117-e20af53b1304\n\nrequire (\n\tfirebase.google.com/go/v4 v4.19.0\n\tgithub.com/apex/gateway v1.1.2\n\tgithub.com/appleboy/gin-status-api v1.2.0\n\tgithub.com/appleboy/go-fcm v1.2.7\n\tgithub.com/appleboy/go-hms-push v1.0.1\n\tgithub.com/appleboy/gofight/v2 v2.2.1\n\tgithub.com/appleboy/graceful v1.3.0\n\tgithub.com/asdine/storm/v3 v3.2.1\n\tgithub.com/buger/jsonparser v1.1.1\n\tgithub.com/dgraph-io/badger/v4 v4.9.1\n\tgithub.com/gin-contrib/logger v1.2.6\n\tgithub.com/gin-gonic/gin v1.12.0\n\tgithub.com/golang-queue/nats v0.2.0\n\tgithub.com/golang-queue/nsq v0.3.0\n\tgithub.com/golang-queue/queue v0.5.0\n\tgithub.com/golang-queue/redisdb-stream v0.3.1\n\tgithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0\n\tgithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0\n\tgithub.com/json-iterator/go v1.1.12\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/mitchellh/mapstructure v1.5.0\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/redis/go-redis/v9 v9.18.0\n\tgithub.com/rs/zerolog v1.34.0\n\tgithub.com/sideshow/apns2 v0.25.0\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/syndtr/goleveldb v1.0.0\n\tgithub.com/thoas/stats v0.0.0-20190407194641-965cb2de1678\n\tgithub.com/tidwall/buntdb v1.3.2\n\tgo.opencensus.io v0.24.0\n\tgo.uber.org/atomic v1.11.0\n\tgolang.org/x/crypto v0.48.0\n\tgolang.org/x/net v0.51.0\n\tgolang.org/x/sync v0.20.0\n\tgoogle.golang.org/grpc v1.79.2\n\tgoogle.golang.org/protobuf v1.36.11\n)\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.18.2 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/firestore v1.21.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/longrunning v0.8.0 // indirect\n\tcloud.google.com/go/monitoring v1.24.3 // indirect\n\tcloud.google.com/go/storage v1.61.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect\n\tgithub.com/MicahParks/keyfunc v1.9.0 // indirect\n\tgithub.com/appleboy/com v1.2.0 // indirect\n\tgithub.com/aws/aws-lambda-go v1.53.0 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // 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/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dgraph-io/ristretto/v2 v2.4.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/fukata/golang-stats-api-handler v1.0.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-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // 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/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/golang/snappy v1.0.0 // indirect\n\tgithub.com/google/flatbuffers v25.12.19+incompatible // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.17.0 // indirect\n\tgithub.com/jpillora/backoff v1.0.0 // 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/mattn/go-colorable v0.1.14 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/nats-io/nats.go v1.49.0 // indirect\n\tgithub.com/nats-io/nkeys v0.4.15 // indirect\n\tgithub.com/nats-io/nuid v1.0.1 // indirect\n\tgithub.com/nsqio/go-nsq v1.1.0 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/procfs v0.20.1 // 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/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.6.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tidwall/btree v1.8.1 // indirect\n\tgithub.com/tidwall/gjson v1.18.0 // indirect\n\tgithub.com/tidwall/grect v0.1.4 // indirect\n\tgithub.com/tidwall/match v1.2.0 // indirect\n\tgithub.com/tidwall/pretty v1.2.1 // indirect\n\tgithub.com/tidwall/rtred v0.1.2 // indirect\n\tgithub.com/tidwall/tinyqueue v0.1.1 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.1 // indirect\n\tgo.etcd.io/bbolt v1.4.3 // indirect\n\tgo.mongodb.org/mongo-driver/v2 v2.5.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect\n\tgo.opentelemetry.io/otel v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.42.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.4 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/arch v0.25.0 // indirect\n\tgolang.org/x/oauth2 v0.36.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgolang.org/x/time v0.15.0 // indirect\n\tgoogle.golang.org/api v0.270.0 // indirect\n\tgoogle.golang.org/appengine/v2 v2.0.6 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM=\ncloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=\ncloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=\ncloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=\ncloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=\ncloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=\ncloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=\ncloud.google.com/go/storage v1.61.0 h1:8NGccs4oDZTqV1nBlom0CVJewloINXYW5Z0LoFqaVeI=\ncloud.google.com/go/storage v1.61.0/go.mod h1:IvExELZv/uJe/DAzLgPeKNT8dm5+DM5gO0H1bkubD6Y=\ncloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=\ncloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=\ndario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=\ndario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=\nfirebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=\nfirebase.google.com/go/v4 v4.19.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=\ngithub.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=\ngithub.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=\ngithub.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=\ngithub.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=\ngithub.com/apex/gateway v1.1.2 h1:OWyLov8eaau8YhkYKkRuOAYqiUhpBJalBR1o+3FzX+8=\ngithub.com/apex/gateway v1.1.2/go.mod h1:AMTkVbz5u5Hvd6QOGhhg0JUrNgCcLVu3XNJOGntdoB4=\ngithub.com/appleboy/com v1.2.0 h1:3jyA+yVofe/uzPHHa7Xrsj7rnDy1sZn/8pYHdzHB3GQ=\ngithub.com/appleboy/com v1.2.0/go.mod h1:XK2kV+JWz/gkzsDPotNJL+aS6XCy5GNlbiTWGvIhqIU=\ngithub.com/appleboy/gin-status-api v1.2.0 h1:v5FpDfgf94z7eFniW87cPHytbfzuM7Km2vfY/BHjAzw=\ngithub.com/appleboy/gin-status-api v1.2.0/go.mod h1:WQFLh5VvFDz8fUPv66XYUeZ2nHEoPa6qZelLZ+9ARsg=\ngithub.com/appleboy/go-fcm v1.2.7 h1:1HFLCSH/HyAPOTVsmWCL1UPSJ+lKr5t60kCXd2F4KlY=\ngithub.com/appleboy/go-fcm v1.2.7/go.mod h1:HbdeRIVWOdoHHTV9TXkEQKa6f9Bg+xJSjgUCYXAjPwU=\ngithub.com/appleboy/go-hms-push v1.0.1 h1:55v173UvjgZgmaj/D+X+y82WLuA+xl4SYHCF8Zb5WtM=\ngithub.com/appleboy/go-hms-push v1.0.1/go.mod h1:JZzS7XUOFTUV8y+MAnDjOIv0LCZmLmVpJj//Q9t0XBw=\ngithub.com/appleboy/gofight/v2 v2.2.1 h1:OOJrZ71tdOFDzyyBvP+h047w0EJHktqTo4mEOTDrKy0=\ngithub.com/appleboy/gofight/v2 v2.2.1/go.mod h1:dOz1A3YtfciapH897IQOAR6JfTFm0nwJctaDuJZiijY=\ngithub.com/appleboy/graceful v1.3.0 h1:IU5In15N4z0qkTAVnuKH/KZv3iat5lsS1pbTsDB1LzU=\ngithub.com/appleboy/graceful v1.3.0/go.mod h1:XlEg3jkgb42weJeCXch3HRXvWrU5r+EYymCyPVy0EkA=\ngithub.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=\ngithub.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=\ngithub.com/aws/aws-lambda-go v1.17.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw=\ngithub.com/aws/aws-lambda-go v1.53.0 h1:uAMv6W/vCP/L494BAUSxe+8KVBIPK+SGPyapFt3FuMk=\ngithub.com/aws/aws-lambda-go v1.53.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\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/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=\ngithub.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=\ngithub.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=\ngithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w=\ngithub.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0=\ngithub.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=\ngithub.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=\ngithub.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=\ngithub.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=\ngithub.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=\ngithub.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=\ngithub.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=\ngithub.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=\ngithub.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fukata/golang-stats-api-handler v1.0.0 h1:N6M25vhs1yAvwGBpFY6oBmMOZeJdcWnvA+wej8pKeko=\ngithub.com/fukata/golang-stats-api-handler v1.0.0/go.mod h1:1sIi4/rHq6s/ednWMZqTmRq3765qTUSs/c3xF6lj8J8=\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/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc=\ngithub.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=\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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\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 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\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/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=\ngithub.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-queue/nats v0.2.0 h1:JGuFxniEM5L7KezEa71OwG+uTsEqA2DM+7L62DzAWEw=\ngithub.com/golang-queue/nats v0.2.0/go.mod h1:rQ33HM1X9Vs4cypzx/XrwMkjumLiwXNfjXXO/+wqAQo=\ngithub.com/golang-queue/nsq v0.3.0 h1:tkf01x06w4KLSE+SHKOISmOBsBck2IkBcurhQxQjTxQ=\ngithub.com/golang-queue/nsq v0.3.0/go.mod h1:tCeoQCEXAWAOmod+mUZWm5Zy8m1PCtmE03BvdkRsPTs=\ngithub.com/golang-queue/queue v0.5.0 h1:j5Qj3h+V9irfbavVkFwxf7TS6/1mtVqsisCSYJ25DAI=\ngithub.com/golang-queue/queue v0.5.0/go.mod h1:BoV3elevg5ksW3mSGKxP2Tu3YKgUONr4pYfWcari5nE=\ngithub.com/golang-queue/redisdb-stream v0.3.1 h1:z4V+csHjc5bF+I2LEPqdYQEqTGQKsrb2w/rC1EcXxQo=\ngithub.com/golang-queue/redisdb-stream v0.3.1/go.mod h1:00bXPJrRnDFaT2KJJ8zYDTbRers/xU8YpfjRRW6w+P8=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=\ngithub.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\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/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=\ngithub.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\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-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=\ngithub.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=\ngithub.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=\ngithub.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=\ngithub.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=\ngithub.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=\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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=\ngithub.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=\ngithub.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=\ngithub.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=\ngithub.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=\ngithub.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=\ngithub.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=\ngithub.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=\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/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=\ngithub.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=\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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=\ngithub.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sideshow/apns2 v0.25.0 h1:XOzanncO9MQxkb03T/2uU2KcdVjYiIf0TMLzec0FTW4=\ngithub.com/sideshow/apns2 v0.25.0/go.mod h1:7Fceu+sL0XscxrfLSkAoH6UtvKefq3Kq1n4W3ayQZqE=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=\ngithub.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=\ngithub.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=\ngithub.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=\ngithub.com/thoas/stats v0.0.0-20190407194641-965cb2de1678 h1:kFej3rMKjbzysHYvLmv5iOlbRymDMkNJxbovYb/iP0c=\ngithub.com/thoas/stats v0.0.0-20190407194641-965cb2de1678/go.mod h1:GkZsNBOco11YY68OnXUARbSl26IOXXAeYf6ZKmSZR2M=\ngithub.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=\ngithub.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=\ngithub.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=\ngithub.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=\ngithub.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=\ngithub.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=\ngithub.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=\ngithub.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=\ngithub.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=\ngithub.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=\ngithub.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=\ngithub.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=\ngithub.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=\ngithub.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=\ngithub.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=\ngithub.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=\ngithub.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=\ngithub.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=\ngithub.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=\ngithub.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=\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/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngithub.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=\ngithub.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=\ngo.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\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.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\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/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=\ngo.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=\ngo.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=\ngo.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=\ngo.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=\ngo.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=\ngo.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=\ngo.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=\ngo.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=\ngo.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\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/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=\ngo.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=\ngo.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=\ngolang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=\ngolang.org/x/crypto v0.0.0-20170512130425-ab89591268e0/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\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.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=\ngolang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\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/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=\ngoogle.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=\ngoogle.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=\ngoogle.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=\ngoogle.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20260226221140-a57be14db171 h1:RxhCsti413yL0IjU9dVvuTbCISo8gs3RW1jPMStck+4=\ngoogle.golang.org/genproto v0.0.0-20260226221140-a57be14db171/go.mod h1:uhvzakVEqAuXU3TC2JCsxIRe5f77l+JySE3EqPoMyqM=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=\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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=\ngoogle.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\n"
  },
  {
    "path": "helm/gorush/.helmignore",
    "content": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation (prefixed with !). Only one pattern per line.\n.DS_Store\n# Common VCS dirs\n.git/\n.gitignore\n.bzr/\n.bzrignore\n.hg/\n.hgignore\n.svn/\n# Common backup files\n*.swp\n*.bak\n*.tmp\n*.orig\n*~\n# Various IDEs\n.project\n.idea/\n*.tmproj\n.vscode/\n"
  },
  {
    "path": "helm/gorush/Chart.yaml",
    "content": "apiVersion: v2\nname: gorush\ndescription: A push notification micro server using Gin framework written in Go (Golang)\ntype: application\nversion: 0.1.0\nappVersion: \"1.14.0\"\ndependencies:\n  - name: redis\n    version: ~14.1\n    repository: https://charts.bitnami.com/bitnami\n    condition: redis.enabled\n"
  },
  {
    "path": "helm/gorush/templates/NOTES.txt",
    "content": "1. Get the application URL by running these commands:\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingress.hosts }}\n  {{- range .paths }}\n  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}\n  {{- end }}\n{{- end }}\n{{- else if contains \"NodePort\" .Values.service.type }}\n  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath=\"{.spec.ports[0].nodePort}\" services {{ include \"gorush.fullname\" . }})\n  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath=\"{.items[0].status.addresses[0].address}\")\n  echo http://$NODE_IP:$NODE_PORT\n{{- else if contains \"LoadBalancer\" .Values.service.type }}\n     NOTE: It may take a few minutes for the LoadBalancer IP to be available.\n           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include \"gorush.fullname\" . }}'\n  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include \"gorush.fullname\" . }} --template \"{{\"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}\"}}\")\n  echo http://$SERVICE_IP:{{ .Values.service.port }}\n{{- else if contains \"ClusterIP\" .Values.service.type }}\n  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l \"app.kubernetes.io/name={{ include \"gorush.name\" . }},app.kubernetes.io/instance={{ .Release.Name }}\" -o jsonpath=\"{.items[0].metadata.name}\")\n  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath=\"{.spec.containers[0].ports[0].containerPort}\")\n  echo \"Visit http://127.0.0.1:8080 to use your application\"\n  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT\n{{- end }}\n"
  },
  {
    "path": "helm/gorush/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"gorush.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"gorush.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"gorush.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"gorush.labels\" -}}\nhelm.sh/chart: {{ include \"gorush.chart\" . }}\n{{ include \"gorush.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"gorush.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"gorush.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"gorush.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"gorush.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "helm/gorush/templates/configmap.yml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: {{ .Chart.Name }}\n  namespace: {{ .Chart.Name }}\ndata:\n  # stat\n  stats:\n    engine: {{ .Values.stat.engine }}\n    {{- if .Values.redis.enabled }}\n    redis:\n      host: {{ .Values.redis.host }}:{{ .Values.redis.port }}\n    {{- end }}\n"
  },
  {
    "path": "helm/gorush/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Chart.Name }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    {{- include \"gorush.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"gorush.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      {{- with .Values.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      labels:\n        {{- include \"gorush.selectorLabels\" . | nindent 8 }}\n    spec:\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      serviceAccountName: {{ include \"gorush.serviceAccountName\" . }}\n      securityContext:\n        {{- toYaml .Values.podSecurityContext | nindent 8 }}\n      containers:\n        - name: {{ .Chart.Name }}\n          securityContext:\n            {{- toYaml .Values.securityContext | nindent 12 }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          ports:\n            - name: http\n              containerPort: {{ .Values.service.port }}\n              protocol: TCP\n          livenessProbe:\n            httpGet:\n              path: /healthz\n              port: http\n            initialDelaySeconds: 15\n            periodSeconds: 15\n          env:\n          - name: GORUSH_STAT_ENGINE\n            valueFrom:\n              configMapKeyRef:\n                name: {{ .Chart.Name }}-config\n                key: stat.engine\n          {{- if .Values.redis.enabled }} \n          - name: GORUSH_STAT_REDIS_ADDR\n            valueFrom:\n              configMapKeyRef:\n                name: {{ .Chart.Name }}-config\n                key: stat.redis.host\n          {{- end }}\n          readinessProbe:\n            httpGet:\n              path: /\n              port: http\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n"
  },
  {
    "path": "helm/gorush/templates/hpa.yaml",
    "content": "{{- if .Values.autoscaling.enabled }}\napiVersion: autoscaling/v2beta1\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: {{ .Chart.Name }}\n  namespace: {{ .Chart.Name }}\n  labels:\n    {{- include \"gorush.labels\" . | nindent 4 }}\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: {{ .Chart.Name }}\n  minReplicas: {{ .Values.autoscaling.minReplicas }}\n  maxReplicas: {{ .Values.autoscaling.maxReplicas }}\n  metrics:\n    {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}\n    - type: Resource\n      resource:\n        name: cpu\n        targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}\n    {{- end }}\n    {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}\n    - type: Resource\n      resource:\n        name: memory\n        targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}\n    {{- end }}\n{{- end }}\n"
  },
  {
    "path": "helm/gorush/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\n{{- $svcPort := .Values.service.port -}}\n{{- if semverCompare \">=1.14-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1\n{{- else -}}\napiVersion: extensions/v1beta1\n{{- end }}\nkind: Ingress\nmetadata:\n  name: {{ .Chart.Name }}\n  namespace: {{ .Chart.Name }}\n  labels:\n    {{- include \"gorush.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            pathType: Prefix\n            backend:\n              service:\n                name: gorush\n                port:\n                  number: {{ $svcPort }}\n          {{- end }}\n    {{- end }}\n  {{- end }}\n"
  },
  {
    "path": "helm/gorush/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Chart.Name }}\n  namespace: {{ .Chart.Name }}\n  labels:\n    {{- include \"gorush.labels\" . | nindent 4 }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: http\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"gorush.selectorLabels\" . | nindent 4 }}\n"
  },
  {
    "path": "helm/gorush/templates/serviceaccount.yaml",
    "content": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"gorush.serviceAccountName\" . }}\n  namespace: {{ .Chart.Name }}\n  labels:\n    {{- include \"gorush.labels\" . | nindent 4 }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "helm/gorush/values.yaml",
    "content": "# Default values for gorush.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 1\n\nimage:\n  repository: appleboy/gorush\n  pullPolicy: Always\n  # Overrides the image tag whose default is the chart appVersion.\n  tag: \"\"\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nserviceAccount:\n  # Specifies whether a service account should be created\n  create: false\n  # Annotations to add to the service account\n  annotations: {}\n  # The name of the service account to use.\n  # If not set and create is true, a name is generated using the fullname template\n  name: \"\"\n\npodAnnotations: {}\n\npodSecurityContext: {}\n  # fsGroup: 2000\n\nsecurityContext: {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\nservice:\n  type: ClusterIP\n  port: 8088\n\nstats:\n  engine: memory\n\nredis:\n  enabled: false\n  host: redis\n  port: 6379\n\ningress:\n  enabled: false\n  annotations: {}\n  hosts:\n    - host: gorush.example.com\n      paths:\n        - path: /\n  tls: []\n\nresources: {}\n\nautoscaling:\n  enabled: false\n  minReplicas: 1\n  maxReplicas: 10\n  targetCPUUtilizationPercentage: 80\n  # targetMemoryUtilizationPercentage: 80\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n"
  },
  {
    "path": "install.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nORANGE='\\033[38;2;255;140;0m'\nNC='\\033[0m' # No Color\n\nfunction print_message() {\n  local level=$1\n  local message=$2\n  local color=\"\"\n\n  case $level in\n  info) color=\"${GREEN}\" ;;\n  warning) color=\"${YELLOW}\" ;;\n  error) color=\"${RED}\" ;;\n  esac\n\n  printf \"%b\\n\" \"${color}${message}${NC}\"\n}\n\nfunction log_error() {\n  print_message error \"$1\" >&2\n  exit \"$2\"\n}\n\nfunction detect_client_info() {\n  # Detect WSL or Cygwin as windows\n  if [[ \"${CLIENT_PLATFORM}\" =~ ^(mingw|cygwin|msys)_nt* ]] || grep -qi microsoft /proc/version 2>/dev/null; then\n    CLIENT_PLATFORM=\"windows\"\n  fi\n\n  case \"${CLIENT_PLATFORM}\" in\n  darwin | linux | windows) ;;\n  *) log_error \"Unknown or unsupported platform: ${CLIENT_PLATFORM}. Supported platforms are Linux, Darwin, and Windows.\" 2 ;;\n  esac\n\n  case \"${CLIENT_ARCH}\" in\n  x86_64* | i?86_64* | amd64*) CLIENT_ARCH=\"amd64\" ;;\n  aarch64* | arm64*) CLIENT_ARCH=\"arm64\" ;;\n  armv7l*) CLIENT_ARCH=\"arm-7\" ;;\n  *) log_error \"Unknown or unsupported architecture: ${CLIENT_ARCH}. Supported architectures are x86_64, i686, arm64, armv7.\" 3 ;;\n  esac\n}\n\nfunction download_and_install() {\n  DOWNLOAD_URL_PREFIX=\"${RELEASE_URL}/v${VERSION}\"\n  CLIENT_BINARY=\"gorush-${VERSION}-${CLIENT_PLATFORM}-${CLIENT_ARCH}\"\n  print_message info \"Downloading ${CLIENT_BINARY} from ${DOWNLOAD_URL_PREFIX}\"\n  mkdir -p \"$INSTALL_DIR\" || log_error \"Failed to create directory: $INSTALL_DIR\" 5\n\n  # Use temp dir for download\n  TARGET=\"${GORUSH_TMPDIR}/${CLIENT_BINARY}\"\n\n  curl -# -fSL --retry 5 --keepalive-time 2 \"$INSECURE_ARG\" \"${DOWNLOAD_URL_PREFIX}/${CLIENT_BINARY}\" -o \"${TARGET}\" || log_error \"Failed to download ${CLIENT_BINARY}\" 6\n  chmod +x \"${TARGET}\" || log_error \"Failed to set executable permission on: ${TARGET}\" 7\n  # Move the binary to install dir and rename to gorush\n  mv \"${TARGET}\" \"${INSTALL_DIR}/gorush\" || log_error \"Failed to move ${TARGET} to ${INSTALL_DIR}/gorush\" 8\n  # show the version\n  printf \"%b\\\\n\" \"Installed ${ORANGE}${CLIENT_BINARY}${NC} to ${GREEN}${INSTALL_DIR}${NC}\"\n  printf \"%b\\\\n\" \"Run ${ORANGE}gorush version${NC} to show the version\"\n  print_message info \"\"\n  print_message info \"===============================\"\n  \"${INSTALL_DIR}/gorush\" --version\n  print_message info \"===============================\"\n  print_message info \"\"\n  print_message info \"✅ Installation completed successfully!\"\n}\n\nfunction add_to_path() {\n  local config_file=$1\n  local command=$2\n\n  if grep -Fxq \"$command\" \"$config_file\"; then\n    print_message info \"Configuration already exists in $config_file, skipping\"\n    return 0\n  fi\n\n  if [[ -w $config_file ]]; then\n    printf \"\\n# gorush\\n\" >>\"$config_file\"\n    echo \"$command\" >>\"$config_file\"\n    print_message info \"Successfully added ${ORANGE}gorush ${GREEN}to \\$PATH in $config_file\"\n  else\n    print_message warning \"Manually add the directory to $config_file (or similar):\"\n    print_message info \"  $command\"\n  fi\n}\n\n# Fetch latest release version from GitHub if VERSION is not set\nfunction get_latest_version() {\n  local latest\n  local response\n  response=$(curl \"$INSECURE_ARG\" -# --retry 5 -fSL https://api.github.com/repos/appleboy/gorush/releases/latest) || log_error \"Failed to fetch release info from GitHub API\" 6\n  if command -v jq >/dev/null 2>&1; then\n    latest=$(echo \"$response\" | jq -r .tag_name)\n  else\n    latest=$(echo \"$response\" | grep '\"tag_name\":' | sed -E 's/.*\"tag_name\": ?\"v?([^\"]+)\".*/\\1/')\n  fi\n  # Remove leading 'v' if present\n  latest=\"${latest#v}\"\n  echo \"$latest\"\n}\n\n# Check for required commands\nfor cmd in curl mktemp; do\n  if ! command -v \"$cmd\" >/dev/null 2>&1; then\n    log_error \"Error: $cmd is not installed. Please install $cmd to proceed.\" 1\n  fi\ndone\n\n# Create temp directory for downloads.\nGORUSH_TMPDIR=\"$(mktemp -d)\"\nfunction cleanup() {\n  if [ -n \"${GORUSH_TMPDIR:-}\" ] && [ -d \"$GORUSH_TMPDIR\" ]; then\n    rm -rf \"$GORUSH_TMPDIR\"\n  fi\n}\ntrap cleanup EXIT INT TERM\n\n# If INSECURE is set to any value, enable curl --insecure\nINSECURE_ARG=\"\"\nif [[ -n \"${INSECURE:-}\" ]]; then\n  INSECURE_ARG=\"--insecure\"\n  print_message warning \"WARNING: INSECURE mode is enabled. Proceeding with insecure download.\"\n  print_message warning \"WARNING: You are bypassing SSL certificate verification. This is insecure and may expose you to man-in-the-middle attacks.\"\nfi\n\nif [[ -z \"${VERSION:-}\" ]]; then\n  LATEST_VERSION=$(get_latest_version)\n  if [[ -z \"$LATEST_VERSION\" ]]; then\n    log_error \"Failed to fetch the latest version from GitHub.\" 6\n  fi\n  VERSION=\"$LATEST_VERSION\"\nfi\n\n# Check if VERSION is a valid semantic version\nif ! [[ \"$VERSION\" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then\n  log_error \"Invalid version format: $VERSION. Expected format: x.y.z\" 1\nfi\n\nRELEASE_URL=\"${RELEASE_URL:-https://github.com/appleboy/gorush/releases/download}\"\nINSTALL_DIR=\"${INSTALL_DIR:-$HOME/.gorush/bin}\"\nCLIENT_PLATFORM=\"${CLIENT_PLATFORM:-$(uname -s | tr '[:upper:]' '[:lower:]')}\"\nCLIENT_ARCH=\"${CLIENT_ARCH:-$(uname -m)}\"\n\ndetect_client_info\ndownload_and_install\n\nXDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}\n\ncurrent_shell=$(basename \"$SHELL\")\ncase $current_shell in\nfish)\n  config_files=\"$HOME/.config/fish/config.fish\"\n  ;;\nzsh)\n  config_files=\"$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv\"\n  ;;\nbash)\n  config_files=\"$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile\"\n  ;;\nash)\n  config_files=\"$HOME/.ashrc $HOME/.profile /etc/profile\"\n  ;;\nsh)\n  config_files=\"$HOME/.ashrc $HOME/.profile /etc/profile\"\n  ;;\n*)\n  # Default case if none of the above matches\n  config_files=\"$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile\"\n  ;;\nesac\n\nconfig_file=\"\"\nfor file in $config_files; do\n  if [[ -f $file ]]; then\n    config_file=$file\n    break\n  fi\ndone\n\nif [[ -z $config_file ]]; then\n  log_error \"No config file found for $current_shell. Checked files: $config_files\" 1\nfi\n\nif [[ \":$PATH:\" != *\":$INSTALL_DIR:\"* ]]; then\n  case $current_shell in\n  fish)\n    add_to_path \"$config_file\" \"fish_add_path $INSTALL_DIR\"\n    ;;\n  zsh | bash | ash | sh)\n    add_to_path \"$config_file\" \"export PATH=$INSTALL_DIR:\\$PATH\"\n    ;;\n  *)\n    print_message warning \"Manually add the directory to $config_file (or similar):\"\n    print_message info \"  export PATH=$INSTALL_DIR:\\$PATH\"\n    ;;\n  esac\nfi\n\nprint_message info \"To use the command, please restart your terminal or run:\"\nprint_message info \"  source $config_file\"\n\nif [ -n \"${GITHUB_ACTIONS-}\" ] && [ \"${GITHUB_ACTIONS}\" == \"true\" ]; then\n  echo \"$INSTALL_DIR\" >>\"$GITHUB_PATH\"\n  print_message info \"Added $INSTALL_DIR to \\$GITHUB_PATH\"\nfi\n"
  },
  {
    "path": "k8s/gorush-aws-alb-ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: gorush\n  namespace: gorush\n  annotations:\n    # Kubernetes Ingress Controller for AWS ALB\n    # https://github.com/coreos/alb-ingress-controller\n    alb.ingress.kubernetes.io/scheme: internet-facing\n    alb.ingress.kubernetes.io/subnets: subnet-aa3dfbe3,subnet-4aff342d\n    alb.ingress.kubernetes.io/security-groups: sg-71069b17\nspec:\n  rules:\n    - host: gorush.example.com\n      http:\n        paths:\n          - path: /\n            pathType: Prefix\n            backend:\n              service:\n                name: gorush\n                port:\n                  number: 8088\n"
  },
  {
    "path": "k8s/gorush-configmap.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: gorush-config\n  namespace: gorush\ndata:\n  # stat\n  stat.engine: redis\n  stat.redis.host: redis:6379\n"
  },
  {
    "path": "k8s/gorush-deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: gorush\n  namespace: gorush\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: gorush\n      tier: frontend\n  template:\n    metadata:\n      labels:\n        app: gorush\n        tier: frontend\n    spec:\n      containers:\n        - image: appleboy/gorush:1.18.4\n          name: gorush\n          imagePullPolicy: Always\n          ports:\n            - containerPort: 8088\n          securityContext:\n            capabilities:\n              drop:\n                - ALL\n            runAsNonRoot: true\n            runAsUser: 1000\n            allowPrivilegeEscalation: false\n            readOnlyRootFilesystem: false\n          resources:\n            requests:\n              cpu: \"250m\"\n              memory: \"256Mi\"\n            limits:\n              cpu: \"500m\"\n              memory: \"512Mi\"\n          livenessProbe:\n            httpGet:\n              path: /healthz\n              port: 8000\n            initialDelaySeconds: 3\n            periodSeconds: 3\n          env:\n            - name: GORUSH_STAT_ENGINE\n              valueFrom:\n                configMapKeyRef:\n                  name: gorush-config\n                  key: stat.engine\n            - name: GORUSH_STAT_REDIS_ADDR\n              valueFrom:\n                configMapKeyRef:\n                  name: gorush-config\n                  key: stat.redis.host\n            - name: GORUSH_CORE_PORT\n              value: \"8000\"\n"
  },
  {
    "path": "k8s/gorush-namespace.yaml",
    "content": "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: gorush\n"
  },
  {
    "path": "k8s/gorush-redis-deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: redis\n  namespace: gorush\nspec:\n  selector:\n    matchLabels:\n      app: redis\n      role: master\n      tier: backend\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: redis\n        role: master\n        tier: backend\n    spec:\n      containers:\n        - name: master\n          image: redis:7\n          ports:\n            - containerPort: 6379\n          securityContext:\n            capabilities:\n              drop:\n                - ALL\n            runAsNonRoot: true\n            runAsUser: 999\n            allowPrivilegeEscalation: false\n            readOnlyRootFilesystem: false\n          resources:\n            requests:\n              cpu: \"250m\"\n              memory: \"256Mi\"\n            limits:\n              cpu: \"500m\"\n              memory: \"512Mi\"\n"
  },
  {
    "path": "k8s/gorush-redis-service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: redis\n  namespace: gorush\n  labels:\n    app: redis\n    role: master\n    tier: backend\nspec:\n  ports:\n    - port: 6379\n      targetPort: 6379\n  selector:\n    app: redis\n    role: master\n    tier: backend\n"
  },
  {
    "path": "k8s/gorush-service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: gorush\n  namespace: gorush\n  labels:\n    app: gorush\n    tier: frontend\nspec:\n  selector:\n    app: gorush\n    tier: frontend\n  # if your cluster supports it, uncomment the following to automatically create\n  # an external load-balanced IP for the frontend service.\n  # type: LoadBalancer\n  #\n  # if you want to expose the service to the outside (without a load balancer in front)\n  # type: NodePort\n  #\n  # if you want gorush to be accessible only within the cluster\n  # type: ClusterIP\n  ports:\n    - protocol: TCP\n      port: 80\n      targetPort: 8088\n"
  },
  {
    "path": "logx/log/.gitkeep",
    "content": ""
  },
  {
    "path": "logx/log/access.log",
    "content": ""
  },
  {
    "path": "logx/log.go",
    "content": "package logx\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nvar (\n\tgreen  = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})\n\tyellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})\n\tred    = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})\n\tblue   = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})\n\treset  = string([]byte{27, 91, 48, 109})\n)\n\n// LogPushEntry is push response log\ntype LogPushEntry struct {\n\tID       string `json:\"notif_id,omitempty\"`\n\tType     string `json:\"type\"`\n\tPlatform string `json:\"platform\"`\n\tToken    string `json:\"token\"`\n\tMessage  string `json:\"message\"`\n\tError    string `json:\"error\"`\n}\n\nvar isTerm bool\n\n//nolint:gochecknoinits // init is used to detect terminal for log formatting\nfunc init() {\n\tisTerm = isatty.IsTerminal(os.Stdout.Fd())\n}\n\nvar (\n\t// LogAccess is log server request log\n\tLogAccess = logrus.New()\n\t// LogError is log server error log\n\tLogError = logrus.New()\n)\n\n// InitLog use for initial log module\nfunc InitLog(accessLevel, accessLog, errorLevel, errorLog string) error {\n\tvar err error\n\n\tif !isTerm {\n\t\tLogAccess.SetFormatter(&logrus.JSONFormatter{})\n\t\tLogError.SetFormatter(&logrus.JSONFormatter{})\n\t} else {\n\t\tLogAccess.Formatter = &logrus.TextFormatter{\n\t\t\tTimestampFormat: \"2006/01/02 - 15:04:05\",\n\t\t\tFullTimestamp:   true,\n\t\t}\n\n\t\tLogError.Formatter = &logrus.TextFormatter{\n\t\t\tTimestampFormat: \"2006/01/02 - 15:04:05\",\n\t\t\tFullTimestamp:   true,\n\t\t}\n\t}\n\n\t// set logger\n\tif err = SetLogLevel(LogAccess, accessLevel); err != nil {\n\t\treturn errors.New(\"Set access log level error: \" + err.Error())\n\t}\n\n\tif err = SetLogLevel(LogError, errorLevel); err != nil {\n\t\treturn errors.New(\"Set error log level error: \" + err.Error())\n\t}\n\n\tif err = SetLogOut(LogAccess, accessLog); err != nil {\n\t\treturn errors.New(\"Set access log path error: \" + err.Error())\n\t}\n\n\tif err = SetLogOut(LogError, errorLog); err != nil {\n\t\treturn errors.New(\"Set error log path error: \" + err.Error())\n\t}\n\n\treturn nil\n}\n\n// SetLogOut provide log stdout and stderr output\nfunc SetLogOut(log *logrus.Logger, outString string) error {\n\tswitch outString {\n\tcase \"stdout\":\n\t\tlog.Out = os.Stdout\n\tcase \"stderr\":\n\t\tlog.Out = os.Stderr\n\tdefault:\n\t\tf, err := os.OpenFile(outString, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Out = f\n\t}\n\n\treturn nil\n}\n\n// SetLogLevel is define log level what you want\n// log level: panic, fatal, error, warn, info and debug\nfunc SetLogLevel(log *logrus.Logger, levelString string) error {\n\tlevel, err := logrus.ParseLevel(levelString)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Level = level\n\n\treturn nil\n}\n\nfunc colorForPlatForm(platform int) string {\n\tswitch platform {\n\tcase core.PlatFormIos:\n\t\treturn blue\n\tcase core.PlatFormAndroid:\n\t\treturn yellow\n\tcase core.PlatFormHuawei:\n\t\treturn green\n\tdefault:\n\t\treturn reset\n\t}\n}\n\nfunc typeForPlatForm(platform int) string {\n\tswitch platform {\n\tcase core.PlatFormIos:\n\t\treturn \"ios\"\n\tcase core.PlatFormAndroid:\n\t\treturn \"android\"\n\tcase core.PlatFormHuawei:\n\t\treturn \"huawei\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc hideToken(token string, markLen int) string {\n\tif token == \"\" {\n\t\treturn \"\"\n\t}\n\n\tif len(token) < markLen*2 {\n\t\treturn strings.Repeat(\"*\", len(token))\n\t}\n\n\tstart := token[len(token)-markLen:]\n\tend := token[0:markLen]\n\n\tresult := strings.ReplaceAll(token, start, strings.Repeat(\"*\", markLen))\n\tresult = strings.ReplaceAll(result, end, strings.Repeat(\"*\", markLen))\n\n\treturn result\n}\n\n// GetLogPushEntry get push data into log structure\nfunc GetLogPushEntry(input *InputLog) LogPushEntry {\n\tvar errMsg string\n\n\tplat := typeForPlatForm(input.Platform)\n\n\tif input.Error != nil {\n\t\terrMsg = input.Error.Error()\n\t}\n\n\ttoken := input.Token\n\tif input.HideToken {\n\t\ttoken = hideToken(input.Token, 10)\n\t}\n\n\tmessage := input.Message\n\tif input.HideMessage {\n\t\tmessage = \"(message redacted)\"\n\t}\n\n\treturn LogPushEntry{\n\t\tID:       input.ID,\n\t\tType:     input.Status,\n\t\tPlatform: plat,\n\t\tToken:    token,\n\t\tMessage:  message,\n\t\tError:    errMsg,\n\t}\n}\n\n// InputLog log request\ntype InputLog struct {\n\tID          string\n\tStatus      string\n\tToken       string\n\tMessage     string\n\tPlatform    int\n\tError       error\n\tHideToken   bool\n\tHideMessage bool\n\tFormat      string\n}\n\n// LogPush record user push request and server response.\nfunc LogPush(input *InputLog) LogPushEntry {\n\tvar platColor, resetColor, output string\n\n\tif isTerm {\n\t\tplatColor = colorForPlatForm(input.Platform)\n\t\tresetColor = reset\n\t}\n\n\tlog := GetLogPushEntry(input)\n\n\tif input.Format == \"json\" {\n\t\tlogJSON, _ := json.Marshal(log)\n\n\t\toutput = string(logJSON)\n\t} else {\n\t\tvar typeColor string\n\t\tswitch input.Status {\n\t\tcase core.SucceededPush:\n\t\t\tif isTerm {\n\t\t\t\ttypeColor = green\n\t\t\t}\n\n\t\t\toutput = fmt.Sprintf(\"|%s %s %s| %s%s%s [%s] %s\",\n\t\t\t\ttypeColor, log.Type, resetColor,\n\t\t\t\tplatColor, log.Platform, resetColor,\n\t\t\t\tlog.Token,\n\t\t\t\tlog.Message,\n\t\t\t)\n\t\tcase core.FailedPush:\n\t\t\tif isTerm {\n\t\t\t\ttypeColor = red\n\t\t\t}\n\n\t\t\toutput = fmt.Sprintf(\"|%s %s %s| %s%s%s [%s] | %s | Error Message: %s\",\n\t\t\t\ttypeColor, log.Type, resetColor,\n\t\t\t\tplatColor, log.Platform, resetColor,\n\t\t\t\tlog.Token,\n\t\t\t\tlog.Message,\n\t\t\t\tlog.Error,\n\t\t\t)\n\t\t}\n\t}\n\n\tswitch input.Status {\n\tcase core.SucceededPush:\n\t\tLogAccess.Info(output)\n\tcase core.FailedPush:\n\t\tLogError.Error(output)\n\t}\n\n\treturn log\n}\n"
  },
  {
    "path": "logx/log_interface.go",
    "content": "package logx\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// QueueLogger for simple logger.\nfunc QueueLogger() DefaultQueueLogger {\n\treturn DefaultQueueLogger{\n\t\taccessLogger: LogAccess,\n\t\terrorLogger:  LogError,\n\t}\n}\n\n// DefaultQueueLogger for queue custom logger\ntype DefaultQueueLogger struct {\n\taccessLogger *logrus.Logger\n\terrorLogger  *logrus.Logger\n}\n\nfunc (l DefaultQueueLogger) Infof(format string, args ...any) {\n\tl.accessLogger.Printf(format, args...)\n}\n\nfunc (l DefaultQueueLogger) Errorf(format string, args ...any) {\n\tl.errorLogger.Printf(format, args...)\n}\n\nfunc (l DefaultQueueLogger) Fatalf(format string, args ...any) {\n\tl.errorLogger.Fatalf(format, args...)\n}\n\nfunc (l DefaultQueueLogger) Info(args ...any) {\n\tl.accessLogger.Println(fmt.Sprint(args...))\n}\n\nfunc (l DefaultQueueLogger) Error(args ...any) {\n\tl.errorLogger.Println(fmt.Sprint(args...))\n}\n\nfunc (l DefaultQueueLogger) Fatal(args ...any) {\n\tl.errorLogger.Println(fmt.Sprint(args...))\n}\n"
  },
  {
    "path": "logx/log_test.go",
    "content": "package logx\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar invalidLevel = \"invalid\"\n\nfunc TestSetLogLevel(t *testing.T) {\n\tlog := logrus.New()\n\n\terr := SetLogLevel(log, \"debug\")\n\trequire.NoError(t, err)\n\n\terr = SetLogLevel(log, invalidLevel)\n\tassert.Equal(t, \"not a valid logrus Level: \\\"invalid\\\"\", err.Error())\n}\n\nfunc TestSetLogOut(t *testing.T) {\n\tlog := logrus.New()\n\n\terr := SetLogOut(log, \"stdout\")\n\trequire.NoError(t, err)\n\n\terr = SetLogOut(log, \"stderr\")\n\trequire.NoError(t, err)\n\n\terr = SetLogOut(log, \"log/access.log\")\n\trequire.NoError(t, err)\n\n\t// missing create logs folder.\n\terr = SetLogOut(log, \"logs/access.log\")\n\trequire.Error(t, err)\n}\n\nfunc TestInitDefaultLog(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\t// no errors on default config\n\trequire.NoError(t, InitLog(\n\t\tcfg.Log.AccessLevel,\n\t\tcfg.Log.AccessLog,\n\t\tcfg.Log.ErrorLevel,\n\t\tcfg.Log.ErrorLog,\n\t))\n\n\tcfg.Log.AccessLevel = invalidLevel\n\n\trequire.Error(t, InitLog(\n\t\tcfg.Log.AccessLevel,\n\t\tcfg.Log.AccessLog,\n\t\tcfg.Log.ErrorLevel,\n\t\tcfg.Log.ErrorLog,\n\t))\n\n\tisTerm = true\n\n\trequire.Error(t, InitLog(\n\t\tcfg.Log.AccessLevel,\n\t\tcfg.Log.AccessLog,\n\t\tcfg.Log.ErrorLevel,\n\t\tcfg.Log.ErrorLog,\n\t))\n}\n\nfunc TestAccessLevel(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Log.AccessLevel = invalidLevel\n\n\trequire.Error(t, InitLog(\n\t\tcfg.Log.AccessLevel,\n\t\tcfg.Log.AccessLog,\n\t\tcfg.Log.ErrorLevel,\n\t\tcfg.Log.ErrorLog,\n\t))\n}\n\nfunc TestErrorLevel(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Log.ErrorLevel = invalidLevel\n\n\trequire.Error(t, InitLog(\n\t\tcfg.Log.AccessLevel,\n\t\tcfg.Log.AccessLog,\n\t\tcfg.Log.ErrorLevel,\n\t\tcfg.Log.ErrorLog,\n\t))\n}\n\nfunc TestAccessLogPath(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Log.AccessLog = \"logs/access.log\"\n\n\trequire.Error(t, InitLog(\n\t\tcfg.Log.AccessLevel,\n\t\tcfg.Log.AccessLog,\n\t\tcfg.Log.ErrorLevel,\n\t\tcfg.Log.ErrorLog,\n\t))\n}\n\nfunc TestErrorLogPath(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Log.ErrorLog = \"logs/error.log\"\n\n\trequire.Error(t, InitLog(\n\t\tcfg.Log.AccessLevel,\n\t\tcfg.Log.AccessLog,\n\t\tcfg.Log.ErrorLevel,\n\t\tcfg.Log.ErrorLog,\n\t))\n}\n\nfunc TestPlatFormType(t *testing.T) {\n\tassert.Equal(t, \"ios\", typeForPlatForm(core.PlatFormIos))\n\tassert.Equal(t, \"android\", typeForPlatForm(core.PlatFormAndroid))\n\tassert.Equal(t, \"huawei\", typeForPlatForm(core.PlatFormHuawei))\n\tassert.Empty(t, typeForPlatForm(10000))\n}\n\nfunc TestPlatFormColor(t *testing.T) {\n\tassert.Equal(t, blue, colorForPlatForm(core.PlatFormIos))\n\tassert.Equal(t, yellow, colorForPlatForm(core.PlatFormAndroid))\n\tassert.Equal(t, green, colorForPlatForm(core.PlatFormHuawei))\n\tassert.Equal(t, reset, colorForPlatForm(1000000))\n}\n\nfunc TestHideToken(t *testing.T) {\n\tassert.Empty(t, hideToken(\"\", 2))\n\tassert.Equal(t, \"**345678**\", hideToken(\"1234567890\", 2))\n\tassert.Equal(t, \"*****\", hideToken(\"12345\", 10))\n}\n\nfunc TestLogPushEntry(t *testing.T) {\n\tin := InputLog{}\n\n\tin.Platform = 1\n\tassert.Equal(t, \"ios\", GetLogPushEntry(&in).Platform)\n\n\tin.Error = errors.New(\"error\")\n\tassert.Equal(t, \"error\", GetLogPushEntry(&in).Error)\n\n\tin.Token = \"1234567890\"\n\tin.HideToken = true\n\tassert.Equal(t, \"**********\", GetLogPushEntry(&in).Token)\n\n\tin.Message = \"hellothisisamessage\"\n\tin.HideMessage = true\n\tassert.Equal(t, \"(message redacted)\", GetLogPushEntry(&in).Message)\n}\n\nfunc TestLogPush(t *testing.T) {\n\tin := InputLog{}\n\tisTerm = true\n\n\tin.Format = \"json\"\n\tin.Status = \"succeeded-push\"\n\tassert.Equal(t, \"succeeded-push\", LogPush(&in).Type)\n\n\tin.Format = \"\"\n\tin.Message = \"success\"\n\tassert.Equal(t, \"success\", LogPush(&in).Message)\n\n\tin.Status = \"failed-push\"\n\tin.Message = \"failed\"\n\tassert.Equal(t, \"failed\", LogPush(&in).Message)\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/appleboy/gorush/app\"\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/notify\"\n\t\"github.com/appleboy/gorush/router\"\n\t\"github.com/appleboy/gorush/rpc\"\n\t\"github.com/appleboy/gorush/status\"\n\n\t\"github.com/appleboy/graceful\"\n)\n\nfunc main() {\n\t// Parse CLI flags\n\topts := app.NewOptions()\n\topts.BindFlags()\n\tflag.Usage = usage\n\tflag.Parse()\n\n\trouter.SetVersion(version)\n\trouter.SetCommit(commit)\n\n\t// Show version and exit\n\tif opts.ShowVersion {\n\t\trouter.PrintGoRushVersion()\n\t\tos.Exit(0)\n\t}\n\n\t// Load and merge configuration\n\tcfg, err := app.ValidateAndMerge(opts)\n\tif err != nil {\n\t\tlog.Fatalf(\"Configuration error: %v\", err)\n\t}\n\n\t// Initialize push slots for concurrent iOS pushes\n\tnotify.MaxConcurrentIOSPushes = make(chan struct{}, cfg.Ios.MaxConcurrentPushes)\n\n\tif err = logx.InitLog(\n\t\tcfg.Log.AccessLevel,\n\t\tcfg.Log.AccessLog,\n\t\tcfg.Log.ErrorLevel,\n\t\tcfg.Log.ErrorLog,\n\t); err != nil {\n\t\tlog.Fatalf(\"can't load log module, error: %v\", err)\n\t}\n\n\tif cfg.Core.HTTPProxy != \"\" {\n\t\tif err = notify.SetProxy(cfg.Core.HTTPProxy); err != nil {\n\t\t\tlogx.LogError.Fatalf(\"Set Proxy error: %v\", err)\n\t\t}\n\t}\n\n\tg := graceful.NewManager(\n\t\tgraceful.WithLogger(logx.QueueLogger()),\n\t)\n\n\tif opts.Ping {\n\t\tif err := pinger(g.ShutdownContext(), cfg); err != nil {\n\t\t\tlogx.LogError.Fatal(err)\n\t\t}\n\t\treturn\n\t}\n\n\t// Handle CLI notification mode\n\tif opts.IsCLIMode() {\n\t\tif err := handleCLINotification(g.ShutdownContext(), cfg, opts); err != nil {\n\t\t\tlogx.LogError.Fatalf(\"Failed to send notification: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\n\tif err = notify.CheckPushConf(cfg); err != nil {\n\t\tlogx.LogError.Fatal(err)\n\t}\n\n\tif err = createPIDFile(cfg); err != nil {\n\t\tlogx.LogError.Fatal(err)\n\t}\n\n\tif err = status.InitAppStatus(cfg); err != nil {\n\t\tlogx.LogError.Fatal(err)\n\t}\n\n\tw, err := app.NewQueueWorker(cfg)\n\tif err != nil {\n\t\tlogx.LogError.Fatal(err)\n\t}\n\n\tq := app.NewQueuePool(cfg, w)\n\n\tg.AddShutdownJob(func() error {\n\t\t// logx.LogAccess.Info(\"close the queue system, current queue usage: \", q.Usage())\n\t\t// stop queue system and wait job completed\n\t\tq.Release()\n\t\t// close the connection with storage\n\t\tlogx.LogAccess.Info(\"close the storage connection: \", cfg.Stat.Engine)\n\t\tif err := status.StatStorage.Close(); err != nil {\n\t\t\tlogx.LogError.Fatal(\"can't close the storage connection: \", err.Error())\n\t\t}\n\t\treturn nil\n\t})\n\n\tif cfg.Ios.Enabled {\n\t\tif err = notify.InitAPNSClient(g.ShutdownContext(), cfg); err != nil {\n\t\t\tlogx.LogError.Fatal(err)\n\t\t}\n\t}\n\n\tif cfg.Android.Enabled {\n\t\tif _, err = notify.InitFCMClient(g.ShutdownContext(), cfg); err != nil {\n\t\t\tlogx.LogError.Fatal(err)\n\t\t}\n\t}\n\n\tif cfg.Huawei.Enabled {\n\t\tif _, err = notify.InitHMSClient(cfg, cfg.Huawei.AppSecret, cfg.Huawei.AppID); err != nil {\n\t\t\tlogx.LogError.Fatal(err)\n\t\t}\n\t}\n\n\tg.AddRunningJob(func(ctx context.Context) error {\n\t\treturn router.RunHTTPServer(ctx, cfg, q)\n\t})\n\n\tg.AddRunningJob(func(ctx context.Context) error {\n\t\treturn rpc.RunGRPCServer(ctx, cfg)\n\t})\n\n\t<-g.Done()\n}\n\n// Version control for notify.\nvar (\n\tversion = \"No Version Provided\"\n\tcommit  = \"No Commit Provided\"\n)\n\nvar usageStr = `\n  ________                              .__\n /  _____/   ____ _______  __ __  ______|  |__\n/   \\  ___  /  _ \\\\_  __ \\|  |  \\/  ___/|  |  \\\n\\    \\_\\  \\(  <_> )|  | \\/|  |  /\\___ \\ |   Y  \\\n \\______  / \\____/ |__|   |____//____  >|___|  /\n        \\/                           \\/      \\/\n\nUsage: gorush [options]\n\nServer Options:\n    -A, --address <address>          Address to bind (default: any)\n    -p, --port <port>                Use port for clients (default: 8088)\n    -c, --config <file>              Configuration file path\n    -m, --message <message>          Notification message\n    -t, --token <token>              Notification token\n    -e, --engine <engine>            Storage engine (memory, redis ...)\n    --title <title>                  Notification title\n    --proxy <proxy>                  Proxy URL\n    --pid <pid path>                 Process identifier path\n    --redis-addr <redis addr>        Redis addr (default: localhost:6379)\n    --ping                           healthy check command for container\niOS Options:\n    -i, --key <file>                 certificate key file path\n    -P, --password <password>        certificate key password\n    --ios                            enabled iOS (default: false)\n    --production                     iOS production mode (default: false)\nAndroid Options:\n    --fcm-key <fcm_key_path>         FCM Credentials Key Path\n    --android                        enabled android (default: false)\nHuawei Options:\n    -hk, --hmskey <hms_key>          HMS App Secret\n    -hid, --hmsid <hms_id>           HMS App ID\n    --huawei                         enabled huawei (default: false)\nCommon Options:\n    --topic <topic>                  iOS, Android or Huawei topic message\n    -h, --help                       Show this message\n    -V, --version                    Show version\n`\n\n// usage will print out the flag options for the server.\nfunc usage() {\n\tos.Stdout.WriteString(usageStr + \"\\n\")\n}\n\n// handleCLINotification handles sending notifications in CLI mode.\nfunc handleCLINotification(ctx context.Context, cfg *config.ConfYaml, opts *app.Options) error {\n\tsendOpts := opts.CLISendOptions()\n\n\tif opts.Conf.Android.Enabled {\n\t\treturn app.SendAndroidNotification(ctx, cfg, sendOpts)\n\t}\n\n\tif opts.Conf.Huawei.Enabled {\n\t\treturn app.SendHuaweiNotification(ctx, cfg, sendOpts)\n\t}\n\n\tif opts.Conf.Ios.Enabled {\n\t\treturn app.SendIOSNotification(ctx, cfg, sendOpts)\n\t}\n\n\treturn nil\n}\n\n// handles pinging the endpoint and returns an error if the\n// agent is in an unhealthy state.\nfunc pinger(ctx context.Context, cfg *config.ConfYaml) error {\n\ttransport := &http.Transport{\n\t\tDial: (&net.Dialer{\n\t\t\tTimeout: 5 * time.Second,\n\t\t}).Dial,\n\t\tTLSHandshakeTimeout: 5 * time.Second,\n\t}\n\tclient := &http.Client{\n\t\tTimeout:   time.Second * 10,\n\t\tTransport: transport,\n\t}\n\treq, _ := http.NewRequestWithContext(\n\t\tctx,\n\t\thttp.MethodGet,\n\t\t\"http://localhost:\"+cfg.Core.Port+cfg.API.HealthURI,\n\t\tnil,\n\t)\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errors.New(\"server returned non-200 status code\")\n\t}\n\treturn nil\n}\n\nfunc createPIDFile(cfg *config.ConfYaml) error {\n\tif !cfg.Core.PID.Enabled {\n\t\treturn nil\n\t}\n\n\tpidPath := cfg.Core.PID.Path\n\n\t// Additional validation before creating PID file\n\tif err := config.ValidatePIDPath(pidPath); err != nil {\n\t\treturn err\n\t}\n\t_, err := os.Stat(pidPath)\n\tif os.IsNotExist(err) || cfg.Core.PID.Override {\n\t\tcurrentPid := os.Getpid()\n\t\tif err := os.MkdirAll(filepath.Dir(pidPath), os.ModePerm); err != nil {\n\t\t\treturn fmt.Errorf(\"can't create PID folder: %w\", err)\n\t\t}\n\n\t\tfile, err := os.Create(pidPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"can't create PID file: %w\", err)\n\t\t}\n\t\tdefer file.Close()\n\t\tif _, err := file.WriteString(strconv.FormatInt(int64(currentPid), 10)); err != nil {\n\t\t\treturn fmt.Errorf(\"can't write PID information on %s: %w\", pidPath, err)\n\t\t}\n\t} else {\n\t\treturn fmt.Errorf(\"%s already exists\", pidPath)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "metric/metrics.go",
    "content": "package metric\n\nimport (\n\t\"github.com/appleboy/gorush/status\"\n\n\t\"github.com/golang-queue/queue\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nconst namespace = \"gorush_\"\n\n// Metrics implements the prometheus.Metrics interface and\n// exposes gorush metrics for prometheus\ntype Metrics struct {\n\tTotalPushCount *prometheus.Desc\n\tIosSuccess     *prometheus.Desc\n\tIosError       *prometheus.Desc\n\tAndroidSuccess *prometheus.Desc\n\tAndroidError   *prometheus.Desc\n\tHuaweiSuccess  *prometheus.Desc\n\tHuaweiError    *prometheus.Desc\n\tBusyWorkers    *prometheus.Desc\n\tSuccessTasks   *prometheus.Desc\n\tFailureTasks   *prometheus.Desc\n\tSubmittedTasks *prometheus.Desc\n\tq              *queue.Queue\n}\n\n// NewMetrics returns a new Metrics with all prometheus.Desc initialized\nfunc NewMetrics(q *queue.Queue) Metrics {\n\tm := Metrics{\n\t\tTotalPushCount: prometheus.NewDesc(\n\t\t\tnamespace+\"total_push_count\",\n\t\t\t\"Number of push count\",\n\t\t\tnil, nil,\n\t\t),\n\t\tIosSuccess: prometheus.NewDesc(\n\t\t\tnamespace+\"ios_success\",\n\t\t\t\"Number of iOS success count\",\n\t\t\tnil, nil,\n\t\t),\n\t\tIosError: prometheus.NewDesc(\n\t\t\tnamespace+\"ios_error\",\n\t\t\t\"Number of iOS fail count\",\n\t\t\tnil, nil,\n\t\t),\n\t\tAndroidSuccess: prometheus.NewDesc(\n\t\t\tnamespace+\"android_success\",\n\t\t\t\"Number of android success count\",\n\t\t\tnil, nil,\n\t\t),\n\t\tAndroidError: prometheus.NewDesc(\n\t\t\tnamespace+\"android_fail\",\n\t\t\t\"Number of android fail count\",\n\t\t\tnil, nil,\n\t\t),\n\t\tHuaweiSuccess: prometheus.NewDesc(\n\t\t\tnamespace+\"huawei_success\",\n\t\t\t\"Number of huawei success count\",\n\t\t\tnil, nil,\n\t\t),\n\t\tHuaweiError: prometheus.NewDesc(\n\t\t\tnamespace+\"huawei_fail\",\n\t\t\t\"Number of huawei fail count\",\n\t\t\tnil, nil,\n\t\t),\n\t\tBusyWorkers: prometheus.NewDesc(\n\t\t\tnamespace+\"busy_workers\",\n\t\t\t\"Length of busy workers\",\n\t\t\tnil, nil,\n\t\t),\n\t\tFailureTasks: prometheus.NewDesc(\n\t\t\tnamespace+\"failure_tasks\",\n\t\t\t\"Length of Failure Tasks\",\n\t\t\tnil, nil,\n\t\t),\n\t\tSuccessTasks: prometheus.NewDesc(\n\t\t\tnamespace+\"success_tasks\",\n\t\t\t\"Length of Success Tasks\",\n\t\t\tnil, nil,\n\t\t),\n\t\tSubmittedTasks: prometheus.NewDesc(\n\t\t\tnamespace+\"submitted_tasks\",\n\t\t\t\"Length of Submitted Tasks\",\n\t\t\tnil, nil,\n\t\t),\n\t\tq: q,\n\t}\n\n\treturn m\n}\n\n// Describe returns all possible prometheus.Desc\nfunc (c Metrics) Describe(ch chan<- *prometheus.Desc) {\n\tch <- c.TotalPushCount\n\tch <- c.IosSuccess\n\tch <- c.IosError\n\tch <- c.AndroidSuccess\n\tch <- c.AndroidError\n\tch <- c.HuaweiSuccess\n\tch <- c.HuaweiError\n\tch <- c.BusyWorkers\n\tch <- c.SuccessTasks\n\tch <- c.FailureTasks\n\tch <- c.SubmittedTasks\n}\n\n// Collect returns the metrics with values\nfunc (c Metrics) Collect(ch chan<- prometheus.Metric) {\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.TotalPushCount,\n\t\tprometheus.CounterValue,\n\t\tfloat64(status.StatStorage.GetTotalCount()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.IosSuccess,\n\t\tprometheus.CounterValue,\n\t\tfloat64(status.StatStorage.GetIosSuccess()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.IosError,\n\t\tprometheus.CounterValue,\n\t\tfloat64(status.StatStorage.GetIosError()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.AndroidSuccess,\n\t\tprometheus.CounterValue,\n\t\tfloat64(status.StatStorage.GetAndroidSuccess()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.AndroidError,\n\t\tprometheus.CounterValue,\n\t\tfloat64(status.StatStorage.GetAndroidError()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.HuaweiSuccess,\n\t\tprometheus.CounterValue,\n\t\tfloat64(status.StatStorage.GetHuaweiSuccess()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.HuaweiError,\n\t\tprometheus.CounterValue,\n\t\tfloat64(status.StatStorage.GetHuaweiError()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.BusyWorkers,\n\t\tprometheus.GaugeValue,\n\t\tfloat64(c.q.BusyWorkers()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.SuccessTasks,\n\t\tprometheus.CounterValue,\n\t\tfloat64(c.q.SuccessTasks()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.FailureTasks,\n\t\tprometheus.CounterValue,\n\t\tfloat64(c.q.FailureTasks()),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tc.SubmittedTasks,\n\t\tprometheus.CounterValue,\n\t\tfloat64(c.q.SubmittedTasks()),\n\t)\n}\n"
  },
  {
    "path": "metric/metrics_test.go",
    "content": "package metric\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/golang-queue/queue\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar noTask = func(ctx context.Context) error { return nil }\n\nfunc TestNewMetrics(t *testing.T) {\n\tq := queue.NewPool(10)\n\tassert.NoError(t, q.QueueTask(noTask))\n\tassert.NoError(t, q.QueueTask(noTask))\n\ttime.Sleep(10 * time.Millisecond)\n\tdefer q.Release()\n\tm := NewMetrics(q)\n\tassert.Equal(t, uint64(2), m.q.SubmittedTasks())\n\tassert.Equal(t, uint64(2), m.q.SuccessTasks())\n}\n"
  },
  {
    "path": "netlify.toml",
    "content": "[build]\ncommand = \"make build_linux_lambda\"\nfunctions = \"release/linux/lambda\"\n\n[build.environment]\nGO111MODULE = \"on\"\nGO_IMPORT_PATH = \"github.com/appleboy/gorush\"\nGO_VERSION = \"1.25.0\"\n\n[[redirects]]\nfrom = \"/*\"\nstatus = 200\nto = \"/.netlify/functions/gorush/:splat\"\n"
  },
  {
    "path": "notify/feedback.go",
    "content": "package notify\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/appleboy/gorush/logx\"\n)\n\n// extractHeaders converts a slice of strings to a map of strings.\nfunc extractHeaders(headers []string) map[string]string {\n\tresult := make(map[string]string)\n\tfor _, header := range headers {\n\t\tparts := strings.Split(header, \":\")\n\t\tif len(parts) == 2 {\n\t\t\tresult[parts[0]] = parts[1]\n\t\t}\n\t}\n\treturn result\n}\n\n// DispatchFeedback sends a feedback log entry to a specified URL via an HTTP POST request.\n//\n// Parameters:\n//   - ctx: The context for the HTTP request.\n//   - log: The log entry to be sent as feedback.\n//   - url: The destination URL for the feedback.\n//   - timeout: The timeout duration for the HTTP request in seconds.\n//   - header: A slice of strings representing additional headers to be included in the request.\n//\n// Returns:\n//   - error: An error if the request fails or the response status is not OK.\nfunc DispatchFeedback(\n\tctx context.Context,\n\tlog logx.LogPushEntry,\n\turl string,\n\ttimeout int64,\n\theader []string,\n) error {\n\tif url == \"\" {\n\t\treturn errors.New(\"url can't be empty\")\n\t}\n\n\tpayload, err := json.Marshal(log)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)\n\tdefer cancel()\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(payload))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theaders := extractHeaders(header)\n\tfor k, v := range headers {\n\t\treq.Header.Set(k, strings.TrimSpace(v))\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\tfeedbackClient.Timeout = time.Duration(timeout) * time.Second\n\tresp, err := feedbackClient.Do(req)\n\n\tif resp != nil {\n\t\tdefer resp.Body.Close()\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errors.New(\"failed to send feedback\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "notify/feedback_test.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/logx\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEmptyFeedbackURL(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tlogEntry := logx.LogPushEntry{\n\t\tID:       \"\",\n\t\tType:     \"\",\n\t\tPlatform: \"\",\n\t\tToken:    \"\",\n\t\tMessage:  \"\",\n\t\tError:    \"\",\n\t}\n\n\terr := DispatchFeedback(\n\t\tcontext.Background(),\n\t\tlogEntry,\n\t\tcfg.Core.FeedbackURL,\n\t\tcfg.Core.FeedbackTimeout,\n\t\tcfg.Core.FeedbackHeader,\n\t)\n\trequire.Error(t, err)\n}\n\nfunc TestHTTPErrorInFeedbackCall(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tcfg.Core.FeedbackURL = \"http://test.example.com/api/\"\n\tlogEntry := logx.LogPushEntry{\n\t\tID:       \"\",\n\t\tType:     \"\",\n\t\tPlatform: \"\",\n\t\tToken:    \"\",\n\t\tMessage:  \"\",\n\t\tError:    \"\",\n\t}\n\n\terr := DispatchFeedback(\n\t\tcontext.Background(),\n\t\tlogEntry,\n\t\tcfg.Core.FeedbackURL,\n\t\tcfg.Core.FeedbackTimeout,\n\t\tcfg.Core.FeedbackHeader,\n\t)\n\trequire.Error(t, err)\n}\n\nfunc TestSuccessfulFeedbackCall(t *testing.T) {\n\t// Mock http server\n\thttpMock := httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.URL.Path == \"/dispatch\" {\n\t\t\t\t// check http header\n\t\t\t\tif r.Header.Get(\"x-gorush-token\") != \"1234\" {\n\t\t\t\t\tpanic(\"x-gorush-token header is not set\")\n\t\t\t\t}\n\n\t\t\t\tw.Header().Add(\"Content-Type\", \"application/json\")\n\t\t\t\t_, err := w.Write([]byte(`{}`))\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Println(err)\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}),\n\t)\n\tdefer httpMock.Close()\n\n\tcfg, _ := config.LoadConf()\n\tcfg.Core.FeedbackURL = httpMock.URL + \"/dispatch\"\n\tcfg.Core.FeedbackHeader = []string{\n\t\t\"x-gorush-token: 1234\",\n\t}\n\tlogEntry := logx.LogPushEntry{\n\t\tID:       \"\",\n\t\tType:     \"\",\n\t\tPlatform: \"\",\n\t\tToken:    \"\",\n\t\tMessage:  \"\",\n\t\tError:    \"\",\n\t}\n\n\terr := DispatchFeedback(\n\t\tcontext.Background(),\n\t\tlogEntry,\n\t\tcfg.Core.FeedbackURL,\n\t\tcfg.Core.FeedbackTimeout,\n\t\tcfg.Core.FeedbackHeader,\n\t)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "notify/global.go",
    "content": "package notify\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/appleboy/go-fcm\"\n\t\"github.com/appleboy/go-hms-push/push/core\"\n\t\"github.com/sideshow/apns2\"\n)\n\nvar (\n\t// ApnsClient is apns client\n\tApnsClient *apns2.Client\n\t// FCMClient is apns client\n\tFCMClient *fcm.Client\n\t// HMSClient is Huawei push client\n\tHMSClient *core.HMSClient\n\t// MaxConcurrentIOSPushes pool to limit the number of concurrent iOS pushes\n\tMaxConcurrentIOSPushes chan struct{}\n\n\ttransport = &http.Transport{\n\t\tDial: (&net.Dialer{\n\t\t\tTimeout: 5 * time.Second,\n\t\t}).Dial,\n\t\tTLSHandshakeTimeout: 5 * time.Second,\n\t\tMaxIdleConns:        5,\n\t\tMaxIdleConnsPerHost: 5,\n\t\tMaxConnsPerHost:     20,\n\t\tProxy:               http.ProxyFromEnvironment, // Support proxy\n\t}\n\tfeedbackClient = &http.Client{\n\t\tTransport: transport,\n\t}\n)\n\nconst (\n\tHIGH   = \"high\"\n\tNORMAL = \"nornal\"\n)\n"
  },
  {
    "path": "notify/main_test.go",
    "content": "package notify\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/status\"\n)\n\nfunc TestMain(m *testing.M) {\n\tcfg, _ := config.LoadConf()\n\tif err := status.InitAppStatus(cfg); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tos.Exit(m.Run())\n}\n"
  },
  {
    "path": "notify/notification.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\n\t\"firebase.google.com/go/v4/messaging\"\n\t\"github.com/appleboy/go-hms-push/push/model\"\n\tqcore \"github.com/golang-queue/queue/core\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nvar json = jsoniter.ConfigCompatibleWithStandardLibrary\n\n// D provide string array\ntype D map[string]any\n\nconst (\n\t// ApnsPriorityLow will tell APNs to send the push message at a time that takes\n\t// into account power considerations for the device. Notifications with this\n\t// priority might be grouped and delivered in bursts. They are throttled, and\n\t// in some cases are not delivered.\n\tApnsPriorityLow = 5\n\n\t// ApnsPriorityHigh will tell APNs to send the push message immediately.\n\t// Notifications with this priority must trigger an alert, sound, or badge on\n\t// the target device. It is an error to use this priority for a push\n\t// notification that contains only the content-available key.\n\tApnsPriorityHigh = 10\n)\n\n// Alert is APNs payload\ntype Alert struct {\n\tAction          string   `json:\"action,omitempty\"`\n\tActionLocKey    string   `json:\"action-loc-key,omitempty\"`\n\tBody            string   `json:\"body,omitempty\"`\n\tLaunchImage     string   `json:\"launch-image,omitempty\"`\n\tLocArgs         []string `json:\"loc-args,omitempty\"`\n\tLocKey          string   `json:\"loc-key,omitempty\"`\n\tTitle           string   `json:\"title,omitempty\"`\n\tSubtitle        string   `json:\"subtitle,omitempty\"`\n\tTitleLocArgs    []string `json:\"title-loc-args,omitempty\"`\n\tTitleLocKey     string   `json:\"title-loc-key,omitempty\"`\n\tSummaryArg      string   `json:\"summary-arg,omitempty\"`\n\tSummaryArgCount int      `json:\"summary-arg-count,omitempty\"`\n}\n\n// RequestPush support multiple notification request.\ntype RequestPush struct {\n\tNotifications []PushNotification `json:\"notifications\" binding:\"required\"`\n}\n\n// ResponsePush response of notification request.\ntype ResponsePush struct {\n\tLogs []logx.LogPushEntry `json:\"logs\"`\n}\n\n// PushNotification is single notification request\ntype PushNotification struct {\n\t// Common\n\tID               string   `json:\"notif_id,omitempty\"`\n\tTo               string   `json:\"to,omitempty\"`\n\tTopic            string   `json:\"topic,omitempty\"` // FCM and iOS only\n\tTokens           []string `json:\"tokens\"                      binding:\"required\"`\n\tPlatform         int      `json:\"platform\"                    binding:\"required\"`\n\tMessage          string   `json:\"message,omitempty\"`\n\tTitle            string   `json:\"title,omitempty\"`\n\tImage            string   `json:\"image,omitempty\"`\n\tPriority         string   `json:\"priority,omitempty\"`\n\tContentAvailable bool     `json:\"content_available,omitempty\"`\n\tMutableContent   bool     `json:\"mutable_content,omitempty\"`\n\tSound            any      `json:\"sound,omitempty\"`\n\tData             D        `json:\"data,omitempty\"`\n\tRetry            int      `json:\"retry,omitempty\"`\n\n\t// Android\n\tNotification *messaging.Notification  `json:\"notification,omitempty\"`\n\tAndroid      *messaging.AndroidConfig `json:\"android,omitempty\"`\n\tWebpush      *messaging.WebpushConfig `json:\"webpush,omitempty\"`\n\tAPNS         *messaging.APNSConfig    `json:\"apns,omitempty\"`\n\tFCMOptions   *messaging.FCMOptions    `json:\"fcm_options,omitempty\"`\n\tCondition    string                   `json:\"condition,omitempty\"`\n\n\t// Huawei\n\tAppID              string                     `json:\"app_id,omitempty\"`\n\tAppSecret          string                     `json:\"app_secret,omitempty\"`\n\tHuaweiNotification *model.AndroidNotification `json:\"huawei_notification,omitempty\"`\n\tHuaweiData         string                     `json:\"huawei_data,omitempty\"`\n\tHuaweiCollapseKey  int                        `json:\"huawei_collapse_key,omitempty\"`\n\tHuaweiTTL          string                     `json:\"huawei_ttl,omitempty\"`\n\tBiTag              string                     `json:\"bi_tag,omitempty\"`\n\tFastAppTarget      int                        `json:\"fast_app_target,omitempty\"`\n\n\t// iOS\n\tExpiration  *int64   `json:\"expiration,omitempty\"`\n\tApnsID      string   `json:\"apns_id,omitempty\"`\n\tCollapseID  string   `json:\"collapse_id,omitempty\"`\n\tPushType    string   `json:\"push_type,omitempty\"`\n\tBadge       *int     `json:\"badge,omitempty\"`\n\tCategory    string   `json:\"category,omitempty\"`\n\tThreadID    string   `json:\"thread-id,omitempty\"`\n\tURLArgs     []string `json:\"url-args,omitempty\"`\n\tAlert       Alert    `json:\"alert,omitzero\"`\n\tProduction  bool     `json:\"production,omitempty\"`\n\tDevelopment bool     `json:\"development,omitempty\"`\n\tSoundName   string   `json:\"name,omitempty\"`\n\tSoundVolume float32  `json:\"volume,omitempty\"`\n\n\t// ref: https://github.com/sideshow/apns2/blob/54928d6193dfe300b6b88dad72b7e2ae138d4f0a/payload/builder.go#L7-L24\n\tInterruptionLevel string `json:\"interruption_level,omitempty\"`\n\n\t// live-activity support\n\t// ref: https://apple.co/3MLe2DB\n\tContentState  D      `json:\"content-state,omitempty\"`\n\tStaleDate     int64  `json:\"stale-date,omitempty\"`\n\tDismissalDate int64  `json:\"dismissal-date\"`\n\tEvent         string `json:\"event,omitempty\"`\n\tTimestamp     int64  `json:\"timestamp,omitempty\"`\n}\n\n// Bytes for queue message\nfunc (p *PushNotification) Bytes() []byte {\n\tb, err := json.Marshal(p)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn b\n}\n\n// Payload for queue message\nfunc (p *PushNotification) Payload() []byte {\n\treturn nil\n}\n\n// IsTopic check if message format is topic for FCM\n// ref: https://firebase.google.com/docs/cloud-messaging/send-message#topic-http-post-request\nfunc (p *PushNotification) IsTopic() bool {\n\tif p.Platform == core.PlatFormHuawei || p.Platform == core.PlatFormAndroid {\n\t\treturn p.Topic != \"\" || p.Condition != \"\"\n\t}\n\n\treturn false\n}\n\n// CheckMessage for check request message\nfunc CheckMessage(req *PushNotification) error {\n\tvar msg string\n\n\tif req.To != \"\" {\n\t\treq.Tokens = append(req.Tokens, req.To)\n\t}\n\n\t// if the message is a topic, the tokens field is not required\n\tif !req.IsTopic() && len(req.Tokens) == 0 {\n\t\treturn errors.New(\"please provide at least one device token\")\n\t}\n\n\tswitch req.Platform {\n\tcase core.PlatFormIos:\n\t\tif len(req.Tokens) == 1 && req.Tokens[0] == \"\" {\n\t\t\tmsg = \"the device token cannot be empty\"\n\t\t\tlogx.LogAccess.Debug(msg)\n\t\t\treturn errors.New(msg)\n\t\t}\n\tcase\n\t\tcore.PlatFormAndroid,\n\t\tcore.PlatFormHuawei:\n\t\tif len(req.Tokens) > 500 {\n\t\t\t// https://firebase.google.com/docs/cloud-messaging/send-message#send-messages-to-multiple-devices\n\t\t\tmsg = \"you can specify up to 500 device registration tokens per invocation\"\n\t\t\tlogx.LogAccess.Debug(msg)\n\t\t\treturn errors.New(msg)\n\t\t}\n\tdefault:\n\t}\n\n\treturn nil\n}\n\n// SetProxy only working for FCM server.\nfunc SetProxy(proxy string) error {\n\tproxyURL, err := url.ParseRequestURI(proxy)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttp.DefaultTransport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}\n\tlogx.LogAccess.Debug(\"Set http proxy as \" + proxy)\n\n\treturn nil\n}\n\n// checkIOSConf validates iOS configuration.\nfunc checkIOSConf(cfg *config.ConfYaml) error {\n\tif !cfg.Ios.Enabled {\n\t\treturn nil\n\t}\n\tif cfg.Ios.KeyPath == \"\" && cfg.Ios.KeyBase64 == \"\" {\n\t\treturn errors.New(\"missing iOS certificate key\")\n\t}\n\tif cfg.Ios.KeyPath != \"\" {\n\t\tif _, err := os.Stat(cfg.Ios.KeyPath); os.IsNotExist(err) {\n\t\t\treturn errors.New(\"certificate file does not exist\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// checkAndroidConf validates Android/FCM configuration.\nfunc checkAndroidConf(cfg *config.ConfYaml) error {\n\tif !cfg.Android.Enabled {\n\t\treturn nil\n\t}\n\tcredential := os.Getenv(\"GOOGLE_APPLICATION_CREDENTIALS\")\n\tif cfg.Android.Credential == \"\" && cfg.Android.KeyPath == \"\" && credential == \"\" {\n\t\treturn errors.New(\"missing fcm credential data\")\n\t}\n\treturn nil\n}\n\n// checkHuaweiConf validates Huawei/HMS configuration.\nfunc checkHuaweiConf(cfg *config.ConfYaml) error {\n\tif !cfg.Huawei.Enabled {\n\t\treturn nil\n\t}\n\tif cfg.Huawei.AppSecret == \"\" {\n\t\treturn errors.New(\"missing huawei app secret\")\n\t}\n\tif cfg.Huawei.AppID == \"\" {\n\t\treturn errors.New(\"missing huawei app id\")\n\t}\n\treturn nil\n}\n\n// CheckPushConf provide check your yml config.\nfunc CheckPushConf(cfg *config.ConfYaml) error {\n\tif !cfg.Ios.Enabled && !cfg.Android.Enabled && !cfg.Huawei.Enabled {\n\t\treturn errors.New(\"please enable iOS, Android or Huawei config in yml config\")\n\t}\n\n\tif err := checkIOSConf(cfg); err != nil {\n\t\treturn err\n\t}\n\tif err := checkAndroidConf(cfg); err != nil {\n\t\treturn err\n\t}\n\treturn checkHuaweiConf(cfg)\n}\n\n// SendNotification provide send notification.\nfunc SendNotification(\n\tctx context.Context,\n\treq qcore.TaskMessage,\n\tcfg *config.ConfYaml,\n) (resp *ResponsePush, err error) {\n\tv, ok := req.(*PushNotification)\n\tif !ok {\n\t\tif err = json.Unmarshal(req.Payload(), &v); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tswitch v.Platform {\n\tcase core.PlatFormIos:\n\t\tresp, err = PushToIOS(ctx, v, cfg)\n\tcase core.PlatFormAndroid:\n\t\tresp, err = PushToAndroid(ctx, v, cfg)\n\tcase core.PlatFormHuawei:\n\t\tresp, err = PushToHuawei(ctx, v, cfg)\n\t}\n\n\tif cfg.Core.FeedbackURL != \"\" {\n\t\tvar logs []logx.LogPushEntry\n\n\t\tif resp != nil {\n\t\t\tlogs = resp.Logs\n\t\t} else {\n\t\t\tlogs = makeErrorLogs(cfg, v, err)\n\t\t}\n\n\t\tfor _, l := range logs {\n\t\t\terr := DispatchFeedback(\n\t\t\t\tctx,\n\t\t\t\tl,\n\t\t\t\tcfg.Core.FeedbackURL,\n\t\t\t\tcfg.Core.FeedbackTimeout,\n\t\t\t\tcfg.Core.FeedbackHeader,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tlogx.LogError.Error(err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn resp, err\n}\n\n// makeErrorLogs creates a list of LogPushEntries for each token in notification\n// in case when logs are not returned from PushToXYZ() and error err is not nil\nfunc makeErrorLogs(\n\tcfg *config.ConfYaml,\n\tnotification *PushNotification,\n\terr error,\n) []logx.LogPushEntry {\n\tif err == nil {\n\t\treturn []logx.LogPushEntry{}\n\t}\n\n\tlogs := make([]logx.LogPushEntry, 0, len(notification.Tokens))\n\n\tfor _, token := range notification.Tokens {\n\t\tlog := logPush(cfg, core.FailedPush, token, notification, err)\n\n\t\tlogs = append(logs, log)\n\t}\n\n\treturn logs\n}\n\n// Run send notification\nvar Run = func(cfg *config.ConfYaml) func(ctx context.Context, msg qcore.TaskMessage) error {\n\treturn func(ctx context.Context, msg qcore.TaskMessage) error {\n\t\t_, err := SendNotification(ctx, msg, cfg)\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "notify/notification_apns.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"crypto/ecdsa\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/status\"\n\n\t\"github.com/mitchellh/mapstructure\"\n\t\"github.com/sideshow/apns2\"\n\t\"github.com/sideshow/apns2/certificate\"\n\t\"github.com/sideshow/apns2/payload\"\n\t\"github.com/sideshow/apns2/token\"\n\t\"golang.org/x/net/http2\"\n)\n\nvar (\n\tidleConnTimeout = 90 * time.Second\n\ttlsDialTimeout  = 20 * time.Second\n\ttcpKeepAlive    = 60 * time.Second\n)\n\nconst (\n\tdotP8  = \".p8\"\n\tdotPEM = \".pem\"\n\tdotP12 = \".p12\"\n)\n\nvar doOnce sync.Once\n\n// DialTLS is the default dial function for creating TLS connections for\n// non-proxied HTTPS requests.\nvar DialTLS = func(cfg *tls.Config) func(network, addr string) (net.Conn, error) {\n\treturn func(network, addr string) (net.Conn, error) {\n\t\tdialer := &net.Dialer{\n\t\t\tTimeout:   tlsDialTimeout,\n\t\t\tKeepAlive: tcpKeepAlive,\n\t\t}\n\t\treturn tls.DialWithDialer(dialer, network, addr, cfg)\n\t}\n}\n\n// Sound sets the aps sound on the payload.\ntype Sound struct {\n\tCritical int     `json:\"critical,omitempty\"`\n\tName     string  `json:\"name,omitempty\"`\n\tVolume   float32 `json:\"volume,omitempty\"`\n}\n\n// loadCertFromFile loads certificate or auth key from a file path.\nfunc loadCertFromFile(\n\tkeyPath, password string,\n) (tls.Certificate, *ecdsa.PrivateKey, string, error) {\n\tvar cert tls.Certificate\n\tvar authKey *ecdsa.PrivateKey\n\tvar err error\n\n\text := filepath.Ext(keyPath)\n\tswitch ext {\n\tcase dotP12:\n\t\tcert, err = certificate.FromP12File(keyPath, password)\n\tcase dotPEM:\n\t\tcert, err = certificate.FromPemFile(keyPath, password)\n\tcase dotP8:\n\t\tauthKey, err = token.AuthKeyFromFile(keyPath)\n\tdefault:\n\t\terr = errors.New(\"wrong certificate key extension\")\n\t}\n\n\treturn cert, authKey, ext, err\n}\n\n// loadCertFromBase64 loads certificate or auth key from base64 encoded data.\nfunc loadCertFromBase64(\n\tkeyBase64, keyType, password string,\n) (tls.Certificate, *ecdsa.PrivateKey, string, error) {\n\tvar cert tls.Certificate\n\tvar authKey *ecdsa.PrivateKey\n\n\text := \".\" + keyType\n\tkey, err := base64.StdEncoding.DecodeString(keyBase64)\n\tif err != nil {\n\t\treturn cert, nil, ext, err\n\t}\n\n\tswitch ext {\n\tcase dotP12:\n\t\tcert, err = certificate.FromP12Bytes(key, password)\n\tcase dotPEM:\n\t\tcert, err = certificate.FromPemBytes(key, password)\n\tcase dotP8:\n\t\tauthKey, err = token.AuthKeyFromBytes(key)\n\tdefault:\n\t\terr = errors.New(\"wrong certificate key type\")\n\t}\n\n\treturn cert, authKey, ext, err\n}\n\n// createAPNSClientWithToken creates an APNS client using a p8 token.\nfunc createAPNSClientWithToken(\n\tcfg *config.ConfYaml,\n\tauthKey *ecdsa.PrivateKey,\n) (*apns2.Client, error) {\n\tif cfg.Ios.KeyID == \"\" || cfg.Ios.TeamID == \"\" {\n\t\treturn nil, errors.New(\"you should provide ios.KeyID and ios.TeamID for p8 token\")\n\t}\n\tt := &token.Token{\n\t\tAuthKey: authKey,\n\t\tKeyID:   cfg.Ios.KeyID,\n\t\tTeamID:  cfg.Ios.TeamID,\n\t}\n\treturn newApnsTokenClient(cfg, t)\n}\n\n// InitAPNSClient use for initialize APNs Client.\nfunc InitAPNSClient(ctx context.Context, cfg *config.ConfYaml) error {\n\tif !cfg.Ios.Enabled {\n\t\treturn nil\n\t}\n\n\tvar (\n\t\terr            error\n\t\tauthKey        *ecdsa.PrivateKey\n\t\tcertificateKey tls.Certificate\n\t\text            string\n\t)\n\n\tswitch {\n\tcase cfg.Ios.KeyPath != \"\":\n\t\tcertificateKey, authKey, ext, err = loadCertFromFile(cfg.Ios.KeyPath, cfg.Ios.Password)\n\tcase cfg.Ios.KeyBase64 != \"\":\n\t\tcertificateKey, authKey, ext, err = loadCertFromBase64(\n\t\t\tcfg.Ios.KeyBase64,\n\t\t\tcfg.Ios.KeyType,\n\t\t\tcfg.Ios.Password,\n\t\t)\n\t}\n\n\tif err != nil {\n\t\tlogx.LogError.Error(\"Cert Error:\", err.Error())\n\t\treturn err\n\t}\n\n\tif ext == dotP8 {\n\t\tApnsClient, err = createAPNSClientWithToken(cfg, authKey)\n\t} else {\n\t\tApnsClient, err = newApnsClient(cfg, certificateKey)\n\t}\n\n\tif err != nil {\n\t\tlogx.LogError.Error(\"Transport Error:\", err.Error())\n\t\treturn err\n\t}\n\n\tif h2Transport, ok := ApnsClient.HTTPClient.Transport.(*http2.Transport); ok {\n\t\tconfigureHTTP2ConnHealthCheck(h2Transport)\n\t}\n\n\tdoOnce.Do(func() {\n\t\tMaxConcurrentIOSPushes = make(chan struct{}, cfg.Ios.MaxConcurrentPushes)\n\t})\n\n\treturn nil\n}\n\nfunc newApnsClient(cfg *config.ConfYaml, certificate tls.Certificate) (*apns2.Client, error) {\n\tvar client *apns2.Client\n\n\tif cfg.Ios.Production {\n\t\tclient = apns2.NewClient(certificate).Production()\n\t} else {\n\t\tclient = apns2.NewClient(certificate).Development()\n\t}\n\n\tif cfg.Core.HTTPProxy == \"\" {\n\t\treturn client, nil\n\t}\n\n\t//nolint:gosec // TLS min version is managed by the APNS library and proxy transport\n\ttlsConfig := &tls.Config{\n\t\tCertificates: []tls.Certificate{certificate},\n\t}\n\n\tif len(certificate.Certificate) > 0 {\n\t\t//nolint:staticcheck // deprecated but required for APNS proxy certificate mapping\n\t\ttlsConfig.BuildNameToCertificate()\n\t}\n\n\ttransport := &http.Transport{\n\t\tTLSClientConfig: tlsConfig,\n\t\tDialTLS:         DialTLS(tlsConfig),\n\t\tProxy:           http.DefaultTransport.(*http.Transport).Proxy,\n\t\tIdleConnTimeout: idleConnTimeout,\n\t}\n\n\th2Transport, err := http2.ConfigureTransports(transport)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconfigureHTTP2ConnHealthCheck(h2Transport)\n\n\tclient.HTTPClient.Transport = transport\n\n\treturn client, nil\n}\n\nfunc newApnsTokenClient(cfg *config.ConfYaml, token *token.Token) (*apns2.Client, error) {\n\tvar client *apns2.Client\n\n\tif cfg.Ios.Production {\n\t\tclient = apns2.NewTokenClient(token).Production()\n\t} else {\n\t\tclient = apns2.NewTokenClient(token).Development()\n\t}\n\n\tif cfg.Core.HTTPProxy == \"\" {\n\t\treturn client, nil\n\t}\n\n\ttransport := &http.Transport{\n\t\tDialTLS:         DialTLS(nil),\n\t\tProxy:           http.DefaultTransport.(*http.Transport).Proxy,\n\t\tIdleConnTimeout: idleConnTimeout,\n\t}\n\n\th2Transport, err := http2.ConfigureTransports(transport)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconfigureHTTP2ConnHealthCheck(h2Transport)\n\n\tclient.HTTPClient.Transport = transport\n\n\treturn client, nil\n}\n\nfunc configureHTTP2ConnHealthCheck(h2Transport *http2.Transport) {\n\th2Transport.ReadIdleTimeout = 1 * time.Second\n\th2Transport.PingTimeout = 1 * time.Second\n}\n\n// setAlertTitleAndBody sets the title and body fields on the notification payload.\nfunc setAlertTitleAndBody(p *payload.Payload, req *PushNotification) {\n\tif len(req.Title) > 0 {\n\t\tp.AlertTitle(req.Title)\n\t}\n\tif len(req.Message) > 0 && len(req.Title) > 0 {\n\t\tp.AlertBody(req.Message)\n\t}\n\tif len(req.Alert.Title) > 0 {\n\t\tp.AlertTitle(req.Alert.Title)\n\t}\n\tif len(req.Alert.Body) > 0 {\n\t\tp.AlertBody(req.Alert.Body)\n\t}\n\t// Apple Watch & Safari display this string as part of the notification interface.\n\tif len(req.Alert.Subtitle) > 0 {\n\t\tp.AlertSubtitle(req.Alert.Subtitle)\n\t}\n}\n\n// setAlertLocalization sets localization-related fields on the notification payload.\nfunc setAlertLocalization(p *payload.Payload, req *PushNotification) {\n\tif len(req.Alert.TitleLocKey) > 0 {\n\t\tp.AlertTitleLocKey(req.Alert.TitleLocKey)\n\t}\n\tif len(req.Alert.TitleLocArgs) > 0 {\n\t\tp.AlertTitleLocArgs(req.Alert.TitleLocArgs)\n\t}\n\tif len(req.Alert.LocKey) > 0 {\n\t\tp.AlertLocKey(req.Alert.LocKey)\n\t}\n\tif len(req.Alert.LocArgs) > 0 {\n\t\tp.AlertLocArgs(req.Alert.LocArgs)\n\t}\n\tif len(req.Alert.ActionLocKey) > 0 {\n\t\tp.AlertActionLocKey(req.Alert.ActionLocKey)\n\t}\n}\n\n// setAlertActions sets action and launch image fields on the notification payload.\nfunc setAlertActions(p *payload.Payload, req *PushNotification) {\n\tif len(req.Alert.LaunchImage) > 0 {\n\t\tp.AlertLaunchImage(req.Alert.LaunchImage)\n\t}\n\tif len(req.Alert.Action) > 0 {\n\t\tp.AlertAction(req.Alert.Action)\n\t}\n\tif len(req.Alert.SummaryArg) > 0 {\n\t\tp.AlertSummaryArg(req.Alert.SummaryArg)\n\t}\n\tif req.Alert.SummaryArgCount > 0 {\n\t\tp.AlertSummaryArgCount(req.Alert.SummaryArgCount)\n\t}\n}\n\n// setLiveActivityFields sets Live Activity related fields on the notification payload.\nfunc setLiveActivityFields(p *payload.Payload, req *PushNotification) {\n\tif len(req.ContentState) > 0 {\n\t\tp.SetContentState(req.ContentState)\n\t}\n\tif req.StaleDate > 0 {\n\t\tp.SetStaleDate(req.StaleDate)\n\t}\n\tif req.DismissalDate > 0 {\n\t\tp.SetDismissalDate(req.DismissalDate)\n\t}\n\tif len(req.Event) > 0 {\n\t\tp.SetEvent(payload.ELiveActivityEvent(req.Event))\n\t}\n\tif req.Timestamp > 0 {\n\t\tp.SetTimestamp(req.Timestamp)\n\t}\n}\n\nfunc iosAlertDictionary(\n\tnotificationPayload *payload.Payload,\n\treq *PushNotification,\n) *payload.Payload {\n\tif len(req.InterruptionLevel) > 0 {\n\t\tnotificationPayload.InterruptionLevel(payload.EInterruptionLevel(req.InterruptionLevel))\n\t}\n\tif len(req.Category) > 0 {\n\t\tnotificationPayload.Category(req.Category)\n\t}\n\n\tsetAlertTitleAndBody(notificationPayload, req)\n\tsetAlertLocalization(notificationPayload, req)\n\tsetAlertActions(notificationPayload, req)\n\tsetLiveActivityFields(notificationPayload, req)\n\n\treturn notificationPayload\n}\n\n// setNotificationPriority sets the priority on the notification based on the request.\nfunc setNotificationPriority(notification *apns2.Notification, priority string) {\n\tif len(priority) == 0 {\n\t\treturn\n\t}\n\tswitch priority {\n\tcase \"normal\":\n\t\tnotification.Priority = apns2.PriorityLow\n\tcase \"high\":\n\t\tnotification.Priority = apns2.PriorityHigh\n\t}\n}\n\n// setPayloadSound sets the sound on the payload based on the request.\nfunc setPayloadSound(p *payload.Payload, req *PushNotification) {\n\tswitch req.Sound.(type) {\n\tcase map[string]any:\n\t\tresult := &Sound{}\n\t\t_ = mapstructure.Decode(req.Sound, &result)\n\t\tp.Sound(result)\n\tcase string:\n\t\tp.Sound(&req.Sound)\n\tcase Sound:\n\t\tp.Sound(&req.Sound)\n\t}\n\tif len(req.SoundName) > 0 {\n\t\tp.SoundName(req.SoundName)\n\t}\n\tif req.SoundVolume > 0 {\n\t\tp.SoundVolume(req.SoundVolume)\n\t}\n}\n\n// buildIOSPayload constructs the payload for an iOS notification.\nfunc buildIOSPayload(req *PushNotification) *payload.Payload {\n\tp := payload.NewPayload()\n\n\t// add alert object if message length > 0 and title is empty\n\tif len(req.Message) > 0 && req.Title == \"\" {\n\t\tp.Alert(req.Message)\n\t}\n\n\t// zero value for clear the badge on the app icon.\n\tif req.Badge != nil && *req.Badge >= 0 {\n\t\tp.Badge(*req.Badge)\n\t}\n\n\tif req.MutableContent {\n\t\tp.MutableContent()\n\t}\n\n\tsetPayloadSound(p, req)\n\n\tif req.ContentAvailable {\n\t\tp.ContentAvailable()\n\t}\n\n\tif len(req.URLArgs) > 0 {\n\t\tp.URLArgs(req.URLArgs)\n\t}\n\n\tif len(req.ThreadID) > 0 {\n\t\tp.ThreadID(req.ThreadID)\n\t}\n\n\tfor k, v := range req.Data {\n\t\tp.Custom(k, v)\n\t}\n\n\treturn iosAlertDictionary(p, req)\n}\n\n// GetIOSNotification use for define iOS notification.\n// The iOS Notification Payload (Payload Key Reference)\n// Ref: https://apple.co/2VtH6Iu\nfunc GetIOSNotification(req *PushNotification) *apns2.Notification {\n\tnotification := &apns2.Notification{\n\t\tApnsID:     req.ApnsID,\n\t\tTopic:      req.Topic,\n\t\tCollapseID: req.CollapseID,\n\t}\n\n\tif req.Expiration != nil {\n\t\tnotification.Expiration = time.Unix(*req.Expiration, 0)\n\t}\n\n\tsetNotificationPriority(notification, req.Priority)\n\n\tif len(req.PushType) > 0 {\n\t\tnotification.PushType = apns2.EPushType(req.PushType)\n\t}\n\n\tnotification.Payload = buildIOSPayload(req)\n\n\treturn notification\n}\n\nfunc getApnsClient(cfg *config.ConfYaml, req *PushNotification) (client *apns2.Client) {\n\tswitch {\n\tcase req.Production:\n\t\tclient = ApnsClient.Production()\n\tcase req.Development:\n\t\tclient = ApnsClient.Development()\n\tdefault:\n\t\tif cfg.Ios.Production {\n\t\t\tclient = ApnsClient.Production()\n\t\t} else {\n\t\t\tclient = ApnsClient.Development()\n\t\t}\n\t}\n\treturn client\n}\n\n// PushToIOS provide send notification to APNs server.\nfunc PushToIOS(\n\tctx context.Context,\n\treq *PushNotification,\n\tcfg *config.ConfYaml,\n) (resp *ResponsePush, err error) {\n\tlogx.LogAccess.Debug(\"Start push notification for iOS\")\n\n\tvar (\n\t\tretryCount = 0\n\t\tmaxRetry   = cfg.Ios.MaxRetry\n\t)\n\n\tif req.Retry > 0 && req.Retry < maxRetry {\n\t\tmaxRetry = req.Retry\n\t}\n\n\tresp = &ResponsePush{}\n\nRetry:\n\tvar newTokens []string\n\n\tnotification := GetIOSNotification(req)\n\tclient := getApnsClient(cfg, req)\n\n\tvar wg sync.WaitGroup\n\tfor _, token := range req.Tokens {\n\t\t// occupy push slot\n\t\tMaxConcurrentIOSPushes <- struct{}{}\n\t\twg.Add(1)\n\t\tgo func(notification apns2.Notification, token string) {\n\t\t\tnotification.DeviceToken = token\n\n\t\t\t// send ios notification\n\t\t\tres, err := client.PushWithContext(ctx, &notification)\n\t\t\tif err != nil || (res != nil && res.StatusCode != http.StatusOK) {\n\t\t\t\tif err == nil {\n\t\t\t\t\t// error message:\n\t\t\t\t\t// ref: https://github.com/sideshow/apns2/blob/master/response.go#L14-L65\n\t\t\t\t\terr = errors.New(res.Reason)\n\t\t\t\t}\n\n\t\t\t\t// apns server error\n\t\t\t\terrLog := logPush(cfg, core.FailedPush, token, req, err)\n\t\t\t\tresp.Logs = append(resp.Logs, errLog)\n\n\t\t\t\tstatus.StatStorage.AddIosError(1)\n\t\t\t\t// We should retry only \"retryable\" statuses. More info about response:\n\t\t\t\t// See https://apple.co/3AdNane (Handling Notification Responses from APNs)\n\t\t\t\tif res != nil && res.StatusCode >= http.StatusInternalServerError {\n\t\t\t\t\tnewTokens = append(newTokens, token)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif res != nil && res.Sent() {\n\t\t\t\tlogPush(cfg, core.SucceededPush, token, req, nil)\n\t\t\t\tstatus.StatStorage.AddIosSuccess(1)\n\t\t\t}\n\n\t\t\t// free push slot\n\t\t\t<-MaxConcurrentIOSPushes\n\t\t\twg.Done()\n\t\t}(*notification, token)\n\t}\n\n\twg.Wait()\n\n\tif len(newTokens) > 0 && retryCount < maxRetry {\n\t\tretryCount++\n\n\t\t// resend fail token\n\t\treq.Tokens = newTokens\n\t\tgoto Retry\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "notify/notification_apns_test.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/status\"\n\n\t\"github.com/buger/jsonparser\"\n\t\"github.com/sideshow/apns2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\t//nolint:gosec // test certificate data, not a real secret\n\tcertificateValidP12 = `MIIKlgIBAzCCClwGCSqGSIb3DQEHAaCCCk0EggpJMIIKRTCCBMcGCSqGSIb3DQEHBqCCBLgwggS0AgEAMIIErQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQID/GJtcRhjvwCAggAgIIEgE5ralQoQBDgHgdp5+EwBaMjcZEJUXmYRdVCttIwfN2OxlIs54tob3/wpUyWGqJ+UXy9X+4EsWpDPUfTN/w88GMgj0kftpTqG0+3Hu/9pkZO4pdLCiyMGOJnXCOdhHFirtTXAR3QvnKKIpXIKrmZ4rcr/24Uvd/u669Tz8VDgcGOQazKeyvtdW7TJBxMFRv+IsQi/qCj5PkQ0jBbZ1LAc4C8mCMwOcH+gi/e471mzPWihQmynH2yJlZ4jb+taxQ/b8Dhlni2vcIMn+HknRk3Cyo8jfFvvO0BjvVvEAPxPJt7X96VFFS2KlyXjY3zt0siGrzQpczgPB/1vTqhQUvoOBw6kcXWgOjwt+gR8Mmo2DELnQqGhbYuWu52doLgVvD+zGr5vLYXHz6gAXnI6FVyHb+oABeBet3cer3EzGR7r+VoLmWSBm8SyRHwi0mxE63S7oD1j22jaTo7jnQBFZaY+cPaATcFjqW67x4j8kXh9NRPoINSgodLJrgmet2D1iOKuLTkCWf0UTi2HUkn9Zf0y+IIViZaVE4mWaGb9xTBClfa4KwM5gSz3jybksFKbtnzzPFuzClu+2mdthJs/58Ao40eyaykNmzSPhDv1F8Mai8bfaAqSdcBl5ZB2PF33xhuNSS4j2uIh1ICGv9DueyN507iEMQO2yCcaQTMKejV7/52h9LReS5/QPXDJhWMVpTb5FGCP7EmO0lZTeBNO5MlDzDQfz5xcFqHqfoby2sfAMU8HNB8wzdcwHtacgKGLBjLkapxyTsqYE5Kry6UxclvF4soR8TZoQ69E7WsKZLmTaw2+msmnDJubpY0NqkRqkVk7umtVC0D+w6AIKDrY58HMlm80/ImgGXwybA1kuZMxqMzaH/xFiAHOSIGuVPtGgGFYNEdGbfOryuhFo9l1nSECWm8MN9hYwB1Rn9p6rkd+zrvbU1zv13drtrZ/vL0NlT02tlkS8NdWLGJkZhWgc2c89GyRb7mjuHRHu/BWGED3y7vjHo/lnkPsLJXw0ovIlqhtW0BtN/xSpGg0phDbn0Et5jb7Xmc+fWimgbtIUHcnJOV5QSYFzlR+kbzx0oKRARU4B3CWkdPeaXkrmw0IriS6vOdZcM8YBJ6BtXEDLsrSH7tHxeknYHLEl0uy9Oc1+Huyrz8j7Zxo8SQj9H+RX0HeMl8YB3HUBLHYcqCEBjm7mHI4rP8ULVkC5oCA5w3tJfMyvS/jZRiwMUyr0tiWhrh/AM3wPPX54cqozefojWKrqGtK9I+n0cfwW9rU3FsUcpMTo9uQ27O7NejKP2X/LLMZkQvWUEabZNjNrWsbp6d51/frfIR7kRlZAmmt2yS23h6w6RvKTAVUrNatEyzokfNAIDml6lYLweNJATZU08BznhPpuvh3bKOSos5uaJBYpsOYexoMGnAig428qypw0cmv6sCjO/xdIL86COVNQp/UtjcXJ9/E0bnVmzfpgA3WCy+29YXPx7DZ1U+bQ9jOO/P9pwqLwTH+gpcZiVm3ru1Tmiq6iZ8cG7tMLfTBNXljvtlDzCCBXYGCSqGSIb3DQEHAaCCBWcEggVjMIIFXzCCBVsGCyqGSIb3DQEMCgECoIIE7jCCBOowHAYKKoZIhvcNAQwBAzAOBAgCvAo2HCM89AICCAAEggTIOcfaF6qWYXlo+BNBjYIllg0VwQSJXZmcqj2vXlDPIPrTuQ+QDmGnhYR6hVbcMrk3o7eQhH3ThyHM+KEzkYx1IAYCOdEQXYcFguoDG1CxHrgE1Y0H8yndc/yPw2tqkx6X9ZemdYp3welXZjYgUi9MKvGbN6lZ0cFTU+2+0+H/IyKQ3OUjDNymhOxypOPBaK2eQsJ7XumgJ6nLvNZDRx/f277J+LD/z0pOhzUOljhvA3dkBMpEvomX4erZihErunqP1jbH9O3eIYq9J7czGS2xuckolW19KqWOyWh8KRI/LnAqiEh2e0hZ7lpltj79PenO66VGPbn2f85A6b6PD4kipgoMB2IRibkoodyn/oo3WizO386fqtEfUlbFmxI4y4utobWe7nZ2VuBLgA/mgyyxqAJK1erM98NDWB/Njo1CPsaMl9ubXKPOyIZG0fOLUa23DfkJUEiCb839yKc2oEJkI0wtrvbeh1TAPv4vL4TxiXdiJ/6YrSa0/FQh6nqk1jiK+p22MzvEIkDOyPqk/GsAlc/k2kQ/M86tF50wtc08wnXv8+G8k6qTZ7VCluffzAUt64La47qj8XIfh7tKleznzQSbyjlNX8DsFVzGbCg9G4PKxrLAVnKEgIK1kOopSF1UUMqSKE0D3s5AURQhX8/Cf9h+WtNsWK+y7EMOntsBc2op0M7fQ9Jm73NF7CCYeqb0W7sziJSzqJsJgNp0+ArAcZQExeltxAb6kye3Z5JtP/oaB+jmcHKy9l/nhzKA3MzJwCZ5Q3oviPlNqJvFVBmGEEvC6iULLuv6VSxNdB2uH3Tsfa1TMOOHOadBTcyWatjscYS9ynkXuw1+8+FvEu3EV0UwopZmlSaYfMKQ2jshT4Cgg1zy15uKjomojtAaaF+D/U6KZVQk/7rzdaDmvkJvNtc5n9BW96tmrOhI6L+/WihS570qaitQUsHBBTOetlHXYEPiOkH8BhjzNHXLH9YpC8OEQOhO+1jEninDKNdbU7SCqV0+YE6kfR5Bfkw2MxoIQLtUnHjK6GR/q3fxo1TirbTe8c8dp907wgcXkT/rONX/iG1JTjxV2ixR1oM68LYI3eJzY801/xBSnmOjdzOPUHXCNHDTf9kPjkOtZWkGbZugf4ckRH/L8dK2Vo4QpFUN8AZjomanzLxjQZ+DVFNoPDT2K+0pezsMiwSJlyBGoIQHN0/2zVNVLo/KfARIOac1iC8+duj5S/1c52+PvP7FkMe72QUV0KUQ7AJHXUvQtFZx4Ny579/B/3c4D72CFSydhw3/+nL9+Nz956UafZ6G7HZ96frMTgajMcXQe1uXwgN2iTnnNtLdcC/ARHS1RkjgXHohO+VGuQxOo23PPABVaxex2SGGXX7Fc4MI2Xr4uaimZIzcUkuHUnhZQGkcFlVekZ/wJXookq0Fv8DuPuv7mGCx6BKERU9I+NMU6xLNe6VsfkS8t5uVq1EIINnddGl9VGpqOPN8EgU47gh6CcDkP8sxXsT8pZ1vQyJrUlWGYp68/okoQ+7lqnd06wzVDIwAE/+pq9PUxLdNvYE0sNe4JrEcKO0xp/zxCqLjHLT+rB896v2OsU0BA5tPQA7xkKp4PuQr6qO8fTVyfhImVmoFX6b9VgtLHIlJMVowIwYJKoZIhvcNAQkVMRYEFIwanwBmvSRCuV0e6/5ei8oEPXODMDMGCSqGSIb3DQEJFDEmHiQAQQBQAE4AUwAvADIAIABQAHIAaQB2AGEAdABlACAASwBlAHkwMTAhMAkGBSsOAwIaBQAEFK7XWCbKGSKmxNqE2E8dmCfwhaQxBAjPcbkv12ro6gICCAA=`\n\t//nolint:gosec // test certificate data, not a real secret\n\tcertificateValidPEM = `QmFnIEF0dHJpYnV0ZXMKICAgIGxvY2FsS2V5SUQ6IDhDIDFBIDlGIDAwIDY2IEJEIDI0IDQyIEI5IDVEIDFFIEVCIEZFIDVFIDhCIENBIDA0IDNEIDczIDgzIAogICAgZnJpZW5kbHlOYW1lOiBBUE5TLzIgUHJpdmF0ZSBLZXkKc3ViamVjdD0vQz1OWi9TVD1XZWxsaW5ndG9uL0w9V2VsbGluZ3Rvbi9PPUludGVybmV0IFdpZGdpdHMgUHR5IEx0ZC9PVT05WkVINjJLUlZWL0NOPUFQTlMvMiBEZXZlbG9wbWVudCBJT1MgUHVzaCBTZXJ2aWNlczogY29tLnNpZGVzaG93LkFwbnMyCmlzc3Vlcj0vQz1OWi9TVD1XZWxsaW5ndG9uL0w9V2VsbGluZ3Rvbi9PPUFQTlMvMiBJbmMuL09VPUFQTlMvMiBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucy9DTj1BUE5TLzIgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ2ekNDQXRNQ0FRSXdEUVlKS29aSWh2Y05BUUVMQlFBd2djTXhDekFKQmdOVkJBWVRBazVhTVJNd0VRWUQKVlFRSUV3cFhaV3hzYVc1bmRHOXVNUk13RVFZRFZRUUhFd3BYWld4c2FXNW5kRzl1TVJRd0VnWURWUVFLRXd0QgpVRTVUTHpJZ1NXNWpMakV0TUNzR0ExVUVDeE1rUVZCT1V5OHlJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnClVtVnNZWFJwYjI1ek1VVXdRd1lEVlFRREV6eEJVRTVUTHpJZ1YyOXliR1IzYVdSbElFUmxkbVZzYjNCbGNpQlMKWld4aGRHbHZibk1nUTJWeWRHbG1hV05oZEdsdmJpQkJkWFJvYjNKcGRIa3dIaGNOTVRZd01UQTRNRGd6TkRNdwpXaGNOTWpZd01UQTFNRGd6TkRNd1dqQ0JzakVMTUFrR0ExVUVCaE1DVGxveEV6QVJCZ05WQkFnVENsZGxiR3hwCmJtZDBiMjR4RXpBUkJnTlZCQWNUQ2xkbGJHeHBibWQwYjI0eElUQWZCZ05WQkFvVEdFbHVkR1Z5Ym1WMElGZHAKWkdkcGRITWdVSFI1SUV4MFpERVRNQkVHQTFVRUN4TUtPVnBGU0RZeVMxSldWakZCTUQ4R0ExVUVBeE00UVZCTwpVeTh5SUVSbGRtVnNiM0J0Wlc1MElFbFBVeUJRZFhOb0lGTmxjblpwWTJWek9pQmpiMjB1YzJsa1pYTm9iM2N1ClFYQnVjekl3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRFkwYzFUS0I1b1pQd1EKN3QxQ3dNSXJ2cUI2R0lVM3RQeTZSaGNrWlhUa09COFllQldKN1VLZkN6OEhHSEZWb21CUDBUNU9VYmVxUXpxVwpZSmJRelo4YTZaTXN6YkwwbE80WDkrKzNPaTUvVHRBd09VT0s4ck9GTjI1bTJLZnNheUhRWi80dldTdEsyRndtCjVhSmJHTGxwSC9iLzd6MUQ0dmhtTWdvQnVUMUl1eWhHaXlGeGxaOUV0VGxvRnZzcU0xRTVmWVpPU1pBQ3lYVGEKSzR2ZGdiUU1nVVZzSTcxNEZBZ0xUbEswVWVpUmttS20zcGRidGZWYnJ0aHpJK0lIWEtJdFVJeStGbjIwUFJNaApkU25henRTejd0Z0JXQ0l4MjJxdmNZb2dIV2lPZ1VZSU03NzJ6RTJ5OFVWT3I4RHNpUmxzT0hTQTdFSTRNSmNRCkcyRlVxMlovQWdNQkFBRXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBR3lmeU8ySE1nY2RlQmN6M2J0NUJJTFgKZjdSQTIvVW1WSXdjS1IxcW90VHNGK1BuQm1jSUxleU9RZ0RlOXRHVTVjUmM3OWtEdDNKUm1NWVJPRklNZ0ZSZgpXZjIydU9LdGhvN0dRUWFLdkcrYmtnTVZkWUZSbEJIbkYrS2VxS0g4MXFiOXArQ1Q0SXcwR2VoSUwxRGlqRkxSClZJQUlCWXB6NG9CUENJRTFJU1ZUK0ZnYWYzSkFoNTlrYlBiTnc5QUlEeGFCdFA4RXV6U1ROd2ZieG9HYkNvYlMKV2kxVThJc0N3UUZ0OHRNMW00WlhEMUNjWklyR2RyeWVBaFZrdktJSlJpVTVRWVdJMm5xWk4rSnFRdWNtOWFkMAptWU81bUprSW9iVWE0K1pKaENQS0VkbWdwRmJSR2swd1Z1YURNOUN2NlAyc3JzWUFqYU80eTNWUDBHdk5LUkk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KQmFnIEF0dHJpYnV0ZXMKICAgIGxvY2FsS2V5SUQ6IDhDIDFBIDlGIDAwIDY2IEJEIDI0IDQyIEI5IDVEIDFFIEVCIEZFIDVFIDhCIENBIDA0IDNEIDczIDgzIAogICAgZnJpZW5kbHlOYW1lOiBBUE5TLzIgUHJpdmF0ZSBLZXkKS2V5IEF0dHJpYnV0ZXM6IDxObyBBdHRyaWJ1dGVzPgotLS0tLUJFR0lOIFJTQSBQUklWQVRFIEtFWS0tLS0tCk1JSUVvd0lCQUFLQ0FRRUEyTkhOVXlnZWFHVDhFTzdkUXNEQ0s3NmdlaGlGTjdUOHVrWVhKR1YwNURnZkdIZ1YKaWUxQ253cy9CeGh4VmFKZ1Q5RStUbEczcWtNNmxtQ1cwTTJmR3VtVExNMnk5SlR1Ri9mdnR6b3VmMDdRTURsRAppdkt6aFRkdVp0aW43R3NoMEdmK0wxa3JTdGhjSnVXaVd4aTVhUi8yLys4OVErTDRaaklLQWJrOVNMc29Sb3NoCmNaV2ZSTFU1YUJiN0tqTlJPWDJHVGttUUFzbDAyaXVMM1lHMERJRkZiQ085ZUJRSUMwNVN0Rkhva1pKaXB0NlgKVzdYMVc2N1ljeVBpQjF5aUxWQ012aFo5dEQwVElYVXAyczdVcys3WUFWZ2lNZHRxcjNHS0lCMW9qb0ZHQ0RPKwo5c3hOc3ZGRlRxL0E3SWtaYkRoMGdPeENPRENYRUJ0aFZLdG1md0lEQVFBQkFvSUJBUUNXOFpDSStPQWFlMXRFCmlwWjlGMmJXUDNMSExYVG84RllWZENBK1ZXZUlUazNQb2lJVWtKbVYwYVdDVWhEc3RndG81ZG9EZWo1c0NUdXIKWHZqL3luYWVyTWVxSkZZV2tld2p3WmNnTHlBWnZ3dU8xdjdmcDlFMHgvOVRHRGZuampuUE5lYXVuZHhXMGNOdAp6T1kzbDBIVkhzeTlKcGUzUURjQUpvdnk0VHY1K2hGWTRrRHhVQkdzeWp2aFNjVmdLZzV0TGtKY2xtM3NPdS9MCkd5THFwd05JM09KQWRNSXVWRDROMkJaMWFPRWFwNm1wMnk4SWUwL1I0WVdjYVo1QTRQdzd4VVBsNlNYYzl1dWEKLzc4UVRFUnRQQzZlanlDQmlFMDVhOG0zUTNpdWQzWHRubHl3czJLd2hnQkFmRTZNNHpSL2YzT1FCN1pJWE1oeQpacG1aWnc1eEFvR0JBUFluODRJcmxJUWV0V1FmdlBkTTdLemdoNlVESEN1Z25sQ0RnaHdZcFJKR2k4aE1mdVpWCnhOSXJZQUp6TFlEUTAxbEZKUkpnV1hUY2JxejlOQnoxbmhnK2NOT3oxL0tZKzM4ZXVkZWU2RE5ZbXp0UDdqRFAKMmpuYVMrZHRqQzhoQVhPYm5GcUcrTmlsTURMTHU2YVJtckphSW1ialNyZnlMaUU2bXZKN3U4MW5Bb0dCQU9GOQpnOTN3WjBtTDFyazJzNVd3SEdUTlUvSGFPdG1XUzR6N2tBN2Y0UWFSdWIrTXdwcFptbURaUEhwaVpYN0JQY1p6CmlPUFFoK3huN0lxUkdvUVdCTHlrQlZ0OHpaRm9MWkpvQ1IzbjYzbGV4NUE0cC8wUHAxZ0ZaclIreFg4UFlWb3MKM3llZWlXeVBLc1hYTmMwczVRd0haY1g2V2I4RUhUaFRYR0NCZXRjcEFvR0FNZVFKQzlJUGFQUGNhZTJ3M0NMQQpPWTNNa0ZwZ0JFdXFxc0RzeHdzTHNmZVFiMGxwMHYrQlErTzhzdUpyVDVlRHJxMUFCVWgzK1NLUVlBbDEzWVMrCnhVVXFrdzM1YjljbjZpenRGOUhDV0YzV0lLQmpzNHI5UFFxTXBkeGpORTRwUUNoQytXb3YxNkVyY3JBdVdXVmIKaUZpU2JtNFUvOUZiSGlzRnFxMy9jM01DZ1lCK3Z6U3VQZ0Z3MzcrMG9FRFZ0UVpneXVHU29wNU56Q052ZmIvOQovRzNhYVhORmJuTzhtdjBoenpvbGVNV2dPRExuSis0Y1VBejNIM3RnY0N1OWJ6citaaHYwenZRbDlhOFlDbzZGClZ1V1BkVzByYmcxUE84dE91TXFBVG5ubzc5WkMvOUgzelM5bDdCdVkxVjJTbE5leXFUM1Z5T0ZGYzZTUkVwcHMKVEp1bDhRS0JnQXhuUUI4TUE3elBVTHUxY2x5YUpMZHRFZFJQa0tXTjdsS1lwdGMwZS9WSGZTc0t4c2VXa2ZxaQp6Z1haNTFrUVRyVDZaYjZIWVJmd0MxbU1YSFdSS1J5WWpBbkN4VmltNllRZCtLVlQ0OWlSRERBaUlGb01HQTRpCnZ2Y0lsbmVxT1paUERJb0tKNjBJak8vRFpIV2t3NW1MamFJclQrcVEzWEFHZEpBMTNoY20KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K`\n\t//nolint:gosec // test certificate data, not a real secret\n\tauthkeyInvalidP8 = `TUlHSEFnRUFNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQkcwd2F3SUJBUVFnRWJWemZQblpQeGZBeXhxRQpaVjA1bGFBb0pBbCsvNlh0Mk80bU9CNjExc09oUkFOQ0FBU2dGVEtqd0pBQVU5NWcrKy92ektXSGt6QVZtTk1JCnRCNXZUalpPT0l3bkViNzBNc1daRkl5VUZEMVA5R3dzdHo0K2FrSFg3dkk4Qkg2aEhtQm1mWlpaCg==`\n\t//nolint:gosec // test certificate data, not a real secret\n\tauthkeyValidP8 = `LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ0ViVnpmUG5aUHhmQXl4cUUKWlYwNWxhQW9KQWwrLzZYdDJPNG1PQjYxMXNPaFJBTkNBQVNnRlRLandKQUFVOTVnKysvdnpLV0hrekFWbU5NSQp0QjV2VGpaT09Jd25FYjcwTXNXWkZJeVVGRDFQOUd3c3R6NCtha0hYN3ZJOEJINmhIbUJtZmVRbAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==`\n)\n\nvar (\n\ttestMessage    = \"test\"\n\ttestKeyPathP8  = \"../certificate/authkey-valid.p8\"\n\ttestKeyPath    = \"../certificate/certificate-valid.pem\"\n\twelcomeMessage = \"Welcome notification Server\"\n)\n\nconst (\n\ttestKeyID  = \"key-id\"\n\ttestTeamID = \"team-id\"\n)\n\nfunc TestDisabledAndroidIosConf(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tcfg.Android.Enabled = false\n\tcfg.Huawei.Enabled = false\n\n\terr := CheckPushConf(cfg)\n\n\trequire.Error(t, err)\n\tassert.Equal(t, \"please enable iOS, Android or Huawei config in yml config\", err.Error())\n}\n\nfunc TestMissingIOSCertificate(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = \"\"\n\tcfg.Ios.KeyBase64 = \"\"\n\terr := CheckPushConf(cfg)\n\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing iOS certificate key\", err.Error())\n\n\tcfg.Ios.KeyPath = \"test.pem\"\n\terr = CheckPushConf(cfg)\n\n\trequire.Error(t, err)\n\tassert.Equal(t, \"certificate file does not exist\", err.Error())\n}\n\nfunc TestIOSNotificationStructure(t *testing.T) {\n\tvar dat map[string]any\n\tunix := time.Now().Unix()\n\n\texpectBadge := 0\n\tmessage := welcomeMessage\n\texpiration := time.Now().Unix()\n\treq := &PushNotification{\n\t\tApnsID:     testMessage,\n\t\tTopic:      testMessage,\n\t\tExpiration: &expiration,\n\t\tPriority:   \"normal\",\n\t\tMessage:    message,\n\t\tBadge:      &expectBadge,\n\t\tSound: Sound{\n\t\t\tCritical: 1,\n\t\t\tName:     testMessage,\n\t\t\tVolume:   1.0,\n\t\t},\n\t\tContentAvailable: true,\n\t\tData: D{\n\t\t\t\"key1\": \"test\",\n\t\t\t\"key2\": 2,\n\t\t},\n\t\tCategory: testMessage,\n\t\tURLArgs:  []string{\"a\", \"b\"},\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tpanic(err)\n\t}\n\n\talert, _ := jsonparser.GetString(data, \"aps\", \"alert\")\n\tbadge, _ := jsonparser.GetInt(data, \"aps\", \"badge\")\n\tsoundName, _ := jsonparser.GetString(data, \"aps\", \"sound\", \"name\")\n\tsoundCritical, _ := jsonparser.GetInt(data, \"aps\", \"sound\", \"critical\")\n\tsoundVolume, _ := jsonparser.GetFloat(data, \"aps\", \"sound\", \"volume\")\n\tcontentAvailable, _ := jsonparser.GetInt(data, \"aps\", \"content-available\")\n\tcategory, _ := jsonparser.GetString(data, \"aps\", \"category\")\n\tkey1 := dat[\"key1\"]\n\tkey2 := dat[\"key2\"]\n\taps := dat[\"aps\"].(map[string]any)\n\turlArgs := aps[\"url-args\"].([]any)\n\n\tassert.Equal(t, testMessage, notification.ApnsID)\n\tassert.Equal(t, testMessage, notification.Topic)\n\tassert.Equal(t, unix, notification.Expiration.Unix())\n\tassert.Equal(t, ApnsPriorityLow, notification.Priority)\n\tassert.Equal(t, message, alert)\n\tassert.Equal(t, expectBadge, int(badge))\n\tassert.Equal(t, expectBadge, *req.Badge)\n\tassert.Equal(t, testMessage, soundName)\n\tassert.InDelta(t, 1.0, soundVolume, 0.01)\n\tassert.Equal(t, int64(1), soundCritical)\n\tassert.Equal(t, 1, int(contentAvailable))\n\tassert.Equal(t, \"test\", key1)\n\tassert.Equal(t, 2, int(key2.(float64)))\n\tassert.Equal(t, testMessage, category)\n\tassert.Contains(t, urlArgs, \"a\")\n\tassert.Contains(t, urlArgs, \"b\")\n}\n\nfunc TestIOSSoundAndVolume(t *testing.T) {\n\tvar dat map[string]any\n\n\tmessage := welcomeMessage\n\treq := &PushNotification{\n\t\tApnsID:   testMessage,\n\t\tTopic:    testMessage,\n\t\tPriority: \"normal\",\n\t\tMessage:  message,\n\t\tSound: Sound{\n\t\t\tCritical: 3,\n\t\t\tName:     testMessage,\n\t\t\tVolume:   4.5,\n\t\t},\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tpanic(err)\n\t}\n\n\talert, _ := jsonparser.GetString(data, \"aps\", \"alert\")\n\tsoundName, _ := jsonparser.GetString(data, \"aps\", \"sound\", \"name\")\n\tsoundCritical, _ := jsonparser.GetInt(data, \"aps\", \"sound\", \"critical\")\n\tsoundVolume, _ := jsonparser.GetFloat(data, \"aps\", \"sound\", \"volume\")\n\n\tassert.Equal(t, testMessage, notification.ApnsID)\n\tassert.Equal(t, testMessage, notification.Topic)\n\tassert.Equal(t, ApnsPriorityLow, notification.Priority)\n\tassert.Equal(t, message, alert)\n\tassert.Equal(t, testMessage, soundName)\n\tassert.InDelta(t, 4.5, soundVolume, 0.01)\n\tassert.Equal(t, int64(3), soundCritical)\n\n\treq.SoundName = \"foobar\"\n\treq.SoundVolume = 5.5\n\tnotification = GetIOSNotification(req)\n\tdump, _ = json.Marshal(notification.Payload)\n\tdata = []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tpanic(err)\n\t}\n\n\tsoundName, _ = jsonparser.GetString(data, \"aps\", \"sound\", \"name\")\n\tsoundVolume, _ = jsonparser.GetFloat(data, \"aps\", \"sound\", \"volume\")\n\tsoundCritical, _ = jsonparser.GetInt(data, \"aps\", \"sound\", \"critical\")\n\tassert.InDelta(t, 5.5, soundVolume, 0.01)\n\tassert.Equal(t, int64(1), soundCritical)\n\tassert.Equal(t, \"foobar\", soundName)\n\n\treq = &PushNotification{\n\t\tApnsID:   testMessage,\n\t\tTopic:    testMessage,\n\t\tPriority: \"normal\",\n\t\tMessage:  message,\n\t\tSound: map[string]any{\n\t\t\t\"critical\": 3,\n\t\t\t\"name\":     \"test\",\n\t\t\t\"volume\":   4.5,\n\t\t},\n\t}\n\n\tnotification = GetIOSNotification(req)\n\tdump, _ = json.Marshal(notification.Payload)\n\tdata = []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tpanic(err)\n\t}\n\n\tsoundName, _ = jsonparser.GetString(data, \"aps\", \"sound\", \"name\")\n\tsoundVolume, _ = jsonparser.GetFloat(data, \"aps\", \"sound\", \"volume\")\n\tsoundCritical, _ = jsonparser.GetInt(data, \"aps\", \"sound\", \"critical\")\n\tassert.InDelta(t, 4.5, soundVolume, 0.01)\n\tassert.Equal(t, int64(3), soundCritical)\n\tassert.Equal(t, \"test\", soundName)\n\n\treq = &PushNotification{\n\t\tApnsID:   testMessage,\n\t\tTopic:    testMessage,\n\t\tPriority: \"normal\",\n\t\tMessage:  message,\n\t\tSound:    \"default\",\n\t}\n\n\tnotification = GetIOSNotification(req)\n\tdump, _ = json.Marshal(notification.Payload)\n\tdata = []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tpanic(err)\n\t}\n\n\tsoundName, _ = jsonparser.GetString(data, \"aps\", \"sound\")\n\tassert.Equal(t, \"default\", soundName)\n}\n\nfunc TestIOSSummaryArg(t *testing.T) {\n\tvar dat map[string]any\n\n\tmessage := welcomeMessage\n\treq := &PushNotification{\n\t\tApnsID:   testMessage,\n\t\tTopic:    testMessage,\n\t\tPriority: \"normal\",\n\t\tMessage:  message,\n\t\tAlert: Alert{\n\t\t\tSummaryArg:      \"test\",\n\t\t\tSummaryArgCount: 3,\n\t\t},\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tpanic(err)\n\t}\n\n\tassert.Equal(t, testMessage, notification.ApnsID)\n\tassert.Equal(t, testMessage, notification.Topic)\n\tassert.Equal(t, ApnsPriorityLow, notification.Priority)\n\tassert.Equal(\n\t\tt,\n\t\t\"test\",\n\t\tdat[\"aps\"].(map[string]any)[\"alert\"].(map[string]any)[\"summary-arg\"],\n\t)\n\tassert.InDelta(\n\t\tt,\n\t\tfloat64(3),\n\t\tdat[\"aps\"].(map[string]any)[\"alert\"].(map[string]any)[\"summary-arg-count\"],\n\t\t0.01,\n\t)\n}\n\n// Silent Notification which payload’s aps dictionary must not contain the alert, sound, or badge keys.\n// ref: https://goo.gl/m9xyqG\nfunc TestSendZeroValueForBadgeKey(t *testing.T) {\n\tvar dat map[string]any\n\n\tmessage := welcomeMessage\n\treq := &PushNotification{\n\t\tApnsID:           testMessage,\n\t\tTopic:            testMessage,\n\t\tPriority:         \"normal\",\n\t\tMessage:          message,\n\t\tSound:            testMessage,\n\t\tContentAvailable: true,\n\t\tMutableContent:   true,\n\t\tThreadID:         testMessage,\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tlog.Println(err)\n\t\tpanic(err)\n\t}\n\n\talert, _ := jsonparser.GetString(data, \"aps\", \"alert\")\n\tbadge, _ := jsonparser.GetInt(data, \"aps\", \"badge\")\n\tsound, _ := jsonparser.GetString(data, \"aps\", \"sound\")\n\tthreadID, _ := jsonparser.GetString(data, \"aps\", \"thread-id\")\n\tcontentAvailable, _ := jsonparser.GetInt(data, \"aps\", \"content-available\")\n\tmutableContent, _ := jsonparser.GetInt(data, \"aps\", \"mutable-content\")\n\n\tif req.Badge != nil {\n\t\tt.Errorf(\"req.Badge must be nil\")\n\t}\n\n\tassert.Equal(t, testMessage, notification.ApnsID)\n\tassert.Equal(t, testMessage, notification.Topic)\n\tassert.Equal(t, ApnsPriorityLow, notification.Priority)\n\tassert.Equal(t, message, alert)\n\tassert.Equal(t, 0, int(badge))\n\tassert.Equal(t, testMessage, sound)\n\tassert.Equal(t, testMessage, threadID)\n\tassert.Equal(t, 1, int(contentAvailable))\n\tassert.Equal(t, 1, int(mutableContent))\n\n\t// Add Bage\n\texpectBadge := 10\n\treq.Badge = &expectBadge\n\n\tnotification = GetIOSNotification(req)\n\n\tdump, _ = json.Marshal(notification.Payload)\n\tdata = []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tlog.Println(err)\n\t\tpanic(err)\n\t}\n\n\tif req.Badge == nil {\n\t\tt.Errorf(\"req.Badge must be equal %d\", *req.Badge)\n\t}\n\n\tbadge, _ = jsonparser.GetInt(data, \"aps\", \"badge\")\n\tassert.Equal(t, expectBadge, *req.Badge)\n\tassert.Equal(t, expectBadge, int(badge))\n}\n\n// Silent Notification:\n// The payload’s aps dictionary must include the content-available key with a value of 1.\n// The payload’s aps dictionary must not contain the alert, sound, or badge keys.\n// ref: https://goo.gl/m9xyqG\nfunc TestCheckSilentNotification(t *testing.T) {\n\tvar dat map[string]any\n\n\treq := &PushNotification{\n\t\tApnsID:           testMessage,\n\t\tTopic:            testMessage,\n\t\tCollapseID:       testMessage,\n\t\tPriority:         \"normal\",\n\t\tContentAvailable: true,\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tlog.Println(err)\n\t\tpanic(err)\n\t}\n\n\tassert.Equal(t, testMessage, notification.CollapseID)\n\tassert.Equal(t, testMessage, notification.ApnsID)\n\tassert.Equal(t, testMessage, notification.Topic)\n\tassert.Nil(t, dat[\"aps\"].(map[string]any)[\"alert\"])\n\tassert.Nil(t, dat[\"aps\"].(map[string]any)[\"sound\"])\n\tassert.Nil(t, dat[\"aps\"].(map[string]any)[\"badge\"])\n}\n\n// URL: https://goo.gl/5xFo3C\n// Example 2\n//\n//\t{\n//\t    \"aps\" : {\n//\t        \"alert\" : {\n//\t            \"title\" : \"Game Request\",\n//\t            \"body\" : \"Bob wants to play poker\",\n//\t            \"action-loc-key\" : \"PLAY\"\n//\t        },\n//\t        \"badge\" : 5\n//\t    },\n//\t    \"acme1\" : \"bar\",\n//\t    \"acme2\" : [ \"bang\",  \"whiz\" ]\n//\t}\nfunc TestAlertStringExample2ForIos(t *testing.T) {\n\tvar dat map[string]any\n\n\ttitle := \"Game Request\"\n\tbody := \"Bob wants to play poker\"\n\tactionLocKey := \"PLAY\"\n\treq := &PushNotification{\n\t\tApnsID:   testMessage,\n\t\tTopic:    testMessage,\n\t\tPriority: \"normal\",\n\t\tAlert: Alert{\n\t\t\tTitle:        title,\n\t\t\tBody:         body,\n\t\t\tActionLocKey: actionLocKey,\n\t\t},\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tlog.Println(err)\n\t\tpanic(err)\n\t}\n\n\tassert.Equal(\n\t\tt,\n\t\ttitle,\n\t\tdat[\"aps\"].(map[string]any)[\"alert\"].(map[string]any)[\"title\"],\n\t)\n\tassert.Equal(\n\t\tt,\n\t\tbody,\n\t\tdat[\"aps\"].(map[string]any)[\"alert\"].(map[string]any)[\"body\"],\n\t)\n\tassert.Equal(\n\t\tt,\n\t\tactionLocKey,\n\t\tdat[\"aps\"].(map[string]any)[\"alert\"].(map[string]any)[\"action-loc-key\"],\n\t)\n}\n\n// URL: https://goo.gl/5xFo3C\n// Example 3\n//\n//\t{\n//\t    \"aps\" : {\n//\t        \"alert\" : \"You got your emails.\",\n//\t        \"badge\" : 9,\n//\t        \"sound\" : \"bingbong.aiff\"\n//\t    },\n//\t    \"acme1\" : \"bar\",\n//\t    \"acme2\" : 42\n//\t}\nfunc TestAlertStringExample3ForIos(t *testing.T) {\n\tvar dat map[string]any\n\n\tbadge := 9\n\tsound := \"bingbong.aiff\"\n\treq := &PushNotification{\n\t\tApnsID:           testMessage,\n\t\tTopic:            testMessage,\n\t\tPriority:         \"normal\",\n\t\tContentAvailable: true,\n\t\tMessage:          testMessage,\n\t\tBadge:            &badge,\n\t\tSound:            sound,\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tlog.Println(err)\n\t\tpanic(err)\n\t}\n\n\tassert.Equal(t, sound, dat[\"aps\"].(map[string]any)[\"sound\"])\n\tassert.InDelta(t, float64(badge), dat[\"aps\"].(map[string]any)[\"badge\"].(float64), 0.01)\n\tassert.Equal(t, testMessage, dat[\"aps\"].(map[string]any)[\"alert\"])\n}\n\nfunc TestMessageAndTitle(t *testing.T) {\n\tvar dat map[string]any\n\n\tmessage := welcomeMessage\n\ttitle := \"Welcome notification Server title\"\n\treq := &PushNotification{\n\t\tApnsID:           testMessage,\n\t\tTopic:            testMessage,\n\t\tPriority:         \"normal\",\n\t\tMessage:          message,\n\t\tTitle:            title,\n\t\tContentAvailable: true,\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tlog.Println(err)\n\t\tpanic(err)\n\t}\n\n\talert, _ := jsonparser.GetString(data, \"aps\", \"alert\")\n\talertBody, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"body\")\n\talertTitle, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"title\")\n\n\tassert.Equal(t, testMessage, notification.ApnsID)\n\tassert.Equal(t, ApnsPriorityLow, notification.Priority)\n\tassert.Equal(t, message, alertBody)\n\tassert.Equal(t, title, alertTitle)\n\tassert.NotEqual(t, message, alert)\n\n\t// Add alert body\n\tmessageOverride := \"Welcome notification Server overridden\"\n\treq.Alert.Body = messageOverride\n\n\tnotification = GetIOSNotification(req)\n\n\tdump, _ = json.Marshal(notification.Payload)\n\tdata = []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tlog.Println(err)\n\t\tpanic(err)\n\t}\n\n\talertBodyOverridden, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"body\")\n\talertTitle, _ = jsonparser.GetString(data, \"aps\", \"alert\", \"title\")\n\tassert.Equal(t, messageOverride, alertBodyOverridden)\n\tassert.NotEqual(t, message, alertBodyOverridden)\n\tassert.Equal(t, title, alertTitle)\n}\n\nfunc TestIOSAlertNotificationStructure(t *testing.T) {\n\tvar dat map[string]any\n\tunix := time.Now().Unix()\n\tstaleDate := time.Now().Unix()\n\tdismissalDate := staleDate + 5\n\ttimeStamp := time.Now().Unix()\n\titemID := float64(12345)\n\n\treq := &PushNotification{\n\t\tMessage: \"Welcome\",\n\t\tTitle:   testMessage,\n\t\tAlert: Alert{\n\t\t\tAction:       testMessage,\n\t\t\tActionLocKey: testMessage,\n\t\t\tBody:         testMessage,\n\t\t\tLaunchImage:  testMessage,\n\t\t\tLocArgs:      []string{\"a\", \"b\"},\n\t\t\tLocKey:       testMessage,\n\t\t\tSubtitle:     testMessage,\n\t\t\tTitleLocArgs: []string{\"a\", \"b\"},\n\t\t\tTitleLocKey:  testMessage,\n\t\t},\n\t\tInterruptionLevel: testMessage,\n\t\tStaleDate:         staleDate,\n\t\tDismissalDate:     dismissalDate,\n\t\tEvent:             testMessage,\n\t\tTimestamp:         timeStamp,\n\t\tContentState: D{\n\t\t\t\"item_id\":   itemID,\n\t\t\t\"item_name\": testMessage,\n\t\t},\n\t}\n\n\tnotification := GetIOSNotification(req)\n\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tif err := json.Unmarshal(data, &dat); err != nil {\n\t\tlog.Println(err)\n\t\tpanic(err)\n\t}\n\n\taction, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"action\")\n\tactionLocKey, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"action-loc-key\")\n\tbody, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"body\")\n\tlaunchImage, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"launch-image\")\n\tlocKey, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"loc-key\")\n\ttitle, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"title\")\n\tsubtitle, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"subtitle\")\n\ttitleLocKey, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"title-loc-key\")\n\tinterruptionLevel, _ := jsonparser.GetString(data, \"aps\", \"interruption-level\")\n\tstaleDate, _ = jsonparser.GetInt(data, \"aps\", \"stale-date\")\n\tevent, _ := jsonparser.GetString(data, \"aps\", \"event\")\n\ttimestamp, _ := jsonparser.GetInt(data, \"aps\", \"timestamp\")\n\taps := dat[\"aps\"].(map[string]any)\n\talert := aps[\"alert\"].(map[string]any)\n\ttitleLocArgs := alert[\"title-loc-args\"].([]any)\n\tlocArgs := alert[\"loc-args\"].([]any)\n\tcontentState := aps[\"content-state\"].(map[string]any)\n\tcontentStateItemID := contentState[\"item_id\"]\n\tcontentStateItemName := contentState[\"item_name\"]\n\n\tassert.Equal(t, testMessage, action)\n\tassert.Equal(t, testMessage, actionLocKey)\n\tassert.Equal(t, testMessage, body)\n\tassert.Equal(t, testMessage, launchImage)\n\tassert.Equal(t, testMessage, locKey)\n\tassert.Equal(t, testMessage, title)\n\tassert.Equal(t, testMessage, subtitle)\n\tassert.Equal(t, testMessage, titleLocKey)\n\tassert.Equal(t, testMessage, interruptionLevel)\n\tassert.Equal(t, testMessage, event)\n\tassert.Equal(t, unix, staleDate)\n\tassert.Equal(t, unix, timestamp)\n\n\t// dynamic contentState content\n\tassert.InDelta(t, contentStateItemID, itemID, 0.01)\n\tassert.Equal(t, contentStateItemName, testMessage)\n\tassert.Contains(t, contentState, \"item_id\")\n\tassert.Contains(t, contentState, \"item_name\")\n\n\tassert.Contains(t, titleLocArgs, \"a\")\n\tassert.Contains(t, titleLocArgs, \"b\")\n\tassert.Contains(t, locArgs, \"a\")\n\tassert.Contains(t, locArgs, \"b\")\n}\n\nfunc TestWrongIosCertificateExt(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = \"test\"\n\terr := InitAPNSClient(context.Background(), cfg)\n\n\trequire.Error(t, err)\n\tassert.Equal(t, \"wrong certificate key extension\", err.Error())\n\n\tcfg.Ios.KeyPath = \"\"\n\tcfg.Ios.KeyBase64 = \"abcd\"\n\tcfg.Ios.KeyType = \"abcd\"\n\terr = InitAPNSClient(context.Background(), cfg)\n\n\trequire.Error(t, err)\n\tassert.Equal(t, \"wrong certificate key type\", err.Error())\n}\n\nfunc TestAPNSClientDevHost(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = \"../certificate/certificate-valid.p12\"\n\terr := InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)\n\n\tcfg.Ios.KeyPath = \"\"\n\tcfg.Ios.KeyBase64 = certificateValidP12\n\tcfg.Ios.KeyType = \"p12\"\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)\n}\n\nfunc TestAPNSClientProdHost(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.Production = true\n\tcfg.Ios.KeyPath = testKeyPath\n\terr := InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostProduction, ApnsClient.Host)\n\n\tcfg.Ios.KeyPath = \"\"\n\tcfg.Ios.KeyBase64 = certificateValidPEM\n\tcfg.Ios.KeyType = \"pem\"\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostProduction, ApnsClient.Host)\n}\n\nfunc TestAPNSClientInvaildToken(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = \"../certificate/authkey-invalid.p8\"\n\terr := InitAPNSClient(context.Background(), cfg)\n\trequire.Error(t, err)\n\n\tcfg.Ios.KeyPath = \"\"\n\tcfg.Ios.KeyBase64 = authkeyInvalidP8\n\tcfg.Ios.KeyType = \"p8\"\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.Error(t, err)\n\n\t// empty key-id or team-id\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPathP8\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.Error(t, err)\n\n\tcfg.Ios.KeyID = testKeyID\n\tcfg.Ios.TeamID = \"\"\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.Error(t, err)\n\n\tcfg.Ios.KeyID = \"\"\n\tcfg.Ios.TeamID = testTeamID\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.Error(t, err)\n}\n\nfunc TestAPNSClientVaildToken(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPathP8\n\tcfg.Ios.KeyID = testKeyID\n\tcfg.Ios.TeamID = testTeamID\n\terr := InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)\n\n\tcfg.Ios.Production = true\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostProduction, ApnsClient.Host)\n\n\t// test base64\n\tcfg.Ios.Production = false\n\tcfg.Ios.KeyPath = \"\"\n\tcfg.Ios.KeyBase64 = authkeyValidP8\n\tcfg.Ios.KeyType = \"p8\"\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)\n\n\tcfg.Ios.Production = true\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostProduction, ApnsClient.Host)\n}\n\nfunc TestAPNSClientUseProxy(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = \"../certificate/certificate-valid.p12\"\n\tcfg.Core.HTTPProxy = \"http://127.0.0.1:8080\"\n\t_ = SetProxy(cfg.Core.HTTPProxy)\n\terr := InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)\n\n\treq, _ := http.NewRequestWithContext(\n\t\tcontext.Background(), http.MethodGet, apns2.HostDevelopment, nil,\n\t)\n\tactualProxyURL, err := ApnsClient.HTTPClient.Transport.(*http.Transport).Proxy(req)\n\trequire.NoError(t, err)\n\n\texpectedProxyURL, _ := url.ParseRequestURI(cfg.Core.HTTPProxy)\n\tassert.Equal(t, expectedProxyURL, actualProxyURL)\n\n\tcfg.Ios.KeyPath = testKeyPathP8\n\tcfg.Ios.TeamID = \"example.team\"\n\tcfg.Ios.KeyID = \"example.key\"\n\terr = InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)\n\tassert.NotNil(t, ApnsClient.Token)\n\n\treq, _ = http.NewRequestWithContext(\n\t\tcontext.Background(), http.MethodGet, apns2.HostDevelopment, nil,\n\t)\n\tactualProxyURL, err = ApnsClient.HTTPClient.Transport.(*http.Transport).Proxy(req)\n\trequire.NoError(t, err)\n\n\texpectedProxyURL, _ = url.ParseRequestURI(cfg.Core.HTTPProxy)\n\tassert.Equal(t, expectedProxyURL, actualProxyURL)\n\n\thttp.DefaultTransport.(*http.Transport).Proxy = nil\n}\n\nfunc TestPushToIOS(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tMaxConcurrentIOSPushes = make(chan struct{}, cfg.Ios.MaxConcurrentPushes)\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPath\n\terr := InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\terr = status.InitAppStatus(cfg)\n\trequire.NoError(t, err)\n\n\treq := &PushNotification{\n\t\tTokens: []string{\n\t\t\t\"11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7\",\n\t\t\t\"11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef1\",\n\t\t},\n\t\tPlatform: 1,\n\t\tMessage:  \"Welcome\",\n\t}\n\n\t// send fail\n\tresp, err := PushToIOS(context.Background(), req, cfg)\n\trequire.NoError(t, err)\n\tassert.Len(t, resp.Logs, 2)\n}\n\nfunc TestApnsHostFromRequest(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPath\n\terr := InitAPNSClient(context.Background(), cfg)\n\trequire.NoError(t, err)\n\terr = status.InitAppStatus(cfg)\n\trequire.NoError(t, err)\n\n\treq := &PushNotification{\n\t\tProduction: true,\n\t}\n\tclient := getApnsClient(cfg, req)\n\tassert.Equal(t, apns2.HostProduction, client.Host)\n\n\treq = &PushNotification{\n\t\tDevelopment: true,\n\t}\n\tclient = getApnsClient(cfg, req)\n\tassert.Equal(t, apns2.HostDevelopment, client.Host)\n\n\treq = &PushNotification{}\n\tcfg.Ios.Production = true\n\tclient = getApnsClient(cfg, req)\n\tassert.Equal(t, apns2.HostProduction, client.Host)\n\n\tcfg.Ios.Production = false\n\tclient = getApnsClient(cfg, req)\n\tassert.Equal(t, apns2.HostDevelopment, client.Host)\n}\n\n// Tests for refactored helper functions\n\nfunc TestSetAlertTitleAndBody(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\treq       *PushNotification\n\t\twantBody  string\n\t\twantTitle string\n\t}{\n\t\t{\n\t\t\tname: \"title and message set\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTitle:   \"Test Title\",\n\t\t\t\tMessage: \"Test Message\",\n\t\t\t},\n\t\t\twantTitle: \"Test Title\",\n\t\t\twantBody:  \"Test Message\",\n\t\t},\n\t\t{\n\t\t\tname: \"alert title overrides req title\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTitle: \"Original Title\",\n\t\t\t\tAlert: Alert{\n\t\t\t\t\tTitle: \"Alert Title\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantTitle: \"Alert Title\",\n\t\t},\n\t\t{\n\t\t\tname: \"alert body overrides req message\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTitle:   \"Title\",\n\t\t\t\tMessage: \"Original Message\",\n\t\t\t\tAlert: Alert{\n\t\t\t\t\tBody: \"Alert Body\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantBody: \"Alert Body\",\n\t\t},\n\t\t{\n\t\t\tname: \"subtitle is set\",\n\t\t\treq: &PushNotification{\n\t\t\t\tAlert: Alert{\n\t\t\t\t\tSubtitle: \"Test Subtitle\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnotification := GetIOSNotification(tt.req)\n\t\t\tdump, _ := json.Marshal(notification.Payload)\n\t\t\tdata := []byte(string(dump))\n\n\t\t\tif tt.wantTitle != \"\" {\n\t\t\t\ttitle, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"title\")\n\t\t\t\tassert.Equal(t, tt.wantTitle, title)\n\t\t\t}\n\t\t\tif tt.wantBody != \"\" {\n\t\t\t\tbody, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"body\")\n\t\t\t\tassert.Equal(t, tt.wantBody, body)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetAlertLocalization(t *testing.T) {\n\treq := &PushNotification{\n\t\tAlert: Alert{\n\t\t\tTitleLocKey:  \"TITLE_LOC_KEY\",\n\t\t\tTitleLocArgs: []string{\"arg1\", \"arg2\"},\n\t\t\tLocKey:       \"LOC_KEY\",\n\t\t\tLocArgs:      []string{\"locarg1\"},\n\t\t\tActionLocKey: \"ACTION_LOC_KEY\",\n\t\t},\n\t}\n\n\tnotification := GetIOSNotification(req)\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\ttitleLocKey, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"title-loc-key\")\n\tlocKey, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"loc-key\")\n\tactionLocKey, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"action-loc-key\")\n\n\tassert.Equal(t, \"TITLE_LOC_KEY\", titleLocKey)\n\tassert.Equal(t, \"LOC_KEY\", locKey)\n\tassert.Equal(t, \"ACTION_LOC_KEY\", actionLocKey)\n}\n\nfunc TestSetAlertActions(t *testing.T) {\n\treq := &PushNotification{\n\t\tAlert: Alert{\n\t\t\tLaunchImage:     \"launch.png\",\n\t\t\tAction:          \"View\",\n\t\t\tSummaryArg:      \"summary\",\n\t\t\tSummaryArgCount: 5,\n\t\t},\n\t}\n\n\tnotification := GetIOSNotification(req)\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tlaunchImage, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"launch-image\")\n\taction, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"action\")\n\tsummaryArg, _ := jsonparser.GetString(data, \"aps\", \"alert\", \"summary-arg\")\n\tsummaryArgCount, _ := jsonparser.GetInt(data, \"aps\", \"alert\", \"summary-arg-count\")\n\n\tassert.Equal(t, \"launch.png\", launchImage)\n\tassert.Equal(t, \"View\", action)\n\tassert.Equal(t, \"summary\", summaryArg)\n\tassert.Equal(t, int64(5), summaryArgCount)\n}\n\nfunc TestSetLiveActivityFields(t *testing.T) {\n\tstaleDate := time.Now().Unix()\n\tdismissalDate := staleDate + 100\n\ttimestamp := time.Now().Unix()\n\n\treq := &PushNotification{\n\t\tContentState: D{\n\t\t\t\"score\": 10,\n\t\t},\n\t\tStaleDate:     staleDate,\n\t\tDismissalDate: dismissalDate,\n\t\tEvent:         \"update\",\n\t\tTimestamp:     timestamp,\n\t}\n\n\tnotification := GetIOSNotification(req)\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\tgotStaleDate, _ := jsonparser.GetInt(data, \"aps\", \"stale-date\")\n\tgotEvent, _ := jsonparser.GetString(data, \"aps\", \"event\")\n\tgotTimestamp, _ := jsonparser.GetInt(data, \"aps\", \"timestamp\")\n\n\tassert.Equal(t, staleDate, gotStaleDate)\n\tassert.Equal(t, \"update\", gotEvent)\n\tassert.Equal(t, timestamp, gotTimestamp)\n}\n\nfunc TestSetNotificationPriority(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tpriority     string\n\t\twantPriority int\n\t}{\n\t\t{\n\t\t\tname:         \"normal priority\",\n\t\t\tpriority:     \"normal\",\n\t\t\twantPriority: apns2.PriorityLow,\n\t\t},\n\t\t{\n\t\t\tname:         \"high priority\",\n\t\t\tpriority:     \"high\",\n\t\t\twantPriority: apns2.PriorityHigh,\n\t\t},\n\t\t{\n\t\t\tname:         \"empty priority\",\n\t\t\tpriority:     \"\",\n\t\t\twantPriority: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treq := &PushNotification{\n\t\t\t\tPriority: tt.priority,\n\t\t\t\tMessage:  \"test\",\n\t\t\t}\n\t\t\tnotification := GetIOSNotification(req)\n\t\t\tassert.Equal(t, tt.wantPriority, notification.Priority)\n\t\t})\n\t}\n}\n\nfunc TestSetPayloadSound(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\treq       *PushNotification\n\t\twantSound string\n\t}{\n\t\t{\n\t\t\tname: \"string sound\",\n\t\t\treq: &PushNotification{\n\t\t\t\tMessage: \"test\",\n\t\t\t\tSound:   \"default\",\n\t\t\t},\n\t\t\twantSound: \"default\",\n\t\t},\n\t\t{\n\t\t\tname: \"sound name override\",\n\t\t\treq: &PushNotification{\n\t\t\t\tMessage:   \"test\",\n\t\t\t\tSoundName: \"custom.aiff\",\n\t\t\t},\n\t\t\twantSound: \"custom.aiff\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnotification := GetIOSNotification(tt.req)\n\t\t\tdump, _ := json.Marshal(notification.Payload)\n\t\t\tdata := []byte(string(dump))\n\n\t\t\tif tt.req.SoundName != \"\" {\n\t\t\t\tsoundName, _ := jsonparser.GetString(data, \"aps\", \"sound\", \"name\")\n\t\t\t\tassert.Equal(t, tt.wantSound, soundName)\n\t\t\t} else {\n\t\t\t\tsound, _ := jsonparser.GetString(data, \"aps\", \"sound\")\n\t\t\t\tassert.Equal(t, tt.wantSound, sound)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBuildIOSPayload(t *testing.T) {\n\tbadge := 5\n\treq := &PushNotification{\n\t\tMessage:          \"Hello World\",\n\t\tBadge:            &badge,\n\t\tMutableContent:   true,\n\t\tContentAvailable: true,\n\t\tURLArgs:          []string{\"foo\", \"bar\"},\n\t\tThreadID:         \"thread-123\",\n\t\tData: D{\n\t\t\t\"custom_key\": \"custom_value\",\n\t\t},\n\t}\n\n\tnotification := GetIOSNotification(req)\n\tdump, _ := json.Marshal(notification.Payload)\n\tdata := []byte(string(dump))\n\n\talert, _ := jsonparser.GetString(data, \"aps\", \"alert\")\n\tgotBadge, _ := jsonparser.GetInt(data, \"aps\", \"badge\")\n\tmutableContent, _ := jsonparser.GetInt(data, \"aps\", \"mutable-content\")\n\tcontentAvailable, _ := jsonparser.GetInt(data, \"aps\", \"content-available\")\n\tthreadID, _ := jsonparser.GetString(data, \"aps\", \"thread-id\")\n\tcustomKey, _ := jsonparser.GetString(data, \"custom_key\")\n\n\tassert.Equal(t, \"Hello World\", alert)\n\tassert.Equal(t, int64(5), gotBadge)\n\tassert.Equal(t, int64(1), mutableContent)\n\tassert.Equal(t, int64(1), contentAvailable)\n\tassert.Equal(t, \"thread-123\", threadID)\n\tassert.Equal(t, \"custom_value\", customKey)\n}\n\nfunc TestLoadCertFromFile(t *testing.T) {\n\t// Test valid p12 file\n\tcert, authKey, ext, err := loadCertFromFile(\"../certificate/certificate-valid.p12\", \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, dotP12, ext)\n\tassert.NotNil(t, cert.Certificate)\n\tassert.Nil(t, authKey)\n\n\t// Test valid pem file\n\tcert, authKey, ext, err = loadCertFromFile(\"../certificate/certificate-valid.pem\", \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, dotPEM, ext)\n\tassert.NotNil(t, cert.Certificate)\n\tassert.Nil(t, authKey)\n\n\t// Test valid p8 file\n\t_, authKey, ext, err = loadCertFromFile(\"../certificate/authkey-valid.p8\", \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, dotP8, ext)\n\tassert.NotNil(t, authKey)\n\n\t// Test invalid extension\n\tinvalidCert, invalidAuthKey, invalidExt, err := loadCertFromFile(\"test.invalid\", \"\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"wrong certificate key extension\", err.Error())\n\tassert.Empty(t, invalidCert.Certificate)\n\tassert.Nil(t, invalidAuthKey)\n\tassert.Equal(t, \".invalid\", invalidExt)\n}\n\nfunc TestLoadCertFromBase64(t *testing.T) {\n\t// Test valid p12 base64\n\tcert, authKey, ext, err := loadCertFromBase64(certificateValidP12, \"p12\", \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, dotP12, ext)\n\tassert.NotNil(t, cert.Certificate)\n\tassert.Nil(t, authKey)\n\n\t// Test valid pem base64\n\tcert, authKey, ext, err = loadCertFromBase64(certificateValidPEM, \"pem\", \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, dotPEM, ext)\n\tassert.NotNil(t, cert.Certificate)\n\tassert.Nil(t, authKey)\n\n\t// Test valid p8 base64\n\t_, authKey, ext, err = loadCertFromBase64(authkeyValidP8, \"p8\", \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, dotP8, ext)\n\tassert.NotNil(t, authKey)\n\n\t// Test invalid key type\n\tinvalidCert, invalidAuthKey, invalidExt, err := loadCertFromBase64(\"test\", \"invalid\", \"\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"wrong certificate key type\", err.Error())\n\tassert.Empty(t, invalidCert.Certificate)\n\tassert.Nil(t, invalidAuthKey)\n\tassert.Equal(t, \".invalid\", invalidExt)\n\n\t// Test invalid base64\n\tbadBase64Cert, badBase64AuthKey, badBase64Ext, err := loadCertFromBase64(\n\t\t\"not-valid-base64!!!\",\n\t\t\"p12\",\n\t\t\"\",\n\t)\n\trequire.Error(t, err)\n\tassert.Empty(t, badBase64Cert.Certificate)\n\tassert.Nil(t, badBase64AuthKey)\n\tassert.Equal(t, \".p12\", badBase64Ext)\n}\n\nfunc TestCreateAPNSClientWithToken(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\t// Test missing KeyID\n\tcfg.Ios.KeyID = \"\"\n\tcfg.Ios.TeamID = testTeamID\n\t_, err := createAPNSClientWithToken(cfg, nil)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"you should provide ios.KeyID and ios.TeamID for p8 token\", err.Error())\n\n\t// Test missing TeamID\n\tcfg.Ios.KeyID = testKeyID\n\tcfg.Ios.TeamID = \"\"\n\t_, err = createAPNSClientWithToken(cfg, nil)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"you should provide ios.KeyID and ios.TeamID for p8 token\", err.Error())\n}\n"
  },
  {
    "path": "notify/notification_fcm.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/status\"\n\n\t\"firebase.google.com/go/v4/messaging\"\n\t\"github.com/appleboy/go-fcm\"\n)\n\nfunc fileExists(filename string) bool {\n\tinfo, err := os.Stat(filename)\n\tif os.IsNotExist(err) {\n\t\treturn false\n\t}\n\treturn !info.IsDir()\n}\n\n// InitFCMClient use for initialize FCM Client.\nfunc InitFCMClient(ctx context.Context, cfg *config.ConfYaml) (*fcm.Client, error) {\n\tvar opts []fcm.Option\n\n\tcredential := os.Getenv(\"GOOGLE_APPLICATION_CREDENTIALS\")\n\tif cfg.Android.Credential == \"\" &&\n\t\tcfg.Android.KeyPath == \"\" &&\n\t\tcredential == \"\" {\n\t\treturn nil, errors.New(\"missing fcm credential data\")\n\t}\n\n\tif cfg.Android.KeyPath != \"\" && fileExists(cfg.Android.KeyPath) {\n\t\topts = append(opts, fcm.WithCredentialsFile(cfg.Android.KeyPath))\n\t}\n\n\tif cfg.Android.Credential != \"\" {\n\t\topts = append(opts, fcm.WithCredentialsJSON([]byte(cfg.Android.Credential)))\n\t}\n\n\tif FCMClient != nil {\n\t\treturn FCMClient, nil\n\t}\n\n\tvar err error\n\tFCMClient, err = fcm.NewClient(\n\t\tctx,\n\t\topts...,\n\t)\n\n\treturn FCMClient, err\n}\n\n// setupFCMNotification sets up the notification fields on the request.\nfunc setupFCMNotification(req *PushNotification) {\n\tif req.Title == \"\" && req.Message == \"\" && req.Image == \"\" {\n\t\treturn\n\t}\n\tif req.Notification == nil {\n\t\treq.Notification = &messaging.Notification{}\n\t}\n\tif req.Title != \"\" {\n\t\treq.Notification.Title = req.Title\n\t}\n\tif req.Message != \"\" {\n\t\treq.Notification.Body = req.Message\n\t}\n\tif req.Image != \"\" {\n\t\treq.Notification.ImageURL = req.Image\n\t}\n\tif req.MutableContent {\n\t\treq.APNS = &messaging.APNSConfig{\n\t\t\tPayload: &messaging.APNSPayload{\n\t\t\t\tAps: &messaging.Aps{\n\t\t\t\t\tMutableContent: req.MutableContent,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n}\n\n// setupFCMContentAvailable sets up the content available config for background notifications.\nfunc setupFCMContentAvailable(req *PushNotification) {\n\tif !req.ContentAvailable {\n\t\treturn\n\t}\n\treq.APNS = &messaging.APNSConfig{\n\t\tHeaders: map[string]string{\n\t\t\t\"apns-priority\": \"5\",\n\t\t},\n\t\tPayload: &messaging.APNSPayload{\n\t\t\tAps: &messaging.Aps{\n\t\t\t\tContentAvailable: req.ContentAvailable,\n\t\t\t\tCustomData:       req.Data,\n\t\t\t},\n\t\t},\n\t}\n}\n\n// setAPNSSound sets the sound on the APNS config, initializing nested structs as needed.\nfunc setAPNSSound(req *PushNotification, sound string) {\n\tswitch {\n\tcase req.APNS == nil:\n\t\treq.APNS = &messaging.APNSConfig{\n\t\t\tPayload: &messaging.APNSPayload{\n\t\t\t\tAps: &messaging.Aps{Sound: sound},\n\t\t\t},\n\t\t}\n\tcase req.APNS.Payload == nil:\n\t\treq.APNS.Payload = &messaging.APNSPayload{\n\t\t\tAps: &messaging.Aps{Sound: sound},\n\t\t}\n\tcase req.APNS.Payload.Aps == nil:\n\t\treq.APNS.Payload.Aps = &messaging.Aps{Sound: sound}\n\tdefault:\n\t\treq.APNS.Payload.Aps.Sound = sound\n\t}\n}\n\n// setupFCMSound sets up the sound configuration for FCM notifications.\nfunc setupFCMSound(req *PushNotification) {\n\tif req.Sound == nil {\n\t\treturn\n\t}\n\tsound, ok := req.Sound.(string)\n\tif !ok {\n\t\treturn\n\t}\n\tsetAPNSSound(req, sound)\n\tif req.Android == nil {\n\t\treq.Android = &messaging.AndroidConfig{\n\t\t\tPriority: req.Priority,\n\t\t\tNotification: &messaging.AndroidNotification{\n\t\t\t\tSound: sound,\n\t\t\t},\n\t\t}\n\t}\n}\n\n// convertDataToStringMap converts the request data to a string map.\nfunc convertDataToStringMap(data map[string]any) map[string]string {\n\tif len(data) == 0 {\n\t\treturn nil\n\t}\n\tresult := make(map[string]string, len(data))\n\tfor k, v := range data {\n\t\tswitch v.(type) {\n\t\tcase string:\n\t\t\tresult[k] = fmt.Sprintf(\"%s\", v)\n\t\tdefault:\n\t\t\tif b, err := json.Marshal(v); err == nil {\n\t\t\t\tresult[k] = string(b)\n\t\t\t}\n\t\t}\n\t}\n\treturn result\n}\n\n// buildFCMMessage creates a new FCM message with common fields.\nfunc buildFCMMessage(req *PushNotification, data map[string]string) *messaging.Message {\n\tmsg := &messaging.Message{\n\t\tNotification: req.Notification,\n\t\tAndroid:      req.Android,\n\t\tWebpush:      req.Webpush,\n\t\tAPNS:         req.APNS,\n\t\tFCMOptions:   req.FCMOptions,\n\t}\n\tif len(data) > 0 {\n\t\tmsg.Data = data\n\t}\n\treturn msg\n}\n\n// GetAndroidNotification use for define Android notification.\n// HTTP Connection Server Reference for Android\n// https://firebase.google.com/docs/cloud-messaging/http-server-ref\nfunc GetAndroidNotification(req *PushNotification) []*messaging.Message {\n\tsetupFCMNotification(req)\n\tsetupFCMContentAvailable(req)\n\tsetupFCMSound(req)\n\n\tdata := convertDataToStringMap(req.Data)\n\tvar messages []*messaging.Message\n\n\tif req.IsTopic() {\n\t\tmsg := buildFCMMessage(req, data)\n\t\tmsg.Topic = req.Topic\n\t\tmsg.Condition = req.Condition\n\t\tmessages = append(messages, msg)\n\t}\n\n\tfor _, token := range req.Tokens {\n\t\tmsg := buildFCMMessage(req, data)\n\t\tmsg.Token = token\n\t\tmessages = append(messages, msg)\n\t}\n\n\treturn messages\n}\n\n// handleTopicResponse processes the topic message response and returns whether to retry.\nfunc handleTopicResponse(\n\tcfg *config.ConfYaml, req *PushNotification, res *messaging.BatchResponse, resp *ResponsePush,\n) bool {\n\tif !req.IsTopic() {\n\t\treturn false\n\t}\n\n\tto := req.Topic\n\tif req.Condition != \"\" {\n\t\tto = req.Condition\n\t}\n\tlogx.LogAccess.Debug(\"Send Topic Message: \", to)\n\n\ttopicResp := res.Responses[0]\n\tif topicResp.Success {\n\t\tlogPush(cfg, core.SucceededPush, to, req, nil)\n\t}\n\n\tretryTopic := false\n\tif topicResp.Error != nil {\n\t\terrLog := logPush(cfg, core.FailedPush, to, req, topicResp.Error)\n\t\tresp.Logs = append(resp.Logs, errLog)\n\t\tretryTopic = true\n\t}\n\n\t// remove the first response\n\tres.Responses = res.Responses[1:]\n\treturn retryTopic\n}\n\n// handleTokenResponses processes individual token responses and returns tokens that need retry.\nfunc handleTokenResponses(\n\tcfg *config.ConfYaml, req *PushNotification, res *messaging.BatchResponse, resp *ResponsePush,\n) []string {\n\tvar newTokens []string\n\tfor k, result := range res.Responses {\n\t\tif result.Error != nil {\n\t\t\terrLog := logPush(cfg, core.FailedPush, req.Tokens[k], req, result.Error)\n\t\t\tresp.Logs = append(resp.Logs, errLog)\n\t\t\tnewTokens = append(newTokens, req.Tokens[k])\n\t\t\tcontinue\n\t\t}\n\t\tlogPush(cfg, core.SucceededPush, req.Tokens[k], req, nil)\n\t}\n\treturn newTokens\n}\n\n// logDevMessages logs messages in development mode.\nfunc logDevMessages(messages []*messaging.Message) {\n\tfor i, msg := range messages {\n\t\tm, _ := json.Marshal(msg)\n\t\tlogx.LogAccess.Infof(\"message #%d - %s\", i, m)\n\t}\n}\n\n// PushToAndroid provide send notification to Android server.\nfunc PushToAndroid(\n\tctx context.Context,\n\treq *PushNotification,\n\tcfg *config.ConfYaml,\n) (resp *ResponsePush, err error) {\n\tlogx.LogAccess.Debug(\"Start push notification for Android\")\n\n\tif err = CheckMessage(req); err != nil {\n\t\tlogx.LogError.Error(\"request error: \" + err.Error())\n\t\treturn nil, err\n\t}\n\n\tclient, err := InitFCMClient(ctx, cfg)\n\tif err != nil {\n\t\tlogx.LogError.Error(\"FCM server error: \" + err.Error())\n\t\treturn nil, err\n\t}\n\n\tmaxRetry := cfg.Android.MaxRetry\n\tif req.Retry > 0 && req.Retry < maxRetry {\n\t\tmaxRetry = req.Retry\n\t}\n\n\tresp = &ResponsePush{}\n\tretryCount := 0\n\nRetry:\n\tmessages := GetAndroidNotification(req)\n\n\tif req.Development {\n\t\tlogDevMessages(messages)\n\t}\n\n\tres, err := client.Send(ctx, messages...)\n\tif err != nil {\n\t\tnewErr := fmt.Errorf(\"fcm service send message error: %v\", err)\n\t\tlogx.LogError.Error(newErr)\n\t\terrLog := logPush(cfg, core.FailedPush, \"\", req, newErr)\n\t\tresp.Logs = append(resp.Logs, errLog)\n\t\tstatus.StatStorage.AddAndroidError(1)\n\t\treturn resp, newErr\n\t}\n\n\tlogx.LogAccess.Debug(\n\t\tfmt.Sprintf(\n\t\t\t\"Android Success count: %d, Failure count: %d\",\n\t\t\tres.SuccessCount,\n\t\t\tres.FailureCount,\n\t\t),\n\t)\n\tstatus.StatStorage.AddAndroidSuccess(int64(res.SuccessCount))\n\tstatus.StatStorage.AddAndroidError(int64(res.FailureCount))\n\n\tretryTopic := handleTopicResponse(cfg, req, res, resp)\n\tnewTokens := handleTokenResponses(cfg, req, res, resp)\n\n\tif len(newTokens) > 0 && retryCount < maxRetry {\n\t\tretryCount++\n\t\tif req.IsTopic() && !retryTopic {\n\t\t\treq.Topic = \"\"\n\t\t\treq.Condition = \"\"\n\t\t}\n\t\treq.Tokens = newTokens\n\t\tgoto Retry\n\t}\n\n\treturn resp, nil\n}\n\nfunc logPush(\n\tcfg *config.ConfYaml,\n\tstatus, token string,\n\treq *PushNotification,\n\terr error,\n) logx.LogPushEntry {\n\treturn logx.LogPush(&logx.InputLog{\n\t\tID:          req.ID,\n\t\tStatus:      status,\n\t\tToken:       token,\n\t\tMessage:     req.Message,\n\t\tPlatform:    req.Platform,\n\t\tError:       err,\n\t\tHideToken:   cfg.Log.HideToken,\n\t\tHideMessage: cfg.Log.HideMessages,\n\t\tFormat:      cfg.Log.Format,\n\t})\n}\n"
  },
  {
    "path": "notify/notification_fcm_test.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"firebase.google.com/go/v4/messaging\"\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMissingAndroidCredential(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = \"\"\n\n\terr := CheckPushConf(cfg)\n\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing fcm credential data\", err.Error())\n}\n\nfunc TestMissingKeyForInitFCMClient(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tcfg.Android.Credential = \"\"\n\tcfg.Android.KeyPath = \"\"\n\tclient, err := InitFCMClient(context.Background(), cfg)\n\n\tassert.Nil(t, client)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing fcm credential data\", err.Error())\n}\n\nfunc TestPushToAndroidWrongToken(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\n\treq := &PushNotification{\n\t\tTokens:   []string{\"aaaaaa\", \"bbbbb\"},\n\t\tPlatform: core.PlatFormAndroid,\n\t\tMessage:  \"Welcome\",\n\t}\n\n\t// Android Success count: 0, Failure count: 2\n\tresp, err := PushToAndroid(context.Background(), req, cfg)\n\trequire.NoError(t, err)\n\tassert.Len(t, resp.Logs, 2)\n}\n\nfunc TestPushToAndroidRightTokenForJSONLog(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\t// log for json\n\tcfg.Log.Format = \"json\"\n\n\tandroidToken := os.Getenv(\"FCM_TEST_TOKEN\")\n\n\treq := &PushNotification{\n\t\tTokens:   []string{androidToken},\n\t\tPlatform: core.PlatFormAndroid,\n\t\tMessage:  \"Welcome\",\n\t}\n\n\tresp, err := PushToAndroid(context.Background(), req, cfg)\n\trequire.NoError(t, err)\n\tassert.Empty(t, resp.Logs)\n}\n\nfunc TestPushToAndroidRightTokenForStringLog(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\n\tandroidToken := os.Getenv(\"FCM_TEST_TOKEN\")\n\n\treq := &PushNotification{\n\t\tTokens:   []string{androidToken},\n\t\tPlatform: core.PlatFormAndroid,\n\t\tMessage:  \"Welcome\",\n\t}\n\n\tresp, err := PushToAndroid(context.Background(), req, cfg)\n\trequire.NoError(t, err)\n\tassert.Empty(t, resp.Logs)\n}\n\nfunc TestFCMMessage(t *testing.T) {\n\tvar err error\n\n\t// the message must specify at least one registration ID\n\treq := &PushNotification{\n\t\tMessage: \"Test\",\n\t\tTokens:  []string{},\n\t}\n\n\terr = CheckMessage(req)\n\trequire.Error(t, err)\n\n\t// ignore check token length if send topic message\n\treq = &PushNotification{\n\t\tMessage:  \"Test\",\n\t\tPlatform: core.PlatFormAndroid,\n\t\tTopic:    \"/topics/foo-bar\",\n\t}\n\n\terr = CheckMessage(req)\n\trequire.NoError(t, err)\n\n\t// \"condition\": \"'dogs' in topics || 'cats' in topics\",\n\treq = &PushNotification{\n\t\tMessage:   \"Test\",\n\t\tPlatform:  core.PlatFormAndroid,\n\t\tCondition: \"'dogs' in topics || 'cats' in topics\",\n\t}\n\n\terr = CheckMessage(req)\n\trequire.NoError(t, err)\n\n\t// the message may specify at most 1000 registration IDs\n\treq = &PushNotification{\n\t\tMessage:  \"Test\",\n\t\tPlatform: core.PlatFormAndroid,\n\t\tTokens:   make([]string, 501),\n\t}\n\n\terr = CheckMessage(req)\n\trequire.Error(t, err)\n\n\t// Pass\n\treq = &PushNotification{\n\t\tMessage:  \"Test\",\n\t\tPlatform: core.PlatFormAndroid,\n\t\tTokens:   []string{\"XXXXXXXXX\"},\n\t}\n\n\terr = CheckMessage(req)\n\trequire.NoError(t, err)\n}\n\nfunc TestAndroidNotificationStructure(t *testing.T) {\n\ttest := \"test\"\n\treq := &PushNotification{\n\t\tTokens:         []string{\"a\", \"b\"},\n\t\tMessage:        \"Welcome\",\n\t\tTo:             test,\n\t\tPriority:       HIGH,\n\t\tMutableContent: true,\n\t\tTitle:          test,\n\t\tSound:          test,\n\t\tData: D{\n\t\t\t\"a\": \"1\",\n\t\t\t\"b\": 2,\n\t\t\t\"json\": map[string]any{\n\t\t\t\t\"c\": \"3\",\n\t\t\t\t\"d\": 4,\n\t\t\t},\n\t\t},\n\t\tNotification: &messaging.Notification{\n\t\t\tTitle: test,\n\t\t\tBody:  \"\",\n\t\t},\n\t}\n\n\tmessages := GetAndroidNotification(req)\n\n\tassert.Equal(t, test, messages[0].Notification.Title)\n\tassert.Equal(t, \"Welcome\", messages[0].Notification.Body)\n\tassert.Equal(t, \"1\", messages[0].Data[\"a\"])\n\tassert.Equal(t, \"2\", messages[0].Data[\"b\"])\n\tassert.JSONEq(t, `{\"c\":\"3\",\"d\":4}`, messages[0].Data[\"json\"])\n\tassert.NotNil(t, messages[0].APNS)\n\tassert.Equal(t, req.Sound, messages[0].APNS.Payload.Aps.Sound)\n\tassert.Equal(t, req.MutableContent, messages[0].APNS.Payload.Aps.MutableContent)\n\n\t// test empty body\n\treq = &PushNotification{\n\t\tTokens: []string{\"a\", \"b\"},\n\t\tTo:     test,\n\t\tNotification: &messaging.Notification{\n\t\t\tBody: \"\",\n\t\t},\n\t}\n\tmessages = GetAndroidNotification(req)\n\n\tassert.Empty(t, messages[0].Notification.Body)\n}\n\nfunc TestAndroidBackgroundNotificationStructure(t *testing.T) {\n\tdata := map[string]any{\n\t\t\"a\": \"1\",\n\t\t\"b\": 2,\n\t\t\"json\": map[string]any{\n\t\t\t\"c\": \"3\",\n\t\t\t\"d\": 4,\n\t\t},\n\t}\n\treq := &PushNotification{\n\t\tTokens:           []string{\"a\", \"b\"},\n\t\tPriority:         HIGH,\n\t\tContentAvailable: true,\n\t\tData:             data,\n\t}\n\n\tmessages := GetAndroidNotification(req)\n\n\tassert.Equal(t, \"1\", messages[0].Data[\"a\"])\n\tassert.Equal(t, \"2\", messages[0].Data[\"b\"])\n\tassert.JSONEq(t, `{\"c\":\"3\",\"d\":4}`, messages[0].Data[\"json\"])\n\tassert.NotNil(t, messages[0].APNS)\n\tassert.Equal(t, req.ContentAvailable, messages[0].APNS.Payload.Aps.ContentAvailable)\n\tassert.True(t, reflect.DeepEqual(data, messages[0].APNS.Payload.Aps.CustomData))\n}\n\n// Tests for refactored helper functions\n\nfunc TestSetupFCMNotification(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\treq         *PushNotification\n\t\twantTitle   string\n\t\twantBody    string\n\t\twantImage   string\n\t\twantMutable bool\n\t}{\n\t\t{\n\t\t\tname: \"title message and image\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens:  []string{\"token\"},\n\t\t\t\tTitle:   \"Test Title\",\n\t\t\t\tMessage: \"Test Message\",\n\t\t\t\tImage:   \"https://example.com/image.png\",\n\t\t\t},\n\t\t\twantTitle: \"Test Title\",\n\t\t\twantBody:  \"Test Message\",\n\t\t\twantImage: \"https://example.com/image.png\",\n\t\t},\n\t\t{\n\t\t\tname: \"mutable content\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens:         []string{\"token\"},\n\t\t\t\tTitle:          \"Title\",\n\t\t\t\tMutableContent: true,\n\t\t\t},\n\t\t\twantTitle:   \"Title\",\n\t\t\twantMutable: true,\n\t\t},\n\t\t{\n\t\t\tname: \"empty notification fields\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens: []string{\"token\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmessages := GetAndroidNotification(tt.req)\n\t\t\tif len(messages) > 0 {\n\t\t\t\tmsg := messages[0]\n\t\t\t\tif tt.wantTitle != \"\" {\n\t\t\t\t\tassert.Equal(t, tt.wantTitle, msg.Notification.Title)\n\t\t\t\t}\n\t\t\t\tif tt.wantBody != \"\" {\n\t\t\t\t\tassert.Equal(t, tt.wantBody, msg.Notification.Body)\n\t\t\t\t}\n\t\t\t\tif tt.wantImage != \"\" {\n\t\t\t\t\tassert.Equal(t, tt.wantImage, msg.Notification.ImageURL)\n\t\t\t\t}\n\t\t\t\tif tt.wantMutable {\n\t\t\t\t\tassert.NotNil(t, msg.APNS)\n\t\t\t\t\tassert.True(t, msg.APNS.Payload.Aps.MutableContent)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetupFCMContentAvailable(t *testing.T) {\n\tdata := D{\"key\": \"value\"}\n\treq := &PushNotification{\n\t\tTokens:           []string{\"token\"},\n\t\tContentAvailable: true,\n\t\tData:             data,\n\t}\n\n\tmessages := GetAndroidNotification(req)\n\tassert.Len(t, messages, 1)\n\tmsg := messages[0]\n\n\tassert.NotNil(t, msg.APNS)\n\tassert.Equal(t, \"5\", msg.APNS.Headers[\"apns-priority\"])\n\tassert.True(t, msg.APNS.Payload.Aps.ContentAvailable)\n\tassert.Equal(t, data, D(msg.APNS.Payload.Aps.CustomData))\n}\n\nfunc TestSetAPNSSound(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\treq       *PushNotification\n\t\twantSound string\n\t}{\n\t\t{\n\t\t\tname: \"sound with no existing APNS\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens: []string{\"token\"},\n\t\t\t\tSound:  \"default\",\n\t\t\t},\n\t\t\twantSound: \"default\",\n\t\t},\n\t\t{\n\t\t\tname: \"sound with existing APNS from mutable content\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens:         []string{\"token\"},\n\t\t\t\tTitle:          \"Title\",\n\t\t\t\tMutableContent: true,\n\t\t\t\tSound:          \"custom.aiff\",\n\t\t\t},\n\t\t\twantSound: \"custom.aiff\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmessages := GetAndroidNotification(tt.req)\n\t\t\tif len(messages) > 0 {\n\t\t\t\tmsg := messages[0]\n\t\t\t\tassert.NotNil(t, msg.APNS)\n\t\t\t\tassert.Equal(t, tt.wantSound, msg.APNS.Payload.Aps.Sound)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetupFCMSound(t *testing.T) {\n\treq := &PushNotification{\n\t\tTokens:   []string{\"token\"},\n\t\tSound:    \"test.aiff\",\n\t\tPriority: HIGH,\n\t}\n\n\tmessages := GetAndroidNotification(req)\n\tassert.Len(t, messages, 1)\n\tmsg := messages[0]\n\n\t// Check APNS sound is set\n\tassert.NotNil(t, msg.APNS)\n\tassert.Equal(t, \"test.aiff\", msg.APNS.Payload.Aps.Sound)\n\n\t// Check Android config is set with sound\n\tassert.NotNil(t, msg.Android)\n\tassert.Equal(t, \"test.aiff\", msg.Android.Notification.Sound)\n\tassert.Equal(t, HIGH, msg.Android.Priority)\n}\n\nfunc TestConvertDataToStringMap(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata D\n\t\twant map[string]string\n\t}{\n\t\t{\n\t\t\tname: \"empty data\",\n\t\t\tdata: D{},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"string values\",\n\t\t\tdata: D{\"key1\": \"value1\", \"key2\": \"value2\"},\n\t\t\twant: map[string]string{\"key1\": \"value1\", \"key2\": \"value2\"},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed values\",\n\t\t\tdata: D{\"str\": \"text\", \"num\": 42, \"bool\": true},\n\t\t\twant: map[string]string{\"str\": \"text\", \"num\": \"42\", \"bool\": \"true\"},\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tdata: D{\"nested\": map[string]any{\"a\": 1}},\n\t\t\twant: map[string]string{\"nested\": `{\"a\":1}`},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := convertDataToStringMap(tt.data)\n\t\t\tif tt.want == nil {\n\t\t\t\tassert.Nil(t, got)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBuildFCMMessage(t *testing.T) {\n\treq := &PushNotification{\n\t\tTokens: []string{\"token\"},\n\t\tTitle:  \"Title\",\n\t\tNotification: &messaging.Notification{\n\t\t\tTitle: \"Notification Title\",\n\t\t\tBody:  \"Notification Body\",\n\t\t},\n\t\tAndroid: &messaging.AndroidConfig{\n\t\t\tPriority: HIGH,\n\t\t},\n\t\tWebpush: &messaging.WebpushConfig{\n\t\t\tHeaders: map[string]string{\"TTL\": \"3600\"},\n\t\t},\n\t\tFCMOptions: &messaging.FCMOptions{\n\t\t\tAnalyticsLabel: \"test-label\",\n\t\t},\n\t}\n\n\tdata := map[string]string{\"key\": \"value\"}\n\tmsg := buildFCMMessage(req, data)\n\n\tassert.Equal(t, req.Notification, msg.Notification)\n\tassert.Equal(t, req.Android, msg.Android)\n\tassert.Equal(t, req.Webpush, msg.Webpush)\n\tassert.Equal(t, req.FCMOptions, msg.FCMOptions)\n\tassert.Equal(t, data, msg.Data)\n}\n\nfunc TestGetAndroidNotificationWithTopic(t *testing.T) {\n\treq := &PushNotification{\n\t\tPlatform:  core.PlatFormAndroid,\n\t\tTopic:     \"test-topic\",\n\t\tCondition: \"'dogs' in topics\",\n\t\tMessage:   \"Topic message\",\n\t}\n\n\tmessages := GetAndroidNotification(req)\n\tassert.Len(t, messages, 1)\n\tassert.Equal(t, \"test-topic\", messages[0].Topic)\n\tassert.Equal(t, \"'dogs' in topics\", messages[0].Condition)\n}\n\nfunc TestGetAndroidNotificationWithTokens(t *testing.T) {\n\treq := &PushNotification{\n\t\tTokens:  []string{\"token1\", \"token2\", \"token3\"},\n\t\tMessage: \"Multi token message\",\n\t}\n\n\tmessages := GetAndroidNotification(req)\n\tassert.Len(t, messages, 3)\n\tassert.Equal(t, \"token1\", messages[0].Token)\n\tassert.Equal(t, \"token2\", messages[1].Token)\n\tassert.Equal(t, \"token3\", messages[2].Token)\n}\n\nfunc TestGetAndroidNotificationWithTopicAndTokens(t *testing.T) {\n\treq := &PushNotification{\n\t\tPlatform: core.PlatFormAndroid,\n\t\tTopic:    \"test-topic\",\n\t\tTokens:   []string{\"token1\", \"token2\"},\n\t\tMessage:  \"Combined message\",\n\t}\n\n\tmessages := GetAndroidNotification(req)\n\t// 1 for topic + 2 for tokens = 3 messages\n\tassert.Len(t, messages, 3)\n\tassert.Equal(t, \"test-topic\", messages[0].Topic)\n\tassert.Equal(t, \"token1\", messages[1].Token)\n\tassert.Equal(t, \"token2\", messages[2].Token)\n}\n"
  },
  {
    "path": "notify/notification_hms.go",
    "content": "package notify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/status\"\n\n\tc \"github.com/appleboy/go-hms-push/push/config\"\n\tclient \"github.com/appleboy/go-hms-push/push/core\"\n\t\"github.com/appleboy/go-hms-push/push/model\"\n)\n\nvar (\n\tpushError  error\n\tpushClient *client.HMSClient\n\tonce       sync.Once\n)\n\n// GetPushClient use for create HMS Push.\nfunc GetPushClient(conf *c.Config) (*client.HMSClient, error) {\n\tonce.Do(func() {\n\t\tclient, err := client.NewHttpClient(conf)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tpushClient = client\n\t\tpushError = err\n\t})\n\n\treturn pushClient, pushError\n}\n\n// InitHMSClient use for initialize HMS Client.\nfunc InitHMSClient(cfg *config.ConfYaml, appSecret, appID string) (*client.HMSClient, error) {\n\tif appSecret == \"\" {\n\t\treturn nil, errors.New(\"missing huawei app secret\")\n\t}\n\n\tif appID == \"\" {\n\t\treturn nil, errors.New(\"missing huawei app id\")\n\t}\n\n\tconf := &c.Config{\n\t\tAppId:     appID,\n\t\tAppSecret: appSecret,\n\t\tAuthUrl:   \"https://oauth-login.cloud.huawei.com/oauth2/v3/token\",\n\t\tPushUrl:   \"https://push-api.cloud.huawei.com\",\n\t}\n\n\tif appSecret != cfg.Huawei.AppSecret || appID != cfg.Huawei.AppID {\n\t\treturn GetPushClient(conf)\n\t}\n\n\tif HMSClient == nil {\n\t\treturn GetPushClient(conf)\n\t}\n\n\treturn HMSClient, nil\n}\n\n// setHuaweiMessageTarget sets the target (tokens, topic, condition) on the message.\nfunc setHuaweiMessageTarget(msg *model.Message, req *PushNotification) {\n\tif len(req.Tokens) > 0 {\n\t\tmsg.Token = req.Tokens\n\t}\n\tif len(req.Topic) > 0 {\n\t\tmsg.Topic = req.Topic\n\t}\n\tif len(req.Condition) > 0 {\n\t\tmsg.Condition = req.Condition\n\t}\n}\n\n// setHuaweiAndroidConfig sets Android-specific configuration on the message.\nfunc setHuaweiAndroidConfig(android *model.AndroidConfig, req *PushNotification) {\n\tif req.Priority == HIGH {\n\t\tandroid.Urgency = \"HIGH\"\n\t}\n\tandroid.CollapseKey = req.HuaweiCollapseKey\n\tif len(req.Category) > 0 {\n\t\tandroid.Category = req.Category\n\t}\n\tif len(req.HuaweiTTL) > 0 {\n\t\tandroid.TTL = req.HuaweiTTL\n\t}\n\tif len(req.BiTag) > 0 {\n\t\tandroid.BiTag = req.BiTag\n\t}\n\tandroid.FastAppTarget = req.FastAppTarget\n}\n\n// setHuaweiNotificationContent sets the notification content fields.\nfunc setHuaweiNotificationContent(android *model.AndroidConfig, req *PushNotification) {\n\tensureNotification := func() {\n\t\tif android.Notification == nil {\n\t\t\tandroid.Notification = model.GetDefaultAndroidNotification()\n\t\t}\n\t}\n\n\tif len(req.Message) > 0 {\n\t\tensureNotification()\n\t\tandroid.Notification.Body = req.Message\n\t}\n\tif len(req.Title) > 0 {\n\t\tensureNotification()\n\t\tandroid.Notification.Title = req.Title\n\t}\n\tif len(req.Image) > 0 {\n\t\tensureNotification()\n\t\tandroid.Notification.Image = req.Image\n\t}\n\tif v, ok := req.Sound.(string); ok && len(v) > 0 {\n\t\tensureNotification()\n\t\tandroid.Notification.Sound = v\n\t} else if android.Notification != nil {\n\t\tandroid.Notification.DefaultSound = true\n\t}\n}\n\n// GetHuaweiNotification use for define HMS notification.\n// HTTP Connection Server Reference for HMS\n// https://developer.huawei.com/consumer/en/doc/development/HMS-References/push-sendapi\nfunc GetHuaweiNotification(req *PushNotification) (*model.MessageRequest, error) {\n\tmsgRequest := model.NewNotificationMsgRequest()\n\tmsgRequest.Message.Android = model.GetDefaultAndroid()\n\n\tsetHuaweiMessageTarget(msgRequest.Message, req)\n\tsetHuaweiAndroidConfig(msgRequest.Message.Android, req)\n\n\tif len(req.HuaweiData) > 0 {\n\t\tmsgRequest.Message.Data = req.HuaweiData\n\t}\n\n\tif req.HuaweiNotification != nil {\n\t\tmsgRequest.Message.Android.Notification = req.HuaweiNotification\n\t\tif msgRequest.Message.Android.Notification.ClickAction == nil {\n\t\t\tmsgRequest.Message.Android.Notification.ClickAction = model.GetDefaultClickAction()\n\t\t}\n\t}\n\n\tsetHuaweiNotificationContent(msgRequest.Message.Android, req)\n\n\tb, err := json.Marshal(msgRequest)\n\tif err != nil {\n\t\tlogx.LogError.Error(\"Failed to marshal the default message! Error is \" + err.Error())\n\t\treturn nil, err\n\t}\n\n\tlogx.LogAccess.Debugf(\"Default message is %s\", string(b))\n\treturn msgRequest, nil\n}\n\n// PushToHuawei provide send notification to Android server.\nfunc PushToHuawei(\n\tctx context.Context,\n\treq *PushNotification,\n\tcfg *config.ConfYaml,\n) (resp *ResponsePush, err error) {\n\tlogx.LogAccess.Debug(\"Start push notification for Huawei\")\n\n\tvar (\n\t\tclient     *client.HMSClient\n\t\tretryCount = 0\n\t\tmaxRetry   = cfg.Huawei.MaxRetry\n\t)\n\n\tif req.Retry > 0 && req.Retry < maxRetry {\n\t\tmaxRetry = req.Retry\n\t}\n\n\t// check message\n\terr = CheckMessage(req)\n\tif err != nil {\n\t\tlogx.LogError.Error(\"request error: \" + err.Error())\n\t\treturn nil, err\n\t}\n\n\tclient, err = InitHMSClient(cfg, cfg.Huawei.AppSecret, cfg.Huawei.AppID)\n\tif err != nil {\n\t\t// HMS server error\n\t\tlogx.LogError.Error(\"HMS server error: \" + err.Error())\n\t\treturn nil, err\n\t}\n\n\tresp = &ResponsePush{}\n\nRetry:\n\tisError := false\n\n\tnotification, _ := GetHuaweiNotification(req)\n\n\tres, err := client.SendMessage(ctx, notification)\n\tif err != nil {\n\t\t// Send Message error\n\t\terrLog := logPush(cfg, core.FailedPush, req.Topic, req, err)\n\t\tresp.Logs = append(resp.Logs, errLog)\n\t\tlogx.LogError.Error(\"HMS server send message error: \" + err.Error())\n\t\treturn resp, err\n\t}\n\n\t// Huawei Push Send API does not support exact results for each token\n\tif res.Code == \"80000000\" {\n\t\tstatus.StatStorage.AddHuaweiSuccess(int64(1))\n\t\tlogx.LogAccess.Debug(\"Huwaei Send Notification is completed successfully!\")\n\t} else {\n\t\tisError = true\n\t\tstatus.StatStorage.AddHuaweiError(int64(1))\n\t\tlogx.LogAccess.Debug(\"Huawei Send Notification is failed! Code: \" + res.Code)\n\t}\n\n\tif isError && retryCount < maxRetry {\n\t\tretryCount++\n\n\t\t// resend all tokens\n\t\tgoto Retry\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "notify/notification_hms_test.go",
    "content": "package notify\n\nimport (\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/config\"\n\n\t\"github.com/appleboy/go-hms-push/push/model\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMissingHuaweiAppSecret(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Android.Enabled = false\n\tcfg.Huawei.Enabled = true\n\tcfg.Huawei.AppSecret = \"\"\n\n\terr := CheckPushConf(cfg)\n\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing huawei app secret\", err.Error())\n}\n\nfunc TestMissingHuaweiAppID(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Android.Enabled = false\n\tcfg.Huawei.Enabled = true\n\tcfg.Huawei.AppID = \"\"\n\n\terr := CheckPushConf(cfg)\n\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing huawei app id\", err.Error())\n}\n\nfunc TestMissingAppSecretForInitHMSClient(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tclient, err := InitHMSClient(cfg, \"\", \"APP_SECRET\")\n\n\tassert.Nil(t, client)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing huawei app secret\", err.Error())\n}\n\nfunc TestMissingAppIDForInitHMSClient(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tclient, err := InitHMSClient(cfg, \"APP_ID\", \"\")\n\n\tassert.Nil(t, client)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing huawei app id\", err.Error())\n}\n\n// Tests for refactored helper functions\n\nfunc TestSetHuaweiMessageTarget(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\treq           *PushNotification\n\t\twantTokens    []string\n\t\twantTopic     string\n\t\twantCondition string\n\t}{\n\t\t{\n\t\t\tname: \"tokens only\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens: []string{\"token1\", \"token2\"},\n\t\t\t},\n\t\t\twantTokens: []string{\"token1\", \"token2\"},\n\t\t},\n\t\t{\n\t\t\tname: \"topic only\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTopic: \"test-topic\",\n\t\t\t},\n\t\t\twantTopic: \"test-topic\",\n\t\t},\n\t\t{\n\t\t\tname: \"condition only\",\n\t\t\treq: &PushNotification{\n\t\t\t\tCondition: \"'dogs' in topics\",\n\t\t\t},\n\t\t\twantCondition: \"'dogs' in topics\",\n\t\t},\n\t\t{\n\t\t\tname: \"all fields\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens:    []string{\"token1\"},\n\t\t\t\tTopic:     \"topic\",\n\t\t\t\tCondition: \"condition\",\n\t\t\t},\n\t\t\twantTokens:    []string{\"token1\"},\n\t\t\twantTopic:     \"topic\",\n\t\t\twantCondition: \"condition\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmsgRequest, err := GetHuaweiNotification(tt.req)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif len(tt.wantTokens) > 0 {\n\t\t\t\tassert.Equal(t, tt.wantTokens, msgRequest.Message.Token)\n\t\t\t}\n\t\t\tif tt.wantTopic != \"\" {\n\t\t\t\tassert.Equal(t, tt.wantTopic, msgRequest.Message.Topic)\n\t\t\t}\n\t\t\tif tt.wantCondition != \"\" {\n\t\t\t\tassert.Equal(t, tt.wantCondition, msgRequest.Message.Condition)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetHuaweiAndroidConfig(t *testing.T) {\n\tcollapseKey := 1\n\treq := &PushNotification{\n\t\tTokens:            []string{\"token\"},\n\t\tPriority:          HIGH,\n\t\tHuaweiCollapseKey: collapseKey,\n\t\tCategory:          \"test-category\",\n\t\tHuaweiTTL:         \"86400s\",\n\t\tBiTag:             \"bi-tag-123\",\n\t\tFastAppTarget:     1,\n\t}\n\n\tmsgRequest, err := GetHuaweiNotification(req)\n\trequire.NoError(t, err)\n\n\tandroid := msgRequest.Message.Android\n\tassert.Equal(t, \"HIGH\", android.Urgency)\n\tassert.Equal(t, collapseKey, android.CollapseKey)\n\tassert.Equal(t, \"test-category\", android.Category)\n\tassert.Equal(t, \"86400s\", android.TTL)\n\tassert.Equal(t, \"bi-tag-123\", android.BiTag)\n\tassert.Equal(t, 1, android.FastAppTarget)\n}\n\nfunc TestSetHuaweiNotificationContent(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\treq       *PushNotification\n\t\twantTitle string\n\t\twantBody  string\n\t\twantImage string\n\t\twantSound string\n\t}{\n\t\t{\n\t\t\tname: \"title and message\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens:  []string{\"token\"},\n\t\t\t\tTitle:   \"Test Title\",\n\t\t\t\tMessage: \"Test Message\",\n\t\t\t},\n\t\t\twantTitle: \"Test Title\",\n\t\t\twantBody:  \"Test Message\",\n\t\t},\n\t\t{\n\t\t\tname: \"image\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens: []string{\"token\"},\n\t\t\t\tImage:  \"https://example.com/image.png\",\n\t\t\t},\n\t\t\twantImage: \"https://example.com/image.png\",\n\t\t},\n\t\t{\n\t\t\tname: \"custom sound\",\n\t\t\treq: &PushNotification{\n\t\t\t\tTokens: []string{\"token\"},\n\t\t\t\tSound:  \"custom.mp3\",\n\t\t\t},\n\t\t\twantSound: \"custom.mp3\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmsgRequest, err := GetHuaweiNotification(tt.req)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tnotification := msgRequest.Message.Android.Notification\n\t\t\tif tt.wantTitle != \"\" {\n\t\t\t\tassert.Equal(t, tt.wantTitle, notification.Title)\n\t\t\t}\n\t\t\tif tt.wantBody != \"\" {\n\t\t\t\tassert.Equal(t, tt.wantBody, notification.Body)\n\t\t\t}\n\t\t\tif tt.wantImage != \"\" {\n\t\t\t\tassert.Equal(t, tt.wantImage, notification.Image)\n\t\t\t}\n\t\t\tif tt.wantSound != \"\" {\n\t\t\t\tassert.Equal(t, tt.wantSound, notification.Sound)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHuaweiNotificationDefaultSound(t *testing.T) {\n\treq := &PushNotification{\n\t\tTokens:  []string{\"token\"},\n\t\tTitle:   \"Title\",\n\t\tMessage: \"Message\",\n\t\t// No sound specified\n\t}\n\n\tmsgRequest, err := GetHuaweiNotification(req)\n\trequire.NoError(t, err)\n\n\tnotification := msgRequest.Message.Android.Notification\n\tassert.True(t, notification.DefaultSound)\n}\n\nfunc TestHuaweiNotificationWithData(t *testing.T) {\n\treq := &PushNotification{\n\t\tTokens:     []string{\"token\"},\n\t\tHuaweiData: `{\"key\":\"value\"}`,\n\t}\n\n\tmsgRequest, err := GetHuaweiNotification(req)\n\trequire.NoError(t, err)\n\n\tassert.JSONEq(t, `{\"key\":\"value\"}`, msgRequest.Message.Data)\n}\n\nfunc TestHuaweiNotificationWithCustomNotification(t *testing.T) {\n\treq := &PushNotification{\n\t\tTokens: []string{\"token\"},\n\t\tHuaweiNotification: &model.AndroidNotification{\n\t\t\tTitle: \"Custom Title\",\n\t\t\tBody:  \"Custom Body\",\n\t\t\tImage: \"custom-image.png\",\n\t\t},\n\t}\n\n\tmsgRequest, err := GetHuaweiNotification(req)\n\trequire.NoError(t, err)\n\n\tnotification := msgRequest.Message.Android.Notification\n\tassert.Equal(t, \"Custom Title\", notification.Title)\n\tassert.Equal(t, \"Custom Body\", notification.Body)\n\tassert.Equal(t, \"custom-image.png\", notification.Image)\n\t// ClickAction should be set to default\n\tassert.NotNil(t, notification.ClickAction)\n}\n"
  },
  {
    "path": "notify/notification_test.go",
    "content": "package notify\n\nimport (\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/config\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestHuaweiAppID     = \"app-id\"\n\ttestHuaweiAppSecret = \"app-secret\"\n)\n\nfunc TestCorrectConf(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = \"xxxxx\"\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPath\n\n\terr := CheckPushConf(cfg)\n\n\trequire.NoError(t, err)\n}\n\nfunc TestSetProxyURL(t *testing.T) {\n\terr := SetProxy(\"87.236.233.92:8080\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"parse \\\"87.236.233.92:8080\\\": invalid URI for request\", err.Error())\n\n\terr = SetProxy(\"a.html\")\n\trequire.Error(t, err)\n\n\terr = SetProxy(\"http://87.236.233.92:8080\")\n\trequire.NoError(t, err)\n}\n\n// Tests for refactored helper functions\n\nfunc TestCheckIOSConf(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\t// iOS disabled - should pass\n\tcfg.Ios.Enabled = false\n\terr := checkIOSConf(cfg)\n\trequire.NoError(t, err)\n\n\t// iOS enabled but missing key path and base64\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = \"\"\n\tcfg.Ios.KeyBase64 = \"\"\n\terr = checkIOSConf(cfg)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing iOS certificate key\", err.Error())\n\n\t// iOS enabled with valid key path\n\tcfg.Ios.KeyPath = testKeyPath\n\terr = checkIOSConf(cfg)\n\trequire.NoError(t, err)\n\n\t// iOS enabled with key base64 (no key path)\n\tcfg.Ios.KeyPath = \"\"\n\tcfg.Ios.KeyBase64 = \"some-base64-data\"\n\terr = checkIOSConf(cfg)\n\trequire.NoError(t, err)\n\n\t// iOS enabled with non-existent file\n\tcfg.Ios.KeyPath = \"non-existent-file.pem\"\n\tcfg.Ios.KeyBase64 = \"\"\n\terr = checkIOSConf(cfg)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"certificate file does not exist\", err.Error())\n}\n\nfunc TestCheckAndroidConf(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\t// Android disabled - should pass\n\tcfg.Android.Enabled = false\n\terr := checkAndroidConf(cfg)\n\trequire.NoError(t, err)\n\n\t// Android enabled but no credentials\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = \"\"\n\tcfg.Android.KeyPath = \"\"\n\t// Clear environment variable for this test\n\tt.Setenv(\"GOOGLE_APPLICATION_CREDENTIALS\", \"\")\n\terr = checkAndroidConf(cfg)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing fcm credential data\", err.Error())\n\n\t// Android enabled with credential\n\tcfg.Android.Credential = \"some-credential\"\n\terr = checkAndroidConf(cfg)\n\trequire.NoError(t, err)\n\n\t// Android enabled with key path\n\tcfg.Android.Credential = \"\"\n\tcfg.Android.KeyPath = \"/path/to/key.json\"\n\terr = checkAndroidConf(cfg)\n\trequire.NoError(t, err)\n}\n\nfunc TestCheckHuaweiConf(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\t// Huawei disabled - should pass\n\tcfg.Huawei.Enabled = false\n\terr := checkHuaweiConf(cfg)\n\trequire.NoError(t, err)\n\n\t// Huawei enabled but missing app secret\n\tcfg.Huawei.Enabled = true\n\tcfg.Huawei.AppSecret = \"\"\n\tcfg.Huawei.AppID = testHuaweiAppID\n\terr = checkHuaweiConf(cfg)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing huawei app secret\", err.Error())\n\n\t// Huawei enabled but missing app id\n\tcfg.Huawei.AppSecret = testHuaweiAppSecret\n\tcfg.Huawei.AppID = \"\"\n\terr = checkHuaweiConf(cfg)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"missing huawei app id\", err.Error())\n\n\t// Huawei enabled with all credentials\n\tcfg.Huawei.AppSecret = testHuaweiAppSecret\n\tcfg.Huawei.AppID = testHuaweiAppID\n\terr = checkHuaweiConf(cfg)\n\trequire.NoError(t, err)\n}\n\nfunc TestCheckPushConfNoPlatformEnabled(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = false\n\tcfg.Android.Enabled = false\n\tcfg.Huawei.Enabled = false\n\n\terr := CheckPushConf(cfg)\n\trequire.Error(t, err)\n\tassert.Equal(t, \"please enable iOS, Android or Huawei config in yml config\", err.Error())\n}\n\nfunc TestCheckPushConfAllPlatformsValid(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPath\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = \"some-credential\"\n\n\tcfg.Huawei.Enabled = true\n\tcfg.Huawei.AppSecret = testHuaweiAppSecret\n\tcfg.Huawei.AppID = testHuaweiAppID\n\n\terr := CheckPushConf(cfg)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "router/server.go",
    "content": "package router\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/metric\"\n\t\"github.com/appleboy/gorush/notify\"\n\t\"github.com/appleboy/gorush/status\"\n\n\tapi \"github.com/appleboy/gin-status-api\"\n\t\"github.com/gin-contrib/logger\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gin-gonic/gin/binding\"\n\t\"github.com/golang-queue/queue\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"github.com/thoas/stats\"\n\t\"golang.org/x/crypto/acme/autocert\"\n)\n\nvar doOnce sync.Once\n\nfunc abortWithError(c *gin.Context, code int, message string) {\n\tc.AbortWithStatusJSON(code, gin.H{\n\t\t\"code\":    code,\n\t\t\"message\": message,\n\t})\n}\n\nfunc rootHandler(c *gin.Context) {\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"text\": \"Welcome to notification server.\",\n\t})\n}\n\nfunc heartbeatHandler(c *gin.Context) {\n\tc.AbortWithStatus(http.StatusOK)\n}\n\nfunc versionHandler(c *gin.Context) {\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"source\":  \"https://github.com/appleboy/gorush\",\n\t\t\"version\": GetVersion(),\n\t})\n}\n\nfunc pushHandler(cfg *config.ConfYaml, q *queue.Queue) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tvar form notify.RequestPush\n\t\tvar msg string\n\n\t\tif err := c.ShouldBindWith(&form, binding.JSON); err != nil {\n\t\t\tmsg = \"Missing notifications field.\"\n\t\t\tlogx.LogAccess.Debug(err)\n\t\t\tabortWithError(c, http.StatusBadRequest, msg)\n\t\t\treturn\n\t\t}\n\n\t\tif len(form.Notifications) == 0 {\n\t\t\tmsg = \"Notifications field is empty.\"\n\t\t\tlogx.LogAccess.Debug(msg)\n\t\t\tabortWithError(c, http.StatusBadRequest, msg)\n\t\t\treturn\n\t\t}\n\n\t\tif int64(len(form.Notifications)) > cfg.Core.MaxNotification {\n\t\t\tmsg = fmt.Sprintf(\n\t\t\t\t\"Number of notifications(%d) over limit(%d)\",\n\t\t\t\tlen(form.Notifications),\n\t\t\t\tcfg.Core.MaxNotification,\n\t\t\t)\n\t\t\tlogx.LogAccess.Debug(msg)\n\t\t\tabortWithError(c, http.StatusBadRequest, msg)\n\t\t\treturn\n\t\t}\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tgo func() {\n\t\t\t// Deprecated: the CloseNotifier interface predates Go's context package.\n\t\t\t// New code should use Request.Context instead.\n\t\t\t// Change to context package\n\t\t\t<-c.Request.Context().Done()\n\t\t\t// Don't send notification after client timeout or disconnected.\n\t\t\t// See the following issue for detail information.\n\t\t\t// https://github.com/appleboy/gorush/issues/422\n\t\t\tif cfg.Core.Sync {\n\t\t\t\tcancel()\n\t\t\t}\n\t\t}()\n\n\t\tcounts, logs := handleNotification(ctx, cfg, form, q)\n\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"success\": \"ok\",\n\t\t\t\"counts\":  counts,\n\t\t\t\"logs\":    logs,\n\t\t})\n\t}\n}\n\nfunc configHandler(cfg *config.ConfYaml) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc.YAML(http.StatusOK, cfg.SanitizedCopy())\n\t}\n}\n\nfunc metricsHandler(c *gin.Context) {\n\tpromhttp.Handler().ServeHTTP(c.Writer, c.Request)\n}\n\nfunc appStatusHandler(q *queue.Queue) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tresult := status.App{}\n\n\t\tresult.Version = GetVersion()\n\t\tresult.BusyWorkers = q.BusyWorkers()\n\t\tresult.SuccessTasks = q.SuccessTasks()\n\t\tresult.FailureTasks = q.FailureTasks()\n\t\tresult.SubmittedTasks = q.SubmittedTasks()\n\t\tresult.TotalCount = status.StatStorage.GetTotalCount()\n\t\tresult.Ios.PushSuccess = status.StatStorage.GetIosSuccess()\n\t\tresult.Ios.PushError = status.StatStorage.GetIosError()\n\t\tresult.Android.PushSuccess = status.StatStorage.GetAndroidSuccess()\n\t\tresult.Android.PushError = status.StatStorage.GetAndroidError()\n\t\tresult.Huawei.PushSuccess = status.StatStorage.GetHuaweiSuccess()\n\t\tresult.Huawei.PushError = status.StatStorage.GetHuaweiError()\n\n\t\tc.JSON(http.StatusOK, result)\n\t}\n}\n\nfunc sysStatsHandler() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc.JSON(http.StatusOK, status.Stats.Data())\n\t}\n}\n\n// StatMiddleware response time, status code count, etc.\nfunc StatMiddleware() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tbeginning, recorder := status.Stats.Begin(c.Writer)\n\t\tc.Next()\n\t\tstatus.Stats.End(beginning, stats.WithRecorder(recorder))\n\t}\n}\n\nfunc autoTLSServer(cfg *config.ConfYaml, q *queue.Queue) *http.Server {\n\tm := autocert.Manager{\n\t\tPrompt:     autocert.AcceptTOS,\n\t\tHostPolicy: autocert.HostWhitelist(cfg.Core.AutoTLS.Host),\n\t\tCache:      autocert.DirCache(cfg.Core.AutoTLS.Folder),\n\t}\n\n\t//nolint:gosec // TLS MinVersion is managed by autocert, not manually configured\n\treturn &http.Server{\n\t\tAddr:      \":https\",\n\t\tTLSConfig: &tls.Config{GetCertificate: m.GetCertificate},\n\t\tHandler:   routerEngine(cfg, q),\n\t}\n}\n\nfunc routerEngine(cfg *config.ConfYaml, q *queue.Queue) *gin.Engine {\n\tzerolog.SetGlobalLevel(zerolog.InfoLevel)\n\tif cfg.Core.Mode == \"debug\" {\n\t\tzerolog.SetGlobalLevel(zerolog.DebugLevel)\n\t}\n\n\tlog.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()\n\n\tisTerm := isatty.IsTerminal(os.Stdout.Fd())\n\tif isTerm {\n\t\tlog.Logger = log.Output(\n\t\t\tzerolog.ConsoleWriter{\n\t\t\t\tOut:     os.Stdout,\n\t\t\t\tNoColor: false,\n\t\t\t},\n\t\t)\n\t}\n\n\t// Support metrics\n\tdoOnce.Do(func() {\n\t\tm := metric.NewMetrics(q)\n\t\tprometheus.MustRegister(m)\n\t})\n\n\t// set server mode\n\tgin.SetMode(cfg.Core.Mode)\n\n\tr := gin.New()\n\n\t// Global middleware\n\tr.Use(logger.SetLogger(\n\t\tlogger.WithUTC(true),\n\t\tlogger.WithSkipPath([]string{\n\t\t\tcfg.API.HealthURI,\n\t\t\tcfg.API.MetricURI,\n\t\t}),\n\t))\n\tr.Use(gin.Recovery())\n\tr.Use(VersionMiddleware())\n\tr.Use(StatMiddleware())\n\n\tr.GET(cfg.API.StatGoURI, api.GinHandler)\n\tr.GET(cfg.API.StatAppURI, appStatusHandler(q))\n\tr.GET(cfg.API.ConfigURI, configHandler(cfg))\n\tr.GET(cfg.API.SysStatURI, sysStatsHandler())\n\tr.POST(cfg.API.PushURI, pushHandler(cfg, q))\n\tr.GET(cfg.API.MetricURI, metricsHandler)\n\tr.GET(cfg.API.HealthURI, heartbeatHandler)\n\tr.HEAD(cfg.API.HealthURI, heartbeatHandler)\n\tr.GET(\"/version\", versionHandler)\n\tr.GET(\"/\", rootHandler)\n\n\treturn r\n}\n\n// markFailedNotification adds failure logs for all tokens in push notification\nfunc markFailedNotification(\n\tcfg *config.ConfYaml,\n\tnotification *notify.PushNotification,\n\treason string,\n) []logx.LogPushEntry {\n\tlogx.LogError.Error(reason)\n\tlogs := make([]logx.LogPushEntry, 0)\n\tfor _, token := range notification.Tokens {\n\t\tlogs = append(logs, logx.GetLogPushEntry(&logx.InputLog{\n\t\t\tID:        notification.ID,\n\t\t\tStatus:    core.FailedPush,\n\t\t\tToken:     token,\n\t\t\tMessage:   notification.Message,\n\t\t\tPlatform:  notification.Platform,\n\t\t\tError:     errors.New(reason),\n\t\t\tHideToken: cfg.Log.HideToken,\n\t\t\tFormat:    cfg.Log.Format,\n\t\t}))\n\t}\n\n\treturn logs\n}\n\n// isPlatformEnabled checks if the notification platform is enabled in config.\nfunc isPlatformEnabled(cfg *config.ConfYaml, platform int) bool {\n\tswitch platform {\n\tcase core.PlatFormIos:\n\t\treturn cfg.Ios.Enabled\n\tcase core.PlatFormAndroid:\n\t\treturn cfg.Android.Enabled\n\tcase core.PlatFormHuawei:\n\t\treturn cfg.Huawei.Enabled\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// filterEnabledNotifications filters notifications to only those with enabled platforms.\nfunc filterEnabledNotifications(\n\tcfg *config.ConfYaml, notifications []notify.PushNotification,\n) []*notify.PushNotification {\n\tresult := make([]*notify.PushNotification, 0, len(notifications))\n\tfor i := range notifications {\n\t\tif isPlatformEnabled(cfg, notifications[i].Platform) {\n\t\t\tresult = append(result, &notifications[i])\n\t\t}\n\t}\n\treturn result\n}\n\n// countNotificationTargets counts the total number of targets (tokens + topics) in a notification.\nfunc countNotificationTargets(notification *notify.PushNotification) int {\n\tcount := len(notification.Tokens)\n\tif notification.Topic != \"\" {\n\t\tcount++\n\t}\n\treturn count\n}\n\n// HandleNotification add notification to queue list.\nfunc handleNotification(\n\t_ context.Context,\n\tcfg *config.ConfYaml,\n\treq notify.RequestPush,\n\tq *queue.Queue,\n) (int, []logx.LogPushEntry) {\n\tif cfg.Core.Sync && !core.IsLocalQueue(core.Queue(cfg.Queue.Engine)) {\n\t\tcfg.Core.Sync = false\n\t}\n\n\tnotifications := filterEnabledNotifications(cfg, req.Notifications)\n\tisLocalSync := core.IsLocalQueue(core.Queue(cfg.Queue.Engine)) && cfg.Core.Sync\n\n\tvar (\n\t\tcount int\n\t\twg    sync.WaitGroup\n\t\tlogs  = make([]logx.LogPushEntry, 0)\n\t)\n\n\tfor _, notification := range notifications {\n\t\tif cfg.Core.Sync {\n\t\t\twg.Add(1)\n\t\t}\n\n\t\tif isLocalSync {\n\t\t\tfunc(msg *notify.PushNotification, cfg *config.ConfYaml) {\n\t\t\t\tif err := q.QueueTask(func(ctx context.Context) error {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tresp, err := notify.SendNotification(ctx, msg, cfg)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tlogs = append(logs, resp.Logs...)\n\t\t\t\t\treturn nil\n\t\t\t\t}); err != nil {\n\t\t\t\t\tlogx.LogError.Error(err)\n\t\t\t\t}\n\t\t\t}(notification, cfg)\n\t\t} else if err := q.Queue(notification); err != nil {\n\t\t\tresp := markFailedNotification(cfg, notification, \"max capacity reached\")\n\t\t\tlogs = append(logs, resp...)\n\t\t\twg.Done()\n\t\t}\n\n\t\tcount += countNotificationTargets(notification)\n\t}\n\n\tif cfg.Core.Sync {\n\t\twg.Wait()\n\t}\n\n\tstatus.StatStorage.AddTotalCount(int64(count))\n\n\treturn count, logs\n}\n"
  },
  {
    "path": "router/server_lambda.go",
    "content": "//go:build lambda\n\npackage router\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/logx\"\n\n\t\"github.com/apex/gateway\"\n\t\"github.com/golang-queue/queue\"\n)\n\n// RunHTTPServer provide run http or https protocol.\nfunc RunHTTPServer(\n\tctx context.Context,\n\tcfg *config.ConfYaml,\n\tq *queue.Queue,\n\ts ...*http.Server,\n) (err error) {\n\tif !cfg.Core.Enabled {\n\t\tlogx.LogAccess.Debug(\"httpd server is disabled.\")\n\t\treturn nil\n\t}\n\n\tlogx.LogAccess.Info(\"HTTPD server is running on \" + cfg.Core.Port + \" port.\")\n\n\treturn gateway.ListenAndServe(cfg.Core.Address+\":\"+cfg.Core.Port, routerEngine(cfg, q))\n}\n"
  },
  {
    "path": "router/server_normal.go",
    "content": "//go:build !lambda\n\npackage router\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/logx\"\n\n\t\"github.com/golang-queue/queue\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// RunHTTPServer provide run http or https protocol.\nfunc RunHTTPServer(\n\tctx context.Context,\n\tcfg *config.ConfYaml,\n\tq *queue.Queue,\n\ts ...*http.Server,\n) (err error) {\n\tvar server *http.Server\n\n\tif !cfg.Core.Enabled {\n\t\tlogx.LogAccess.Info(\"httpd server is disabled.\")\n\t\treturn nil\n\t}\n\n\tif len(s) == 0 {\n\t\t//nolint:gosec // server timeouts are handled by the reverse proxy in production\n\t\tserver = &http.Server{\n\t\t\tAddr:    cfg.Core.Address + \":\" + cfg.Core.Port,\n\t\t\tHandler: routerEngine(cfg, q),\n\t\t}\n\t} else {\n\t\tserver = s[0]\n\t}\n\n\tlogx.LogAccess.Info(\"HTTPD server is running on \" + cfg.Core.Port + \" port.\")\n\tif cfg.Core.AutoTLS.Enabled {\n\t\treturn startServer(ctx, autoTLSServer(cfg, q), cfg)\n\t} else if cfg.Core.SSL {\n\t\tconfig := &tls.Config{\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t}\n\n\t\tif config.NextProtos == nil {\n\t\t\tconfig.NextProtos = []string{\"http/1.1\"}\n\t\t}\n\n\t\tconfig.Certificates = make([]tls.Certificate, 1)\n\t\t//nolint:gocritic // ifElseChain is intentional for clarity of TLS cert loading options\n\t\tif cfg.Core.CertPath != \"\" && cfg.Core.KeyPath != \"\" {\n\t\t\tconfig.Certificates[0], err = tls.LoadX509KeyPair(cfg.Core.CertPath, cfg.Core.KeyPath)\n\t\t\tif err != nil {\n\t\t\t\tlogx.LogError.Error(\"Failed to load https cert file: \", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else if cfg.Core.CertBase64 != \"\" && cfg.Core.KeyBase64 != \"\" {\n\t\t\tcert, err := base64.StdEncoding.DecodeString(cfg.Core.CertBase64)\n\t\t\tif err != nil {\n\t\t\t\tlogx.LogError.Error(\"base64 decode error:\", err.Error())\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tkey, err := base64.StdEncoding.DecodeString(cfg.Core.KeyBase64)\n\t\t\tif err != nil {\n\t\t\t\tlogx.LogError.Error(\"base64 decode error:\", err.Error())\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif config.Certificates[0], err = tls.X509KeyPair(cert, key); err != nil {\n\t\t\t\tlogx.LogError.Error(\"tls key pair error:\", err.Error())\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\treturn errors.New(\"missing https cert config\")\n\t\t}\n\n\t\tserver.TLSConfig = config\n\t}\n\n\treturn startServer(ctx, server, cfg)\n}\n\nfunc listenAndServe(ctx context.Context, s *http.Server, cfg *config.ConfYaml) error {\n\tvar g errgroup.Group\n\tg.Go(func() error {\n\t\t<-ctx.Done()\n\t\ttimeout := time.Duration(cfg.Core.ShutdownTimeout) * time.Second\n\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\tdefer cancel()\n\t\treturn s.Shutdown(ctx)\n\t})\n\tg.Go(func() error {\n\t\tif err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\treturn g.Wait()\n}\n\nfunc listenAndServeTLS(ctx context.Context, s *http.Server, cfg *config.ConfYaml) error {\n\tvar g errgroup.Group\n\tg.Go(func() error {\n\t\t<-ctx.Done()\n\t\ttimeout := time.Duration(cfg.Core.ShutdownTimeout) * time.Second\n\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\tdefer cancel()\n\t\treturn s.Shutdown(ctx)\n\t})\n\tg.Go(func() error {\n\t\tif err := s.ListenAndServeTLS(\"\", \"\"); err != nil && err != http.ErrServerClosed {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\treturn g.Wait()\n}\n\nfunc startServer(ctx context.Context, s *http.Server, cfg *config.ConfYaml) error {\n\tif s.TLSConfig == nil {\n\t\treturn listenAndServe(ctx, s, cfg)\n\t}\n\n\treturn listenAndServeTLS(ctx, s, cfg)\n}\n"
  },
  {
    "path": "router/server_test.go",
    "content": "package router\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/notify\"\n\t\"github.com/appleboy/gorush/status\"\n\n\t\"github.com/appleboy/gofight/v2\"\n\t\"github.com/buger/jsonparser\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/golang-queue/queue\"\n\tqcore \"github.com/golang-queue/queue/core\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tgoVersion   = runtime.Version()\n\tq           *queue.Queue\n\ttestKeyPath = \"../certificate/certificate-valid.pem\"\n)\n\nfunc TestMain(m *testing.M) {\n\tcfg := initTest()\n\tif err := status.InitAppStatus(cfg); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\n\tif _, err := notify.InitFCMClient(context.Background(), cfg); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tq = queue.NewPool(\n\t\tcfg.Core.WorkerNum,\n\t\tqueue.WithFn(func(ctx context.Context, msg qcore.TaskMessage) error {\n\t\t\t_, err := notify.SendNotification(ctx, msg, cfg)\n\t\t\treturn err\n\t\t}),\n\t\tqueue.WithLogger(logx.QueueLogger()),\n\t)\n\n\tcode := m.Run()\n\tdefer func() {\n\t\tq.Release()\n\t\tos.Exit(code)\n\t}()\n}\n\nfunc initTest() *config.ConfYaml {\n\tcfg, _ := config.LoadConf()\n\tcfg.Core.Mode = \"test\"\n\treturn cfg\n}\n\n// testRequest is testing url string if server is running\nfunc testRequest(t *testing.T, url string) {\n\ttr := &http.Transport{\n\t\t//nolint:gosec // InsecureSkipVerify is needed for testing with self-signed certificates\n\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t}\n\tclient := &http.Client{\n\t\tTimeout:   time.Second * 10,\n\t\tTransport: tr,\n\t}\n\treq, _ := http.NewRequestWithContext(\n\t\tcontext.Background(),\n\t\thttp.MethodGet,\n\t\turl,\n\t\tnil,\n\t)\n\tresp, err := client.Do(req)\n\tdefer func() {\n\t\tif err := resp.Body.Close(); err != nil {\n\t\t\tlog.Println(\"close body err:\", err)\n\t\t}\n\t}()\n\n\trequire.NoError(t, err)\n\n\t_, ioerr := io.ReadAll(resp.Body)\n\trequire.NoError(t, ioerr)\n\tassert.Equal(t, \"200 OK\", resp.Status, \"should get a 200\")\n}\n\nfunc TestPrintGoRushVersion(t *testing.T) {\n\tSetVersion(\"3.0.0\")\n\tSetCommit(\"abcdefg\")\n\tver := GetVersion()\n\tPrintGoRushVersion()\n\n\tassert.Equal(t, \"3.0.0\", ver)\n}\n\nfunc TestRunNormalServer(t *testing.T) {\n\tcfg := initTest()\n\n\tgin.SetMode(gin.TestMode)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tassert.NoError(t, RunHTTPServer(ctx, cfg, q))\n\t}()\n\n\tdefer func() {\n\t\t// close the server\n\t\tcancel()\n\t}()\n\t// have to wait for the goroutine to start and run the server\n\t// otherwise the main thread will complete\n\ttime.Sleep(5 * time.Millisecond)\n\n\ttestRequest(t, \"http://localhost:8088/api/stat/go\")\n}\n\nfunc TestRunTLSServer(t *testing.T) {\n\tcfg := initTest()\n\n\tcfg.Core.SSL = true\n\tcfg.Core.Port = \"8087\"\n\tcfg.Core.CertPath = \"../certificate/localhost.cert\"\n\tcfg.Core.KeyPath = \"../certificate/localhost.key\"\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tassert.NoError(t, RunHTTPServer(ctx, cfg, q))\n\t}()\n\n\tdefer func() {\n\t\t// close the server\n\t\tcancel()\n\t}()\n\t// have to wait for the goroutine to start and run the server\n\t// otherwise the main thread will complete\n\ttime.Sleep(5 * time.Millisecond)\n\n\ttestRequest(t, \"https://localhost:8087/api/stat/go\")\n}\n\nfunc TestRunTLSBase64Server(t *testing.T) {\n\t//nolint:lll // base64-encoded test certificate must remain on a single line\n\tcert := `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMrekNDQWVPZ0F3SUJBZ0lKQUxiWkVEdlVRckZLTUEwR0NTcUdTSWIzRFFFQkJRVUFNQlF4RWpBUUJnTlYKQkFNTUNXeHZZMkZzYUc5emREQWVGdzB4TmpBek1qZ3dNek13TkRGYUZ3MHlOakF6TWpZd016TXdOREZhTUJReApFakFRQmdOVkJBTU1DV3h2WTJGc2FHOXpkRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DCmdnRUJBTWoxK3hnNGpWTHpWbkI1ajduMXVsMzBXRUU0QkN6Y05GeGc1QU9CNUg1cSt3amUwWVlpVkZnNlBReXYKR0NpcHFJUlhWUmRWUTFoSFNldW5ZR0tlOGxxM1NiMVg4UFVKMTJ2OXVSYnBTOURLMU93cWs4cnNQRHU2c1ZUTApxS0tnSDFaOHlhenphUzBBYlh1QTVlOWdPL1J6aWpibnBFUCtxdU00ZHVlaU1QVkVKeUxxK0VvSVFZK01NOE1QCjhkWnpMNFhabDd3TDRVc0NON3JQY082VzN0bG5UMGlPM2g5Yy9ZbTJoRmh6K0tOSjlLUlJDdnRQR1pFU2lndEsKYkhzWEgwOTlXRG84di9XcDUvZXZCdy8rSkQwb3B4bUNmSElCQUxIdDl2NTNSdnZzRFoxdDMzUnB1NUM4em5FWQpZMkF5N05neGhxanFvV0pxQTQ4bEplQTBjbHNDQXdFQUFhTlFNRTR3SFFZRFZSME9CQllFRkMwYlRVMVhvZmVoCk5LSWVsYXNoSXNxS2lkRFlNQjhHQTFVZEl3UVlNQmFBRkMwYlRVMVhvZmVoTktJZWxhc2hJc3FLaWREWU1Bd0cKQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ2dFQkFBaUpMOElNVHdOWDlYcVFXWURGZ2tHNApBbnJWd1FocmVBcUM5clN4RENqcXFuTUhQSEd6Y0NlRE1MQU1vaDBrT3kyMG5vd1VHTnRDWjB1QnZuWDJxMWJOCmcxanQrR0JjTEpEUjNMTDRDcE5PbG0zWWhPeWN1TmZXTXhUQTdCWGttblNyWkQvN0toQXJzQkVZOGF1bHh3S0oKSFJnTmxJd2Uxb0ZEMVlkWDFCUzVwcDR0MjVCNlZxNEEzRk1NVWtWb1dFNjg4bkUxNjhodlFnd2pySGtnSGh3ZQplTjhsR0UyRGhGcmFYbldtRE1kd2FIRDNIUkZHaHlwcElGTitmN0JxYldYOWdNK1QyWVJUZk9iSVhMV2JxSkxECjNNay9Oa3hxVmNnNGVZNTR3SjF1ZkNVR0FZQUlhWTZmUXFpTlV6OG5od0szdDQ1TkJWVDl5L3VKWHFuVEx5WT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=`\n\t//nolint:lll // base64-encoded test private key must remain on a single line\n\tkey := `LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBeVBYN0dEaU5Vdk5XY0htUHVmVzZYZlJZUVRnRUxOdzBYR0RrQTRIa2ZtcjdDTjdSCmhpSlVXRG85REs4WUtLbW9oRmRWRjFWRFdFZEo2NmRnWXA3eVdyZEp2VmZ3OVFuWGEvMjVGdWxMME1yVTdDcVQKeXV3OE83cXhWTXVvb3FBZlZuekpyUE5wTFFCdGU0RGw3MkE3OUhPS051ZWtRLzZxNHpoMjU2SXc5VVFuSXVyNApTZ2hCajR3end3L3gxbk12aGRtWHZBdmhTd0kzdXM5dzdwYmUyV2RQU0k3ZUgxejlpYmFFV0hQNG8wbjBwRkVLCiswOFprUktLQzBwc2V4Y2ZUMzFZT2p5Lzlhbm45NjhIRC80a1BTaW5HWUo4Y2dFQXNlMzIvbmRHKyt3Tm5XM2YKZEdtN2tMek9jUmhqWURMczJER0dxT3FoWW1vRGp5VWw0RFJ5V3dJREFRQUJBb0lCQUdUS3FzTjlLYlNmQTQycQpDcUkwVXVMb3VKTU5hMXFzbno1dUFpNllLV2dXZEE0QTQ0bXBFakNtRlJTVmhVSnZ4V3VLK2N5WUlRelh4SVdECkQxNm5aZHFGNzJBZUNXWjlKeVNzdnZaMDBHZktNM3kzNWlSeTA4c0pXZ096bWNMbkdKQ2lTZXlLc1FlM0hUSkMKZGhEWGJYcXZzSFRWUFpnMDFMVGVEeFVpVGZmVThOTUtxUjJBZWNRMnNURHdYRWhBblR5QXRuemwvWGFCZ0Z6dQpVNkc3RnpHTTV5OWJ4a2ZRVmt2eStERUprSEdOT2p6d2NWZkJ5eVZsNjEwaXhtRzF2bXhWajlQYldtSVBzVVY4CnlTbWpodkRRYk9mb3hXMGg5dlRsVHFHdFFjQnc5NjJvc25ERE1XRkNkTTdsek8wVDdSUm5QVkdJUnBDSk9LaHEKa2VxSEt3RUNnWUVBOHd3SS9pWnVnaG9UWFRORzlMblFRL1dBdHNxTzgwRWpNVFVoZW81STFrT3ptVXowOXB5aAppQXNVRG9OMC8yNnRaNVdOamxueVp1N2R2VGMveDNkVFpwbU5ub284Z2NWYlFORUNEUnpxZnVROVBQWG0xU041CjZwZUJxQXZCdjc4aGpWMDVhWHpQRy9WQmJlaWc3bDI5OUVhckVBK2Evb0gzS3JnRG9xVnFFMEVDZ1lFQTA2dkEKWUptZ2c0ZlpSdWNBWW9hWXNMejlaOXJDRmpUZTFQQlRtVUprYk9SOHZGSUhIVFRFV2kvU3V4WEwwd0RTZW9FMgo3QlFtODZnQ0M3L0tnUmRyem9CcVo1cVM5TXYyZHNMZ1k2MzVWU2dqamZaa1ZMaUgxVlJScFNRT2JZbmZveXNnCmdhdGNIU0tNRXhkNFNMUUJ5QXVJbVhQK0w1YXlEQmNFSmZicVNwc0NnWUI3OElzMWIwdXpOTERqT2g3WTlWaHIKRDJxUHpFT1JjSW9Oc2RaY3RPb1h1WGFBbW1uZ3lJYm01UjlaTjFnV1djNDdvRndMVjNyeFdxWGdzNmZtZzhjWAo3djMwOXZGY0M5UTQvVnhhYTRCNUxOSzluM2dUQUlCUFRPdGxVbmwrMm15MXRmQnRCcVJtMFc2SUtiVEhXUzVnCnZ4akVtL0NpRUl5R1VFZ3FUTWdIQVFLQmdCS3VYZFFvdXRuZzYzUXVmd0l6RHRiS1Z6TUxRNFhpTktobWJYcGgKT2F2Q25wK2dQYkIrTDdZbDhsdEFtVFNPSmdWWjBoY1QwRHhBMzYxWngrMk11NThHQmw0T2JsbmNobXdFMXZqMQpLY1F5UHJFUXhkb1VUeWlzd0dmcXZyczhKOWltdmIrejkvVTZUMUtBQjhXaTNXVmlYelByNE1zaWFhUlhnNjQyCkZJZHhBb0dBWjcvNzM1ZGtoSmN5T2ZzK0xLc0xyNjhKU3N0b29yWE9ZdmRNdTErSkdhOWlMdWhuSEVjTVZXQzgKSXVpaHpQZmxvWnRNYkdZa1pKbjhsM0JlR2Q4aG1mRnRnVGdaR1BvVlJldGZ0MkxERkxuUHhwMnNFSDVPRkxzUQpSK0sva0FPdWw4ZVN0V3VNWE9GQTlwTXpHa0dFZ0lGSk1KT3lhSk9OM2tlZFFJOGRlQ009Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==`\n\tcfg := initTest()\n\n\tcfg.Core.SSL = true\n\tcfg.Core.Port = \"8089\"\n\tcfg.Core.CertPath = \"\"\n\tcfg.Core.KeyPath = \"\"\n\tcfg.Core.CertBase64 = cert\n\tcfg.Core.KeyBase64 = key\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tassert.NoError(t, RunHTTPServer(ctx, cfg, q))\n\t}()\n\n\tdefer func() {\n\t\t// close the server\n\t\tcancel()\n\t}()\n\t// have to wait for the goroutine to start and run the server\n\t// otherwise the main thread will complete\n\ttime.Sleep(5 * time.Millisecond)\n\n\ttestRequest(t, \"https://localhost:8089/api/stat/go\")\n}\n\nfunc TestRunAutoTLSServer(t *testing.T) {\n\tcfg := initTest()\n\tcfg.Core.AutoTLS.Enabled = true\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tassert.NoError(t, RunHTTPServer(ctx, cfg, q))\n\t}()\n\n\tdefer func() {\n\t\t// close the server\n\t\tcancel()\n\t}()\n\t// have to wait for the goroutine to start and run the server\n\t// otherwise the main thread will complete\n\ttime.Sleep(5 * time.Millisecond)\n}\n\nfunc TestLoadTLSCertError(t *testing.T) {\n\tcfg := initTest()\n\n\tcfg.Core.SSL = true\n\tcfg.Core.Port = \"8087\"\n\tcfg.Core.CertPath = \"../config/config.yml\"\n\tcfg.Core.KeyPath = \"../config/config.yml\"\n\n\tassert.Error(t, RunHTTPServer(context.Background(), cfg, q))\n}\n\nfunc TestMissingTLSCertcfgg(t *testing.T) {\n\tcfg := initTest()\n\n\tcfg.Core.SSL = true\n\tcfg.Core.Port = \"8087\"\n\tcfg.Core.CertPath = \"\"\n\tcfg.Core.KeyPath = \"\"\n\tcfg.Core.CertBase64 = \"\"\n\tcfg.Core.KeyBase64 = \"\"\n\n\terr := RunHTTPServer(context.Background(), cfg, q)\n\trequire.Error(t, RunHTTPServer(context.Background(), cfg, q))\n\tassert.Equal(t, \"missing https cert config\", err.Error())\n}\n\nfunc TestRootHandler(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\t// log for json\n\tcfg.Log.Format = \"json\"\n\n\tr.GET(\"/\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tdata := r.Body.Bytes()\n\n\t\t\tvalue, _ := jsonparser.GetString(data, \"text\")\n\n\t\t\tassert.Equal(t, \"Welcome to notification server.\", value)\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t})\n}\n\nfunc TestAPIStatusGoHandler(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\tr.GET(\"/api/stat/go\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tdata := r.Body.Bytes()\n\n\t\t\tvalue, _ := jsonparser.GetString(data, \"go_version\")\n\n\t\t\tassert.Equal(t, goVersion, value)\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t})\n}\n\nfunc TestAPIStatusAppHandler(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\tappVersion := \"v1.0.0\"\n\tSetVersion(appVersion)\n\n\tr.GET(\"/api/stat/app\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tdata := r.Body.Bytes()\n\n\t\t\tvalue, _ := jsonparser.GetString(data, \"version\")\n\n\t\t\tassert.Equal(t, appVersion, value)\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t})\n}\n\nfunc TestAPIConfigHandler(t *testing.T) {\n\tcfg := initTest()\n\n\t// Set sensitive values to verify they are redacted\n\tcfg.Android.Credential = \"secret-fcm-credential\"\n\tcfg.Ios.Password = \"secret-ios-password\"\n\tcfg.Stat.Redis.Password = \"secret-redis-password\"\n\n\tr := gofight.New()\n\n\tr.GET(\"/api/config\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\n\t\t\tbody := r.Body.String()\n\t\t\tassert.NotContains(t, body, \"secret-fcm-credential\")\n\t\t\tassert.NotContains(t, body, \"secret-ios-password\")\n\t\t\tassert.NotContains(t, body, \"secret-redis-password\")\n\t\t\tassert.Contains(t, body, \"[REDACTED]\")\n\t\t})\n}\n\nfunc TestMissingNotificationsParameter(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\t// missing notifications parameter.\n\tr.POST(\"/api/push\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusBadRequest, r.Code)\n\t\t})\n}\n\nfunc TestEmptyNotifications(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\t// notifications is empty.\n\tr.POST(\"/api/push\").\n\t\tSetJSON(gofight.D{\n\t\t\t\"notifications\": []notify.PushNotification{},\n\t\t}).\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusBadRequest, r.Code)\n\t\t})\n}\n\nfunc TestMutableContent(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\t// notifications is empty.\n\tr.POST(\"/api/push\").\n\t\tSetJSON(gofight.D{\n\t\t\t\"notifications\": []gofight.D{\n\t\t\t\t{\n\t\t\t\t\t\"tokens\":          []string{\"aaaaa\", \"bbbbb\"},\n\t\t\t\t\t\"platform\":        core.PlatFormAndroid,\n\t\t\t\t\t\"message\":         \"Welcome From API\",\n\t\t\t\t\t\"mutable_content\": 1,\n\t\t\t\t\t\"topic\":           \"test\",\n\t\t\t\t\t\"badge\":           1,\n\t\t\t\t\t\"alert\": gofight.D{\n\t\t\t\t\t\t\"title\": \"title\",\n\t\t\t\t\t\t\"body\":  \"body\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}).\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\t// json: cannot unmarshal number into Go struct field notify.PushNotification.mutable_content of type bool\n\t\t\tassert.Equal(t, http.StatusBadRequest, r.Code)\n\t\t})\n}\n\nfunc TestOutOfRangeMaxNotifications(t *testing.T) {\n\tcfg := initTest()\n\n\tcfg.Core.MaxNotification = int64(1)\n\n\tr := gofight.New()\n\n\t// notifications is empty.\n\tr.POST(\"/api/push\").\n\t\tSetJSON(gofight.D{\n\t\t\t\"notifications\": []gofight.D{\n\t\t\t\t{\n\t\t\t\t\t\"tokens\":   []string{\"aaaaa\", \"bbbbb\"},\n\t\t\t\t\t\"platform\": core.PlatFormAndroid,\n\t\t\t\t\t\"message\":  \"Welcome API From Android\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"tokens\":   []string{\"aaaaa\", \"bbbbb\"},\n\t\t\t\t\t\"platform\": core.PlatFormAndroid,\n\t\t\t\t\t\"message\":  \"Welcome API From Android\",\n\t\t\t\t},\n\t\t\t},\n\t\t}).\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusBadRequest, r.Code)\n\t\t})\n}\n\nfunc TestSuccessPushHandler(t *testing.T) {\n\tt.Skip()\n\tcfg := initTest()\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\n\tandroidToken := os.Getenv(\"FCM_TEST_TOKEN\")\n\n\tr := gofight.New()\n\n\tr.POST(\"/api/push\").\n\t\tSetJSON(gofight.D{\n\t\t\t\"notifications\": []gofight.D{\n\t\t\t\t{\n\t\t\t\t\t\"tokens\":   []string{androidToken, \"bbbbb\"},\n\t\t\t\t\t\"platform\": core.PlatFormAndroid,\n\t\t\t\t\t\"message\":  \"Welcome Android\",\n\t\t\t\t},\n\t\t\t},\n\t\t}).\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t})\n}\n\nfunc TestSysStatsHandler(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\tr.GET(\"/sys/stats\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t})\n}\n\nfunc TestMetricsHandler(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\tr.GET(\"/metrics\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t})\n}\n\nfunc TestGETHeartbeatHandler(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\tr.GET(\"/healthz\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t})\n}\n\nfunc TestHEADHeartbeatHandler(t *testing.T) {\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\tr.HEAD(\"/healthz\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t})\n}\n\nfunc TestVersionHandler(t *testing.T) {\n\tSetVersion(\"3.0.0\")\n\tcfg := initTest()\n\n\tr := gofight.New()\n\n\tr.GET(\"/version\").\n\t\tRun(routerEngine(cfg, q), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {\n\t\t\tassert.Equal(t, http.StatusOK, r.Code)\n\t\t\tdata := r.Body.Bytes()\n\n\t\t\tvalue, _ := jsonparser.GetString(data, \"version\")\n\n\t\t\tassert.Equal(t, \"3.0.0\", value)\n\t\t})\n}\n\nfunc TestDisabledHTTPServer(t *testing.T) {\n\tcfg := initTest()\n\tcfg.Core.Enabled = false\n\terr := RunHTTPServer(context.Background(), cfg, q)\n\tcfg.Core.Enabled = true\n\n\trequire.NoError(t, err)\n}\n\nfunc TestSenMultipleNotifications(t *testing.T) {\n\tctx := context.Background()\n\tcfg := initTest()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPath\n\terr := notify.InitAPNSClient(ctx, cfg)\n\trequire.NoError(t, err)\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\n\tandroidToken := os.Getenv(\"FCM_TEST_TOKEN\")\n\n\treq := notify.RequestPush{\n\t\tNotifications: []notify.PushNotification{\n\t\t\t// ios\n\t\t\t{\n\t\t\t\tTokens: []string{\n\t\t\t\t\t\"11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7\",\n\t\t\t\t},\n\t\t\t\tPlatform: core.PlatFormIos,\n\t\t\t\tMessage:  \"Welcome iOS\",\n\t\t\t},\n\t\t\t// android\n\t\t\t{\n\t\t\t\tTokens:   []string{androidToken, \"bbbbb\"},\n\t\t\t\tPlatform: core.PlatFormAndroid,\n\t\t\t\tMessage:  \"Welcome Android\",\n\t\t\t},\n\t\t},\n\t}\n\n\tcount, logs := handleNotification(ctx, cfg, req, q)\n\tassert.Equal(t, 3, count)\n\tassert.Empty(t, logs)\n}\n\nfunc TestDisabledAndroidNotifications(t *testing.T) {\n\tctx := context.Background()\n\tcfg := initTest()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPath\n\terr := notify.InitAPNSClient(ctx, cfg)\n\trequire.NoError(t, err)\n\n\tcfg.Android.Enabled = false\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\n\tandroidToken := os.Getenv(\"FCM_TEST_TOKEN\")\n\n\treq := notify.RequestPush{\n\t\tNotifications: []notify.PushNotification{\n\t\t\t// ios\n\t\t\t{\n\t\t\t\tTokens: []string{\n\t\t\t\t\t\"11aa01229f15f0f0c5209d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7\",\n\t\t\t\t},\n\t\t\t\tPlatform: core.PlatFormIos,\n\t\t\t\tMessage:  \"Welcome iOS\",\n\t\t\t},\n\t\t\t// android\n\t\t\t{\n\t\t\t\tTokens:   []string{androidToken, \"bbbbb\"},\n\t\t\t\tPlatform: core.PlatFormAndroid,\n\t\t\t\tMessage:  \"Welcome Android\",\n\t\t\t},\n\t\t},\n\t}\n\n\tcount, logs := handleNotification(ctx, cfg, req, q)\n\tassert.Equal(t, 1, count)\n\tassert.Empty(t, logs)\n}\n\nfunc TestSyncModeForNotifications(t *testing.T) {\n\tctx := context.Background()\n\tcfg := initTest()\n\n\tcfg.Ios.Enabled = true\n\tcfg.Ios.KeyPath = testKeyPath\n\terr := notify.InitAPNSClient(ctx, cfg)\n\trequire.NoError(t, err)\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\n\t// enable sync mode\n\tcfg.Core.Sync = true\n\n\tandroidToken := os.Getenv(\"FCM_TEST_TOKEN\")\n\n\treq := notify.RequestPush{\n\t\tNotifications: []notify.PushNotification{\n\t\t\t// ios\n\t\t\t{\n\t\t\t\tTokens: []string{\n\t\t\t\t\t\"11aa01229f15f0f0c12029d8c111d1ae1f2365f14cebc4af26cd6d76b7919ef7\",\n\t\t\t\t},\n\t\t\t\tPlatform: core.PlatFormIos,\n\t\t\t\tMessage:  \"Welcome iOS Sync\",\n\t\t\t},\n\t\t\t// android\n\t\t\t{\n\t\t\t\tTokens:   []string{androidToken, \"bbbbb\"},\n\t\t\t\tPlatform: core.PlatFormAndroid,\n\t\t\t\tMessage:  \"Welcome Android Sync\",\n\t\t\t},\n\t\t},\n\t}\n\n\tcount, logs := handleNotification(ctx, cfg, req, q)\n\tassert.Equal(t, 3, count)\n\tassert.Len(t, logs, 3)\n}\n\nfunc TestSyncModeForTopicNotification(t *testing.T) {\n\tctx := context.Background()\n\tcfg := initTest()\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\tcfg.Log.HideToken = false\n\n\t// enable sync mode\n\tcfg.Core.Sync = true\n\n\treq := notify.RequestPush{\n\t\tNotifications: []notify.PushNotification{\n\t\t\t// android\n\t\t\t{\n\t\t\t\t// error:InvalidParameters\n\t\t\t\t// Check that the provided parameters have the right name and type.\n\t\t\t\tTopic:    \"/topics/foo-bar@@@##\",\n\t\t\t\tPlatform: core.PlatFormAndroid,\n\t\t\t\tMessage:  \"This is a Firebase Cloud Messaging Topic Message!\",\n\t\t\t},\n\t\t\t// android\n\t\t\t{\n\t\t\t\t// success\n\t\t\t\tTopic:    \"/topics/foo-bar\",\n\t\t\t\tPlatform: core.PlatFormAndroid,\n\t\t\t\tMessage:  \"This is a Firebase Cloud Messaging Topic Message!\",\n\t\t\t},\n\t\t\t// android\n\t\t\t{\n\t\t\t\t// success\n\t\t\t\tCondition: \"'dogs' in topics || 'cats' in topics\",\n\t\t\t\tPlatform:  core.PlatFormAndroid,\n\t\t\t\tMessage:   \"This is a Firebase Cloud Messaging Topic Message!\",\n\t\t\t},\n\t\t},\n\t}\n\n\tcount, logs := handleNotification(ctx, cfg, req, q)\n\tassert.Equal(t, 2, count)\n\tassert.Empty(t, logs)\n}\n\nfunc TestSyncModeForDeviceGroupNotification(t *testing.T) {\n\tctx := context.Background()\n\tcfg := initTest()\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\tcfg.Log.HideToken = false\n\n\t// enable sync mode\n\tcfg.Core.Sync = true\n\n\treq := notify.RequestPush{\n\t\tNotifications: []notify.PushNotification{\n\t\t\t// android\n\t\t\t{\n\t\t\t\tTopic:    \"aUniqueKey\",\n\t\t\t\tPlatform: core.PlatFormAndroid,\n\t\t\t\tMessage:  \"This is a Firebase Cloud Messaging Device Group Message!\",\n\t\t\t},\n\t\t},\n\t}\n\n\t// success\n\tcount, logs := handleNotification(ctx, cfg, req, q)\n\tassert.Equal(t, 1, count)\n\tassert.Empty(t, logs)\n}\n\nfunc TestDisabledIosNotifications(t *testing.T) {\n\tctx := context.Background()\n\tcfg := initTest()\n\n\tcfg.Ios.Enabled = false\n\tcfg.Ios.KeyPath = testKeyPath\n\terr := notify.InitAPNSClient(ctx, cfg)\n\trequire.NoError(t, err)\n\n\tcfg.Android.Enabled = true\n\tcfg.Android.Credential = os.Getenv(\"FCM_CREDENTIAL\")\n\n\tandroidToken := os.Getenv(\"FCM_TEST_TOKEN\")\n\n\treq := notify.RequestPush{\n\t\tNotifications: []notify.PushNotification{\n\t\t\t// ios\n\t\t\t{\n\t\t\t\tTokens: []string{\n\t\t\t\t\t\"11aa01229f15f0f0c52021d8cf3cd0ae1f2365fe4cebc4af26cd6d76b7919ef7\",\n\t\t\t\t},\n\t\t\t\tPlatform: core.PlatFormIos,\n\t\t\t\tMessage:  \"Welcome iOS platform\",\n\t\t\t},\n\t\t\t// android\n\t\t\t{\n\t\t\t\tTokens:   []string{androidToken, androidToken + \"_\"},\n\t\t\t\tPlatform: core.PlatFormAndroid,\n\t\t\t\tMessage:  \"Welcome Android platform\",\n\t\t\t},\n\t\t},\n\t}\n\n\tcount, logs := handleNotification(ctx, cfg, req, q)\n\tassert.Equal(t, 2, count)\n\tassert.Empty(t, logs)\n}\n\n// Tests for refactored helper functions\n\nfunc TestIsPlatformEnabled(t *testing.T) {\n\tcfg := initTest()\n\n\ttests := []struct {\n\t\tname           string\n\t\tplatform       int\n\t\tiosEnabled     bool\n\t\tandroidEnabled bool\n\t\thuaweiEnabled  bool\n\t\twant           bool\n\t}{\n\t\t{\n\t\t\tname:       \"iOS enabled\",\n\t\t\tplatform:   core.PlatFormIos,\n\t\t\tiosEnabled: true,\n\t\t\twant:       true,\n\t\t},\n\t\t{\n\t\t\tname:       \"iOS disabled\",\n\t\t\tplatform:   core.PlatFormIos,\n\t\t\tiosEnabled: false,\n\t\t\twant:       false,\n\t\t},\n\t\t{\n\t\t\tname:           \"Android enabled\",\n\t\t\tplatform:       core.PlatFormAndroid,\n\t\t\tandroidEnabled: true,\n\t\t\twant:           true,\n\t\t},\n\t\t{\n\t\t\tname:           \"Android disabled\",\n\t\t\tplatform:       core.PlatFormAndroid,\n\t\t\tandroidEnabled: false,\n\t\t\twant:           false,\n\t\t},\n\t\t{\n\t\t\tname:          \"Huawei enabled\",\n\t\t\tplatform:      core.PlatFormHuawei,\n\t\t\thuaweiEnabled: true,\n\t\t\twant:          true,\n\t\t},\n\t\t{\n\t\t\tname:          \"Huawei disabled\",\n\t\t\tplatform:      core.PlatFormHuawei,\n\t\t\thuaweiEnabled: false,\n\t\t\twant:          false,\n\t\t},\n\t\t{\n\t\t\tname:     \"Unknown platform\",\n\t\t\tplatform: 99,\n\t\t\twant:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcfg.Ios.Enabled = tt.iosEnabled\n\t\t\tcfg.Android.Enabled = tt.androidEnabled\n\t\t\tcfg.Huawei.Enabled = tt.huaweiEnabled\n\n\t\t\tgot := isPlatformEnabled(cfg, tt.platform)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestFilterEnabledNotifications(t *testing.T) {\n\tcfg := initTest()\n\tcfg.Ios.Enabled = true\n\tcfg.Android.Enabled = true\n\tcfg.Huawei.Enabled = false\n\n\tnotifications := []notify.PushNotification{\n\t\t{Platform: core.PlatFormIos, Message: \"iOS message\"},\n\t\t{Platform: core.PlatFormAndroid, Message: \"Android message\"},\n\t\t{Platform: core.PlatFormHuawei, Message: \"Huawei message\"},\n\t\t{Platform: core.PlatFormIos, Message: \"iOS message 2\"},\n\t}\n\n\tresult := filterEnabledNotifications(cfg, notifications)\n\n\tassert.Len(t, result, 3)\n\tassert.Equal(t, \"iOS message\", result[0].Message)\n\tassert.Equal(t, \"Android message\", result[1].Message)\n\tassert.Equal(t, \"iOS message 2\", result[2].Message)\n}\n\nfunc TestFilterEnabledNotificationsAllDisabled(t *testing.T) {\n\tcfg := initTest()\n\tcfg.Ios.Enabled = false\n\tcfg.Android.Enabled = false\n\tcfg.Huawei.Enabled = false\n\n\tnotifications := []notify.PushNotification{\n\t\t{Platform: core.PlatFormIos, Message: \"iOS message\"},\n\t\t{Platform: core.PlatFormAndroid, Message: \"Android message\"},\n\t}\n\n\tresult := filterEnabledNotifications(cfg, notifications)\n\n\tassert.Empty(t, result)\n}\n\nfunc TestCountNotificationTargets(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tnotification *notify.PushNotification\n\t\twant         int\n\t}{\n\t\t{\n\t\t\tname: \"tokens only\",\n\t\t\tnotification: &notify.PushNotification{\n\t\t\t\tTokens: []string{\"token1\", \"token2\", \"token3\"},\n\t\t\t},\n\t\t\twant: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"topic only\",\n\t\t\tnotification: &notify.PushNotification{\n\t\t\t\tTopic: \"test-topic\",\n\t\t\t},\n\t\t\twant: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"tokens and topic\",\n\t\t\tnotification: &notify.PushNotification{\n\t\t\t\tTokens: []string{\"token1\", \"token2\"},\n\t\t\t\tTopic:  \"test-topic\",\n\t\t\t},\n\t\t\twant: 3,\n\t\t},\n\t\t{\n\t\t\tname:         \"empty notification\",\n\t\t\tnotification: &notify.PushNotification{},\n\t\t\twant:         0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := countNotificationTargets(tt.notification)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "router/version.go",
    "content": "package router\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nvar (\n\tversion string\n\tcommit  string\n)\n\n// SetVersion for setup version string.\nfunc SetVersion(ver string) {\n\tversion = ver\n}\n\n// SetCommit for setup commit string.\nfunc SetCommit(ver string) {\n\tcommit = ver\n}\n\n// GetVersion for get current version.\nfunc GetVersion() string {\n\treturn version\n}\n\n// PrintGoRushVersion provide print server engine\nfunc PrintGoRushVersion() {\n\tif len(commit) > 7 {\n\t\tcommit = commit[:7]\n\t}\n\n\tfmt.Fprintf(\n\t\tos.Stdout,\n\t\t\"GoRush %s, Commit: %s, Compiler: %s %s, Copyright (C) %d Bo-Yi Wu, Inc.\\n\",\n\t\tversion,\n\t\tcommit,\n\t\truntime.Compiler,\n\t\truntime.Version(),\n\t\ttime.Now().Year(),\n\t)\n}\n\n// VersionMiddleware : add version on header.\nfunc VersionMiddleware() gin.HandlerFunc {\n\t// Set out header value for each response\n\treturn func(c *gin.Context) {\n\t\tc.Header(\"X-GORUSH-VERSION\", version)\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "rpc/client_grpc_health.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/rpc/proto\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n)\n\n// generate protobuffs\n//   protoc --go_out=plugins=grpc,import_path=proto:. *.proto\n\ntype healthClient struct {\n\tclient proto.HealthClient\n\tconn   *grpc.ClientConn\n}\n\n// NewGrpcHealthClient returns a new grpc Client.\nfunc NewGrpcHealthClient(conn *grpc.ClientConn) core.Health {\n\tclient := new(healthClient)\n\tclient.client = proto.NewHealthClient(conn)\n\tclient.conn = conn\n\treturn client\n}\n\nfunc (c *healthClient) Close() error {\n\treturn c.conn.Close()\n}\n\nfunc (c *healthClient) Check(ctx context.Context) (bool, error) {\n\tvar res *proto.HealthCheckResponse\n\tvar err error\n\treq := new(proto.HealthCheckRequest)\n\n\tres, err = c.client.Check(ctx, req)\n\tif err == nil {\n\t\tif res.GetStatus() == proto.HealthCheckResponse_SERVING {\n\t\t\treturn true, nil\n\t\t}\n\t\treturn false, nil\n\t}\n\t//nolint:exhaustive // only specific gRPC error codes are handled as non-fatal; all others return the error\n\tswitch status.Code(err) {\n\tcase\n\t\tcodes.Aborted,\n\t\tcodes.DataLoss,\n\t\tcodes.DeadlineExceeded,\n\t\tcodes.Internal,\n\t\tcodes.Unavailable:\n\t\t// non-fatal errors\n\tdefault:\n\t\treturn false, err\n\t}\n\n\treturn false, err\n}\n"
  },
  {
    "path": "rpc/client_test.go",
    "content": "package rpc\n"
  },
  {
    "path": "rpc/example/go/health/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/appleboy/gorush/rpc\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\t\"google.golang.org/grpc/status\"\n)\n\nconst (\n\taddress = \"localhost:9000\"\n)\n\nfunc main() {\n\t// Set up a connection to the server.\n\tconn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tif err != nil {\n\t\tlog.Fatalf(\"did not connect: %v\", err)\n\t}\n\tdefer conn.Close()\n\n\tclient := rpc.NewGrpcHealthClient(conn)\n\n\tfor {\n\t\tok, err := client.Check(context.Background())\n\t\tif !ok || err != nil {\n\t\t\tlog.Printf(\"can't connect grpc server: %v, code: %v\\n\", err, status.Code(err))\n\t\t} else {\n\t\t\tlog.Println(\"connect the grpc server successfully\")\n\t\t}\n\n\t\t<-time.After(time.Second)\n\t}\n}\n"
  },
  {
    "path": "rpc/example/go/send/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"github.com/appleboy/gorush/rpc/proto\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\tstructpb \"google.golang.org/protobuf/types/known/structpb\"\n)\n\nconst (\n\taddress = \"localhost:9000\"\n)\n\nfunc main() {\n\t// Set up a connection to the server.\n\tconn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tif err != nil {\n\t\tlog.Fatalf(\"did not connect: %v\", err)\n\t}\n\tdefer conn.Close()\n\tc := proto.NewGorushClient(conn)\n\n\tr, err := c.Send(context.Background(), &proto.NotificationRequest{\n\t\tPlatform: 2,\n\t\tTokens:   []string{\"1234567890\"},\n\t\tMessage:  \"test message\",\n\t\tBadge:    1,\n\t\tCategory: \"test\",\n\t\tSound:    \"test\",\n\t\tPriority: proto.NotificationRequest_HIGH,\n\t\tAlert: &proto.Alert{\n\t\t\tTitle:    \"Test Title\",\n\t\t\tBody:     \"Test Alert Body\",\n\t\t\tSubtitle: \"Test Alert Sub Title\",\n\t\t\tLocKey:   \"Test loc key\",\n\t\t\tLocArgs:  []string{\"test\", \"test\"},\n\t\t},\n\t\tData: &structpb.Struct{\n\t\t\tFields: map[string]*structpb.Value{\n\t\t\t\t\"key1\": {\n\t\t\t\t\tKind: &structpb.Value_StringValue{StringValue: \"welcome\"},\n\t\t\t\t},\n\t\t\t\t\"key2\": {\n\t\t\t\t\tKind: &structpb.Value_NumberValue{NumberValue: 2},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tlog.Println(\"could not greet: \", err)\n\t}\n\n\tif r != nil {\n\t\tlog.Printf(\"Success: %t\\n\", r.Success)\n\t\tlog.Printf(\"Count: %d\\n\", r.Counts)\n\t}\n}\n"
  },
  {
    "path": "rpc/example/node/.gitignore",
    "content": "*~\nnode_modules\nnpm-debug.log\n.yarn-cache\n"
  },
  {
    "path": "rpc/example/node/README.md",
    "content": "# gRPC in 3 minutes (Node.js)\n\n## PREREQUISITES\n\n`node`: This requires Node 12.x or greater.\n\n## INSTALL\n\n```sh\nnpm install\nnpm install -g grpc-tools\n```\n\n## Node gRPC protoc\n\n```sh\ncd $GOPATH/src/github.com/appleboy/gorush\nprotoc -I rpc/proto rpc/proto/gorush.proto --js_out=import_style=commonjs,binary:rpc/example/node/ --grpc_out=rpc/example/node/ --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin`\n```\n"
  },
  {
    "path": "rpc/example/node/client.js",
    "content": "var messages = require('./gorush_pb');\nvar services = require('./gorush_grpc_pb');\n\nvar grpc = require('@grpc/grpc-js');\n\nfunction main() {\n  var client = new services.GorushClient('localhost:9000',\n    grpc.credentials.createInsecure());\n  var request = new messages.NotificationRequest();\n  var alert = new messages.Alert();\n  request.setPlatform(2);\n  request.setTokensList([\"1234567890\"]);\n  request.setMessage(\"Hello!!\");\n  request.setTitle(\"hello2\");\n  request.setBadge(2);\n  request.setCategory(\"mycategory\");\n  request.setSound(\"sound\")\n  alert.setTitle(\"title\");\n  request.setAlert(alert);\n  request.setThreadid(\"threadID\");\n  request.setContentavailable(false);\n  request.setMutablecontent(false);\n  client.send(request, function (err, response) {\n    if(err) {\n      console.log(err);\n    } else {\n      console.log(\"Success:\", response.getSuccess());\n      console.log(\"Counts:\", response.getCounts());\n    }\n  });\n}\n\nmain();\n"
  },
  {
    "path": "rpc/example/node/gorush_grpc_pb.js",
    "content": "// GENERATED CODE -- DO NOT EDIT!\n\n'use strict';\nvar grpc = require('@grpc/grpc-js');\nvar gorush_pb = require('./gorush_pb.js');\nvar google_protobuf_struct_pb = require('google-protobuf/google/protobuf/struct_pb.js');\n\nfunction serialize_proto_HealthCheckRequest(arg) {\n  if (!(arg instanceof gorush_pb.HealthCheckRequest)) {\n    throw new Error('Expected argument of type proto.HealthCheckRequest');\n  }\n  return Buffer.from(arg.serializeBinary());\n}\n\nfunction deserialize_proto_HealthCheckRequest(buffer_arg) {\n  return gorush_pb.HealthCheckRequest.deserializeBinary(new Uint8Array(buffer_arg));\n}\n\nfunction serialize_proto_HealthCheckResponse(arg) {\n  if (!(arg instanceof gorush_pb.HealthCheckResponse)) {\n    throw new Error('Expected argument of type proto.HealthCheckResponse');\n  }\n  return Buffer.from(arg.serializeBinary());\n}\n\nfunction deserialize_proto_HealthCheckResponse(buffer_arg) {\n  return gorush_pb.HealthCheckResponse.deserializeBinary(new Uint8Array(buffer_arg));\n}\n\nfunction serialize_proto_NotificationReply(arg) {\n  if (!(arg instanceof gorush_pb.NotificationReply)) {\n    throw new Error('Expected argument of type proto.NotificationReply');\n  }\n  return Buffer.from(arg.serializeBinary());\n}\n\nfunction deserialize_proto_NotificationReply(buffer_arg) {\n  return gorush_pb.NotificationReply.deserializeBinary(new Uint8Array(buffer_arg));\n}\n\nfunction serialize_proto_NotificationRequest(arg) {\n  if (!(arg instanceof gorush_pb.NotificationRequest)) {\n    throw new Error('Expected argument of type proto.NotificationRequest');\n  }\n  return Buffer.from(arg.serializeBinary());\n}\n\nfunction deserialize_proto_NotificationRequest(buffer_arg) {\n  return gorush_pb.NotificationRequest.deserializeBinary(new Uint8Array(buffer_arg));\n}\n\n\nvar GorushService = exports.GorushService = {\n  send: {\n    path: '/proto.Gorush/Send',\n    requestStream: false,\n    responseStream: false,\n    requestType: gorush_pb.NotificationRequest,\n    responseType: gorush_pb.NotificationReply,\n    requestSerialize: serialize_proto_NotificationRequest,\n    requestDeserialize: deserialize_proto_NotificationRequest,\n    responseSerialize: serialize_proto_NotificationReply,\n    responseDeserialize: deserialize_proto_NotificationReply,\n  },\n};\n\nexports.GorushClient = grpc.makeGenericClientConstructor(GorushService, 'Gorush');\nvar HealthService = exports.HealthService = {\n  check: {\n    path: '/proto.Health/Check',\n    requestStream: false,\n    responseStream: false,\n    requestType: gorush_pb.HealthCheckRequest,\n    responseType: gorush_pb.HealthCheckResponse,\n    requestSerialize: serialize_proto_HealthCheckRequest,\n    requestDeserialize: deserialize_proto_HealthCheckRequest,\n    responseSerialize: serialize_proto_HealthCheckResponse,\n    responseDeserialize: deserialize_proto_HealthCheckResponse,\n  },\n};\n\nexports.HealthClient = grpc.makeGenericClientConstructor(HealthService, 'Health');\n"
  },
  {
    "path": "rpc/example/node/gorush_pb.js",
    "content": "// source: gorush.proto\n/**\n * @fileoverview\n * @enhanceable\n * @suppress {missingRequire} reports error on implicit type usages.\n * @suppress {messageConventions} JS Compiler reports an error if a variable or\n *     field starts with 'MSG_' and isn't a translatable message.\n * @public\n */\n// GENERATED CODE -- DO NOT EDIT!\n/* eslint-disable */\n// @ts-nocheck\n\nvar jspb = require('google-protobuf');\nvar goog = jspb;\nvar global =\n    (typeof globalThis !== 'undefined' && globalThis) ||\n    (typeof window !== 'undefined' && window) ||\n    (typeof global !== 'undefined' && global) ||\n    (typeof self !== 'undefined' && self) ||\n    (function () { return this; }).call(null) ||\n    Function('return this')();\n\nvar google_protobuf_struct_pb = require('google-protobuf/google/protobuf/struct_pb.js');\ngoog.object.extend(proto, google_protobuf_struct_pb);\ngoog.exportSymbol('proto.proto.Alert', null, global);\ngoog.exportSymbol('proto.proto.FCMOptions', null, global);\ngoog.exportSymbol('proto.proto.HealthCheckRequest', null, global);\ngoog.exportSymbol('proto.proto.HealthCheckResponse', null, global);\ngoog.exportSymbol('proto.proto.HealthCheckResponse.ServingStatus', null, global);\ngoog.exportSymbol('proto.proto.NotificationReply', null, global);\ngoog.exportSymbol('proto.proto.NotificationRequest', null, global);\ngoog.exportSymbol('proto.proto.NotificationRequest.Priority', null, global);\n/**\n * Generated by JsPbCodeGenerator.\n * @param {Array=} opt_data Optional initial data array, typically from a\n * server response, or constructed directly in Javascript. The array is used\n * in place and becomes part of the constructed object. It is not cloned.\n * If no data is provided, the constructed object will be empty, but still\n * valid.\n * @extends {jspb.Message}\n * @constructor\n */\nproto.proto.Alert = function(opt_data) {\n  jspb.Message.initialize(this, opt_data, 0, -1, proto.proto.Alert.repeatedFields_, null);\n};\ngoog.inherits(proto.proto.Alert, jspb.Message);\nif (goog.DEBUG && !COMPILED) {\n  /**\n   * @public\n   * @override\n   */\n  proto.proto.Alert.displayName = 'proto.proto.Alert';\n}\n/**\n * Generated by JsPbCodeGenerator.\n * @param {Array=} opt_data Optional initial data array, typically from a\n * server response, or constructed directly in Javascript. The array is used\n * in place and becomes part of the constructed object. It is not cloned.\n * If no data is provided, the constructed object will be empty, but still\n * valid.\n * @extends {jspb.Message}\n * @constructor\n */\nproto.proto.NotificationRequest = function(opt_data) {\n  jspb.Message.initialize(this, opt_data, 0, -1, proto.proto.NotificationRequest.repeatedFields_, null);\n};\ngoog.inherits(proto.proto.NotificationRequest, jspb.Message);\nif (goog.DEBUG && !COMPILED) {\n  /**\n   * @public\n   * @override\n   */\n  proto.proto.NotificationRequest.displayName = 'proto.proto.NotificationRequest';\n}\n/**\n * Generated by JsPbCodeGenerator.\n * @param {Array=} opt_data Optional initial data array, typically from a\n * server response, or constructed directly in Javascript. The array is used\n * in place and becomes part of the constructed object. It is not cloned.\n * If no data is provided, the constructed object will be empty, but still\n * valid.\n * @extends {jspb.Message}\n * @constructor\n */\nproto.proto.FCMOptions = function(opt_data) {\n  jspb.Message.initialize(this, opt_data, 0, -1, null, null);\n};\ngoog.inherits(proto.proto.FCMOptions, jspb.Message);\nif (goog.DEBUG && !COMPILED) {\n  /**\n   * @public\n   * @override\n   */\n  proto.proto.FCMOptions.displayName = 'proto.proto.FCMOptions';\n}\n/**\n * Generated by JsPbCodeGenerator.\n * @param {Array=} opt_data Optional initial data array, typically from a\n * server response, or constructed directly in Javascript. The array is used\n * in place and becomes part of the constructed object. It is not cloned.\n * If no data is provided, the constructed object will be empty, but still\n * valid.\n * @extends {jspb.Message}\n * @constructor\n */\nproto.proto.NotificationReply = function(opt_data) {\n  jspb.Message.initialize(this, opt_data, 0, -1, null, null);\n};\ngoog.inherits(proto.proto.NotificationReply, jspb.Message);\nif (goog.DEBUG && !COMPILED) {\n  /**\n   * @public\n   * @override\n   */\n  proto.proto.NotificationReply.displayName = 'proto.proto.NotificationReply';\n}\n/**\n * Generated by JsPbCodeGenerator.\n * @param {Array=} opt_data Optional initial data array, typically from a\n * server response, or constructed directly in Javascript. The array is used\n * in place and becomes part of the constructed object. It is not cloned.\n * If no data is provided, the constructed object will be empty, but still\n * valid.\n * @extends {jspb.Message}\n * @constructor\n */\nproto.proto.HealthCheckRequest = function(opt_data) {\n  jspb.Message.initialize(this, opt_data, 0, -1, null, null);\n};\ngoog.inherits(proto.proto.HealthCheckRequest, jspb.Message);\nif (goog.DEBUG && !COMPILED) {\n  /**\n   * @public\n   * @override\n   */\n  proto.proto.HealthCheckRequest.displayName = 'proto.proto.HealthCheckRequest';\n}\n/**\n * Generated by JsPbCodeGenerator.\n * @param {Array=} opt_data Optional initial data array, typically from a\n * server response, or constructed directly in Javascript. The array is used\n * in place and becomes part of the constructed object. It is not cloned.\n * If no data is provided, the constructed object will be empty, but still\n * valid.\n * @extends {jspb.Message}\n * @constructor\n */\nproto.proto.HealthCheckResponse = function(opt_data) {\n  jspb.Message.initialize(this, opt_data, 0, -1, null, null);\n};\ngoog.inherits(proto.proto.HealthCheckResponse, jspb.Message);\nif (goog.DEBUG && !COMPILED) {\n  /**\n   * @public\n   * @override\n   */\n  proto.proto.HealthCheckResponse.displayName = 'proto.proto.HealthCheckResponse';\n}\n\n/**\n * List of repeated fields within this message type.\n * @private {!Array<number>}\n * @const\n */\nproto.proto.Alert.repeatedFields_ = [9,10];\n\n\n\nif (jspb.Message.GENERATE_TO_OBJECT) {\n/**\n * Creates an object representation of this proto.\n * Field names that are reserved in JavaScript and will be renamed to pb_name.\n * Optional fields that are not set will be set to undefined.\n * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.\n * For the list of reserved names please see:\n *     net/proto2/compiler/js/internal/generator.cc#kKeyword.\n * @param {boolean=} opt_includeInstance Deprecated. whether to include the\n *     JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @return {!Object}\n */\nproto.proto.Alert.prototype.toObject = function(opt_includeInstance) {\n  return proto.proto.Alert.toObject(opt_includeInstance, this);\n};\n\n\n/**\n * Static version of the {@see toObject} method.\n * @param {boolean|undefined} includeInstance Deprecated. Whether to include\n *     the JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @param {!proto.proto.Alert} msg The msg instance to transform.\n * @return {!Object}\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.Alert.toObject = function(includeInstance, msg) {\n  var f, obj = {\ntitle: jspb.Message.getFieldWithDefault(msg, 1, \"\"),\nbody: jspb.Message.getFieldWithDefault(msg, 2, \"\"),\nsubtitle: jspb.Message.getFieldWithDefault(msg, 3, \"\"),\naction: jspb.Message.getFieldWithDefault(msg, 4, \"\"),\nactionlockey: jspb.Message.getFieldWithDefault(msg, 5, \"\"),\nlaunchimage: jspb.Message.getFieldWithDefault(msg, 6, \"\"),\nlockey: jspb.Message.getFieldWithDefault(msg, 7, \"\"),\ntitlelockey: jspb.Message.getFieldWithDefault(msg, 8, \"\"),\nlocargsList: (f = jspb.Message.getRepeatedField(msg, 9)) == null ? undefined : f,\ntitlelocargsList: (f = jspb.Message.getRepeatedField(msg, 10)) == null ? undefined : f\n  };\n\n  if (includeInstance) {\n    obj.$jspbMessageInstance = msg;\n  }\n  return obj;\n};\n}\n\n\n/**\n * Deserializes binary data (in protobuf wire format).\n * @param {jspb.ByteSource} bytes The bytes to deserialize.\n * @return {!proto.proto.Alert}\n */\nproto.proto.Alert.deserializeBinary = function(bytes) {\n  var reader = new jspb.BinaryReader(bytes);\n  var msg = new proto.proto.Alert;\n  return proto.proto.Alert.deserializeBinaryFromReader(msg, reader);\n};\n\n\n/**\n * Deserializes binary data (in protobuf wire format) from the\n * given reader into the given message object.\n * @param {!proto.proto.Alert} msg The message object to deserialize into.\n * @param {!jspb.BinaryReader} reader The BinaryReader to use.\n * @return {!proto.proto.Alert}\n */\nproto.proto.Alert.deserializeBinaryFromReader = function(msg, reader) {\n  while (reader.nextField()) {\n    if (reader.isEndGroup()) {\n      break;\n    }\n    var field = reader.getFieldNumber();\n    switch (field) {\n    case 1:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setTitle(value);\n      break;\n    case 2:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setBody(value);\n      break;\n    case 3:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setSubtitle(value);\n      break;\n    case 4:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setAction(value);\n      break;\n    case 5:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setActionlockey(value);\n      break;\n    case 6:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setLaunchimage(value);\n      break;\n    case 7:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setLockey(value);\n      break;\n    case 8:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setTitlelockey(value);\n      break;\n    case 9:\n      var value = /** @type {string} */ (reader.readString());\n      msg.addLocargs(value);\n      break;\n    case 10:\n      var value = /** @type {string} */ (reader.readString());\n      msg.addTitlelocargs(value);\n      break;\n    default:\n      reader.skipField();\n      break;\n    }\n  }\n  return msg;\n};\n\n\n/**\n * Serializes the message to binary data (in protobuf wire format).\n * @return {!Uint8Array}\n */\nproto.proto.Alert.prototype.serializeBinary = function() {\n  var writer = new jspb.BinaryWriter();\n  proto.proto.Alert.serializeBinaryToWriter(this, writer);\n  return writer.getResultBuffer();\n};\n\n\n/**\n * Serializes the given message to binary data (in protobuf wire\n * format), writing to the given BinaryWriter.\n * @param {!proto.proto.Alert} message\n * @param {!jspb.BinaryWriter} writer\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.Alert.serializeBinaryToWriter = function(message, writer) {\n  var f = undefined;\n  f = message.getTitle();\n  if (f.length > 0) {\n    writer.writeString(\n      1,\n      f\n    );\n  }\n  f = message.getBody();\n  if (f.length > 0) {\n    writer.writeString(\n      2,\n      f\n    );\n  }\n  f = message.getSubtitle();\n  if (f.length > 0) {\n    writer.writeString(\n      3,\n      f\n    );\n  }\n  f = message.getAction();\n  if (f.length > 0) {\n    writer.writeString(\n      4,\n      f\n    );\n  }\n  f = message.getActionlockey();\n  if (f.length > 0) {\n    writer.writeString(\n      5,\n      f\n    );\n  }\n  f = message.getLaunchimage();\n  if (f.length > 0) {\n    writer.writeString(\n      6,\n      f\n    );\n  }\n  f = message.getLockey();\n  if (f.length > 0) {\n    writer.writeString(\n      7,\n      f\n    );\n  }\n  f = message.getTitlelockey();\n  if (f.length > 0) {\n    writer.writeString(\n      8,\n      f\n    );\n  }\n  f = message.getLocargsList();\n  if (f.length > 0) {\n    writer.writeRepeatedString(\n      9,\n      f\n    );\n  }\n  f = message.getTitlelocargsList();\n  if (f.length > 0) {\n    writer.writeRepeatedString(\n      10,\n      f\n    );\n  }\n};\n\n\n/**\n * optional string title = 1;\n * @return {string}\n */\nproto.proto.Alert.prototype.getTitle = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setTitle = function(value) {\n  return jspb.Message.setProto3StringField(this, 1, value);\n};\n\n\n/**\n * optional string body = 2;\n * @return {string}\n */\nproto.proto.Alert.prototype.getBody = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setBody = function(value) {\n  return jspb.Message.setProto3StringField(this, 2, value);\n};\n\n\n/**\n * optional string subtitle = 3;\n * @return {string}\n */\nproto.proto.Alert.prototype.getSubtitle = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setSubtitle = function(value) {\n  return jspb.Message.setProto3StringField(this, 3, value);\n};\n\n\n/**\n * optional string action = 4;\n * @return {string}\n */\nproto.proto.Alert.prototype.getAction = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setAction = function(value) {\n  return jspb.Message.setProto3StringField(this, 4, value);\n};\n\n\n/**\n * optional string actionLocKey = 5;\n * @return {string}\n */\nproto.proto.Alert.prototype.getActionlockey = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setActionlockey = function(value) {\n  return jspb.Message.setProto3StringField(this, 5, value);\n};\n\n\n/**\n * optional string launchImage = 6;\n * @return {string}\n */\nproto.proto.Alert.prototype.getLaunchimage = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 6, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setLaunchimage = function(value) {\n  return jspb.Message.setProto3StringField(this, 6, value);\n};\n\n\n/**\n * optional string locKey = 7;\n * @return {string}\n */\nproto.proto.Alert.prototype.getLockey = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 7, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setLockey = function(value) {\n  return jspb.Message.setProto3StringField(this, 7, value);\n};\n\n\n/**\n * optional string titleLocKey = 8;\n * @return {string}\n */\nproto.proto.Alert.prototype.getTitlelockey = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setTitlelockey = function(value) {\n  return jspb.Message.setProto3StringField(this, 8, value);\n};\n\n\n/**\n * repeated string locArgs = 9;\n * @return {!Array<string>}\n */\nproto.proto.Alert.prototype.getLocargsList = function() {\n  return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 9));\n};\n\n\n/**\n * @param {!Array<string>} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setLocargsList = function(value) {\n  return jspb.Message.setField(this, 9, value || []);\n};\n\n\n/**\n * @param {string} value\n * @param {number=} opt_index\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.addLocargs = function(value, opt_index) {\n  return jspb.Message.addToRepeatedField(this, 9, value, opt_index);\n};\n\n\n/**\n * Clears the list making it empty but non-null.\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.clearLocargsList = function() {\n  return this.setLocargsList([]);\n};\n\n\n/**\n * repeated string titleLocArgs = 10;\n * @return {!Array<string>}\n */\nproto.proto.Alert.prototype.getTitlelocargsList = function() {\n  return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 10));\n};\n\n\n/**\n * @param {!Array<string>} value\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.setTitlelocargsList = function(value) {\n  return jspb.Message.setField(this, 10, value || []);\n};\n\n\n/**\n * @param {string} value\n * @param {number=} opt_index\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.addTitlelocargs = function(value, opt_index) {\n  return jspb.Message.addToRepeatedField(this, 10, value, opt_index);\n};\n\n\n/**\n * Clears the list making it empty but non-null.\n * @return {!proto.proto.Alert} returns this\n */\nproto.proto.Alert.prototype.clearTitlelocargsList = function() {\n  return this.setTitlelocargsList([]);\n};\n\n\n\n/**\n * List of repeated fields within this message type.\n * @private {!Array<number>}\n * @const\n */\nproto.proto.NotificationRequest.repeatedFields_ = [1];\n\n\n\nif (jspb.Message.GENERATE_TO_OBJECT) {\n/**\n * Creates an object representation of this proto.\n * Field names that are reserved in JavaScript and will be renamed to pb_name.\n * Optional fields that are not set will be set to undefined.\n * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.\n * For the list of reserved names please see:\n *     net/proto2/compiler/js/internal/generator.cc#kKeyword.\n * @param {boolean=} opt_includeInstance Deprecated. whether to include the\n *     JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @return {!Object}\n */\nproto.proto.NotificationRequest.prototype.toObject = function(opt_includeInstance) {\n  return proto.proto.NotificationRequest.toObject(opt_includeInstance, this);\n};\n\n\n/**\n * Static version of the {@see toObject} method.\n * @param {boolean|undefined} includeInstance Deprecated. Whether to include\n *     the JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @param {!proto.proto.NotificationRequest} msg The msg instance to transform.\n * @return {!Object}\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.NotificationRequest.toObject = function(includeInstance, msg) {\n  var f, obj = {\ntokensList: (f = jspb.Message.getRepeatedField(msg, 1)) == null ? undefined : f,\nplatform: jspb.Message.getFieldWithDefault(msg, 2, 0),\nmessage: jspb.Message.getFieldWithDefault(msg, 3, \"\"),\ntitle: jspb.Message.getFieldWithDefault(msg, 4, \"\"),\ntopic: jspb.Message.getFieldWithDefault(msg, 5, \"\"),\nkey: jspb.Message.getFieldWithDefault(msg, 6, \"\"),\nbadge: jspb.Message.getFieldWithDefault(msg, 7, 0),\ncategory: jspb.Message.getFieldWithDefault(msg, 8, \"\"),\nalert: (f = msg.getAlert()) && proto.proto.Alert.toObject(includeInstance, f),\nsound: jspb.Message.getFieldWithDefault(msg, 10, \"\"),\ncontentavailable: jspb.Message.getBooleanFieldWithDefault(msg, 11, false),\nthreadid: jspb.Message.getFieldWithDefault(msg, 12, \"\"),\nmutablecontent: jspb.Message.getBooleanFieldWithDefault(msg, 13, false),\ndata: (f = msg.getData()) && google_protobuf_struct_pb.Struct.toObject(includeInstance, f),\nimage: jspb.Message.getFieldWithDefault(msg, 15, \"\"),\npriority: jspb.Message.getFieldWithDefault(msg, 16, 0),\nid: jspb.Message.getFieldWithDefault(msg, 17, \"\"),\npushtype: jspb.Message.getFieldWithDefault(msg, 18, \"\"),\ndevelopment: jspb.Message.getBooleanFieldWithDefault(msg, 19, false),\nfcmoptions: (f = msg.getFcmoptions()) && proto.proto.FCMOptions.toObject(includeInstance, f)\n  };\n\n  if (includeInstance) {\n    obj.$jspbMessageInstance = msg;\n  }\n  return obj;\n};\n}\n\n\n/**\n * Deserializes binary data (in protobuf wire format).\n * @param {jspb.ByteSource} bytes The bytes to deserialize.\n * @return {!proto.proto.NotificationRequest}\n */\nproto.proto.NotificationRequest.deserializeBinary = function(bytes) {\n  var reader = new jspb.BinaryReader(bytes);\n  var msg = new proto.proto.NotificationRequest;\n  return proto.proto.NotificationRequest.deserializeBinaryFromReader(msg, reader);\n};\n\n\n/**\n * Deserializes binary data (in protobuf wire format) from the\n * given reader into the given message object.\n * @param {!proto.proto.NotificationRequest} msg The message object to deserialize into.\n * @param {!jspb.BinaryReader} reader The BinaryReader to use.\n * @return {!proto.proto.NotificationRequest}\n */\nproto.proto.NotificationRequest.deserializeBinaryFromReader = function(msg, reader) {\n  while (reader.nextField()) {\n    if (reader.isEndGroup()) {\n      break;\n    }\n    var field = reader.getFieldNumber();\n    switch (field) {\n    case 1:\n      var value = /** @type {string} */ (reader.readString());\n      msg.addTokens(value);\n      break;\n    case 2:\n      var value = /** @type {number} */ (reader.readInt32());\n      msg.setPlatform(value);\n      break;\n    case 3:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setMessage(value);\n      break;\n    case 4:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setTitle(value);\n      break;\n    case 5:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setTopic(value);\n      break;\n    case 6:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setKey(value);\n      break;\n    case 7:\n      var value = /** @type {number} */ (reader.readInt32());\n      msg.setBadge(value);\n      break;\n    case 8:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setCategory(value);\n      break;\n    case 9:\n      var value = new proto.proto.Alert;\n      reader.readMessage(value,proto.proto.Alert.deserializeBinaryFromReader);\n      msg.setAlert(value);\n      break;\n    case 10:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setSound(value);\n      break;\n    case 11:\n      var value = /** @type {boolean} */ (reader.readBool());\n      msg.setContentavailable(value);\n      break;\n    case 12:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setThreadid(value);\n      break;\n    case 13:\n      var value = /** @type {boolean} */ (reader.readBool());\n      msg.setMutablecontent(value);\n      break;\n    case 14:\n      var value = new google_protobuf_struct_pb.Struct;\n      reader.readMessage(value,google_protobuf_struct_pb.Struct.deserializeBinaryFromReader);\n      msg.setData(value);\n      break;\n    case 15:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setImage(value);\n      break;\n    case 16:\n      var value = /** @type {!proto.proto.NotificationRequest.Priority} */ (reader.readEnum());\n      msg.setPriority(value);\n      break;\n    case 17:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setId(value);\n      break;\n    case 18:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setPushtype(value);\n      break;\n    case 19:\n      var value = /** @type {boolean} */ (reader.readBool());\n      msg.setDevelopment(value);\n      break;\n    case 20:\n      var value = new proto.proto.FCMOptions;\n      reader.readMessage(value,proto.proto.FCMOptions.deserializeBinaryFromReader);\n      msg.setFcmoptions(value);\n      break;\n    default:\n      reader.skipField();\n      break;\n    }\n  }\n  return msg;\n};\n\n\n/**\n * Serializes the message to binary data (in protobuf wire format).\n * @return {!Uint8Array}\n */\nproto.proto.NotificationRequest.prototype.serializeBinary = function() {\n  var writer = new jspb.BinaryWriter();\n  proto.proto.NotificationRequest.serializeBinaryToWriter(this, writer);\n  return writer.getResultBuffer();\n};\n\n\n/**\n * Serializes the given message to binary data (in protobuf wire\n * format), writing to the given BinaryWriter.\n * @param {!proto.proto.NotificationRequest} message\n * @param {!jspb.BinaryWriter} writer\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.NotificationRequest.serializeBinaryToWriter = function(message, writer) {\n  var f = undefined;\n  f = message.getTokensList();\n  if (f.length > 0) {\n    writer.writeRepeatedString(\n      1,\n      f\n    );\n  }\n  f = message.getPlatform();\n  if (f !== 0) {\n    writer.writeInt32(\n      2,\n      f\n    );\n  }\n  f = message.getMessage();\n  if (f.length > 0) {\n    writer.writeString(\n      3,\n      f\n    );\n  }\n  f = message.getTitle();\n  if (f.length > 0) {\n    writer.writeString(\n      4,\n      f\n    );\n  }\n  f = message.getTopic();\n  if (f.length > 0) {\n    writer.writeString(\n      5,\n      f\n    );\n  }\n  f = message.getKey();\n  if (f.length > 0) {\n    writer.writeString(\n      6,\n      f\n    );\n  }\n  f = message.getBadge();\n  if (f !== 0) {\n    writer.writeInt32(\n      7,\n      f\n    );\n  }\n  f = message.getCategory();\n  if (f.length > 0) {\n    writer.writeString(\n      8,\n      f\n    );\n  }\n  f = message.getAlert();\n  if (f != null) {\n    writer.writeMessage(\n      9,\n      f,\n      proto.proto.Alert.serializeBinaryToWriter\n    );\n  }\n  f = message.getSound();\n  if (f.length > 0) {\n    writer.writeString(\n      10,\n      f\n    );\n  }\n  f = message.getContentavailable();\n  if (f) {\n    writer.writeBool(\n      11,\n      f\n    );\n  }\n  f = message.getThreadid();\n  if (f.length > 0) {\n    writer.writeString(\n      12,\n      f\n    );\n  }\n  f = message.getMutablecontent();\n  if (f) {\n    writer.writeBool(\n      13,\n      f\n    );\n  }\n  f = message.getData();\n  if (f != null) {\n    writer.writeMessage(\n      14,\n      f,\n      google_protobuf_struct_pb.Struct.serializeBinaryToWriter\n    );\n  }\n  f = message.getImage();\n  if (f.length > 0) {\n    writer.writeString(\n      15,\n      f\n    );\n  }\n  f = message.getPriority();\n  if (f !== 0.0) {\n    writer.writeEnum(\n      16,\n      f\n    );\n  }\n  f = message.getId();\n  if (f.length > 0) {\n    writer.writeString(\n      17,\n      f\n    );\n  }\n  f = message.getPushtype();\n  if (f.length > 0) {\n    writer.writeString(\n      18,\n      f\n    );\n  }\n  f = message.getDevelopment();\n  if (f) {\n    writer.writeBool(\n      19,\n      f\n    );\n  }\n  f = message.getFcmoptions();\n  if (f != null) {\n    writer.writeMessage(\n      20,\n      f,\n      proto.proto.FCMOptions.serializeBinaryToWriter\n    );\n  }\n};\n\n\n/**\n * @enum {number}\n */\nproto.proto.NotificationRequest.Priority = {\n  NORMAL: 0,\n  HIGH: 1\n};\n\n/**\n * repeated string tokens = 1;\n * @return {!Array<string>}\n */\nproto.proto.NotificationRequest.prototype.getTokensList = function() {\n  return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 1));\n};\n\n\n/**\n * @param {!Array<string>} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setTokensList = function(value) {\n  return jspb.Message.setField(this, 1, value || []);\n};\n\n\n/**\n * @param {string} value\n * @param {number=} opt_index\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.addTokens = function(value, opt_index) {\n  return jspb.Message.addToRepeatedField(this, 1, value, opt_index);\n};\n\n\n/**\n * Clears the list making it empty but non-null.\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.clearTokensList = function() {\n  return this.setTokensList([]);\n};\n\n\n/**\n * optional int32 platform = 2;\n * @return {number}\n */\nproto.proto.NotificationRequest.prototype.getPlatform = function() {\n  return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0));\n};\n\n\n/**\n * @param {number} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setPlatform = function(value) {\n  return jspb.Message.setProto3IntField(this, 2, value);\n};\n\n\n/**\n * optional string message = 3;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getMessage = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setMessage = function(value) {\n  return jspb.Message.setProto3StringField(this, 3, value);\n};\n\n\n/**\n * optional string title = 4;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getTitle = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setTitle = function(value) {\n  return jspb.Message.setProto3StringField(this, 4, value);\n};\n\n\n/**\n * optional string topic = 5;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getTopic = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setTopic = function(value) {\n  return jspb.Message.setProto3StringField(this, 5, value);\n};\n\n\n/**\n * optional string key = 6;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getKey = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 6, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setKey = function(value) {\n  return jspb.Message.setProto3StringField(this, 6, value);\n};\n\n\n/**\n * optional int32 badge = 7;\n * @return {number}\n */\nproto.proto.NotificationRequest.prototype.getBadge = function() {\n  return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 7, 0));\n};\n\n\n/**\n * @param {number} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setBadge = function(value) {\n  return jspb.Message.setProto3IntField(this, 7, value);\n};\n\n\n/**\n * optional string category = 8;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getCategory = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setCategory = function(value) {\n  return jspb.Message.setProto3StringField(this, 8, value);\n};\n\n\n/**\n * optional Alert alert = 9;\n * @return {?proto.proto.Alert}\n */\nproto.proto.NotificationRequest.prototype.getAlert = function() {\n  return /** @type{?proto.proto.Alert} */ (\n    jspb.Message.getWrapperField(this, proto.proto.Alert, 9));\n};\n\n\n/**\n * @param {?proto.proto.Alert|undefined} value\n * @return {!proto.proto.NotificationRequest} returns this\n*/\nproto.proto.NotificationRequest.prototype.setAlert = function(value) {\n  return jspb.Message.setWrapperField(this, 9, value);\n};\n\n\n/**\n * Clears the message field making it undefined.\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.clearAlert = function() {\n  return this.setAlert(undefined);\n};\n\n\n/**\n * Returns whether this field is set.\n * @return {boolean}\n */\nproto.proto.NotificationRequest.prototype.hasAlert = function() {\n  return jspb.Message.getField(this, 9) != null;\n};\n\n\n/**\n * optional string sound = 10;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getSound = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 10, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setSound = function(value) {\n  return jspb.Message.setProto3StringField(this, 10, value);\n};\n\n\n/**\n * optional bool contentAvailable = 11;\n * @return {boolean}\n */\nproto.proto.NotificationRequest.prototype.getContentavailable = function() {\n  return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 11, false));\n};\n\n\n/**\n * @param {boolean} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setContentavailable = function(value) {\n  return jspb.Message.setProto3BooleanField(this, 11, value);\n};\n\n\n/**\n * optional string threadID = 12;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getThreadid = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 12, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setThreadid = function(value) {\n  return jspb.Message.setProto3StringField(this, 12, value);\n};\n\n\n/**\n * optional bool mutableContent = 13;\n * @return {boolean}\n */\nproto.proto.NotificationRequest.prototype.getMutablecontent = function() {\n  return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 13, false));\n};\n\n\n/**\n * @param {boolean} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setMutablecontent = function(value) {\n  return jspb.Message.setProto3BooleanField(this, 13, value);\n};\n\n\n/**\n * optional google.protobuf.Struct data = 14;\n * @return {?proto.google.protobuf.Struct}\n */\nproto.proto.NotificationRequest.prototype.getData = function() {\n  return /** @type{?proto.google.protobuf.Struct} */ (\n    jspb.Message.getWrapperField(this, google_protobuf_struct_pb.Struct, 14));\n};\n\n\n/**\n * @param {?proto.google.protobuf.Struct|undefined} value\n * @return {!proto.proto.NotificationRequest} returns this\n*/\nproto.proto.NotificationRequest.prototype.setData = function(value) {\n  return jspb.Message.setWrapperField(this, 14, value);\n};\n\n\n/**\n * Clears the message field making it undefined.\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.clearData = function() {\n  return this.setData(undefined);\n};\n\n\n/**\n * Returns whether this field is set.\n * @return {boolean}\n */\nproto.proto.NotificationRequest.prototype.hasData = function() {\n  return jspb.Message.getField(this, 14) != null;\n};\n\n\n/**\n * optional string image = 15;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getImage = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 15, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setImage = function(value) {\n  return jspb.Message.setProto3StringField(this, 15, value);\n};\n\n\n/**\n * optional Priority priority = 16;\n * @return {!proto.proto.NotificationRequest.Priority}\n */\nproto.proto.NotificationRequest.prototype.getPriority = function() {\n  return /** @type {!proto.proto.NotificationRequest.Priority} */ (jspb.Message.getFieldWithDefault(this, 16, 0));\n};\n\n\n/**\n * @param {!proto.proto.NotificationRequest.Priority} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setPriority = function(value) {\n  return jspb.Message.setProto3EnumField(this, 16, value);\n};\n\n\n/**\n * optional string ID = 17;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getId = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 17, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setId = function(value) {\n  return jspb.Message.setProto3StringField(this, 17, value);\n};\n\n\n/**\n * optional string pushType = 18;\n * @return {string}\n */\nproto.proto.NotificationRequest.prototype.getPushtype = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 18, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setPushtype = function(value) {\n  return jspb.Message.setProto3StringField(this, 18, value);\n};\n\n\n/**\n * optional bool development = 19;\n * @return {boolean}\n */\nproto.proto.NotificationRequest.prototype.getDevelopment = function() {\n  return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 19, false));\n};\n\n\n/**\n * @param {boolean} value\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.setDevelopment = function(value) {\n  return jspb.Message.setProto3BooleanField(this, 19, value);\n};\n\n\n/**\n * optional FCMOptions fcmOptions = 20;\n * @return {?proto.proto.FCMOptions}\n */\nproto.proto.NotificationRequest.prototype.getFcmoptions = function() {\n  return /** @type{?proto.proto.FCMOptions} */ (\n    jspb.Message.getWrapperField(this, proto.proto.FCMOptions, 20));\n};\n\n\n/**\n * @param {?proto.proto.FCMOptions|undefined} value\n * @return {!proto.proto.NotificationRequest} returns this\n*/\nproto.proto.NotificationRequest.prototype.setFcmoptions = function(value) {\n  return jspb.Message.setWrapperField(this, 20, value);\n};\n\n\n/**\n * Clears the message field making it undefined.\n * @return {!proto.proto.NotificationRequest} returns this\n */\nproto.proto.NotificationRequest.prototype.clearFcmoptions = function() {\n  return this.setFcmoptions(undefined);\n};\n\n\n/**\n * Returns whether this field is set.\n * @return {boolean}\n */\nproto.proto.NotificationRequest.prototype.hasFcmoptions = function() {\n  return jspb.Message.getField(this, 20) != null;\n};\n\n\n\n\n\nif (jspb.Message.GENERATE_TO_OBJECT) {\n/**\n * Creates an object representation of this proto.\n * Field names that are reserved in JavaScript and will be renamed to pb_name.\n * Optional fields that are not set will be set to undefined.\n * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.\n * For the list of reserved names please see:\n *     net/proto2/compiler/js/internal/generator.cc#kKeyword.\n * @param {boolean=} opt_includeInstance Deprecated. whether to include the\n *     JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @return {!Object}\n */\nproto.proto.FCMOptions.prototype.toObject = function(opt_includeInstance) {\n  return proto.proto.FCMOptions.toObject(opt_includeInstance, this);\n};\n\n\n/**\n * Static version of the {@see toObject} method.\n * @param {boolean|undefined} includeInstance Deprecated. Whether to include\n *     the JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @param {!proto.proto.FCMOptions} msg The msg instance to transform.\n * @return {!Object}\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.FCMOptions.toObject = function(includeInstance, msg) {\n  var f, obj = {\nanalyticslabel: jspb.Message.getFieldWithDefault(msg, 1, \"\")\n  };\n\n  if (includeInstance) {\n    obj.$jspbMessageInstance = msg;\n  }\n  return obj;\n};\n}\n\n\n/**\n * Deserializes binary data (in protobuf wire format).\n * @param {jspb.ByteSource} bytes The bytes to deserialize.\n * @return {!proto.proto.FCMOptions}\n */\nproto.proto.FCMOptions.deserializeBinary = function(bytes) {\n  var reader = new jspb.BinaryReader(bytes);\n  var msg = new proto.proto.FCMOptions;\n  return proto.proto.FCMOptions.deserializeBinaryFromReader(msg, reader);\n};\n\n\n/**\n * Deserializes binary data (in protobuf wire format) from the\n * given reader into the given message object.\n * @param {!proto.proto.FCMOptions} msg The message object to deserialize into.\n * @param {!jspb.BinaryReader} reader The BinaryReader to use.\n * @return {!proto.proto.FCMOptions}\n */\nproto.proto.FCMOptions.deserializeBinaryFromReader = function(msg, reader) {\n  while (reader.nextField()) {\n    if (reader.isEndGroup()) {\n      break;\n    }\n    var field = reader.getFieldNumber();\n    switch (field) {\n    case 1:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setAnalyticslabel(value);\n      break;\n    default:\n      reader.skipField();\n      break;\n    }\n  }\n  return msg;\n};\n\n\n/**\n * Serializes the message to binary data (in protobuf wire format).\n * @return {!Uint8Array}\n */\nproto.proto.FCMOptions.prototype.serializeBinary = function() {\n  var writer = new jspb.BinaryWriter();\n  proto.proto.FCMOptions.serializeBinaryToWriter(this, writer);\n  return writer.getResultBuffer();\n};\n\n\n/**\n * Serializes the given message to binary data (in protobuf wire\n * format), writing to the given BinaryWriter.\n * @param {!proto.proto.FCMOptions} message\n * @param {!jspb.BinaryWriter} writer\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.FCMOptions.serializeBinaryToWriter = function(message, writer) {\n  var f = undefined;\n  f = message.getAnalyticslabel();\n  if (f.length > 0) {\n    writer.writeString(\n      1,\n      f\n    );\n  }\n};\n\n\n/**\n * optional string analyticsLabel = 1;\n * @return {string}\n */\nproto.proto.FCMOptions.prototype.getAnalyticslabel = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.FCMOptions} returns this\n */\nproto.proto.FCMOptions.prototype.setAnalyticslabel = function(value) {\n  return jspb.Message.setProto3StringField(this, 1, value);\n};\n\n\n\n\n\nif (jspb.Message.GENERATE_TO_OBJECT) {\n/**\n * Creates an object representation of this proto.\n * Field names that are reserved in JavaScript and will be renamed to pb_name.\n * Optional fields that are not set will be set to undefined.\n * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.\n * For the list of reserved names please see:\n *     net/proto2/compiler/js/internal/generator.cc#kKeyword.\n * @param {boolean=} opt_includeInstance Deprecated. whether to include the\n *     JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @return {!Object}\n */\nproto.proto.NotificationReply.prototype.toObject = function(opt_includeInstance) {\n  return proto.proto.NotificationReply.toObject(opt_includeInstance, this);\n};\n\n\n/**\n * Static version of the {@see toObject} method.\n * @param {boolean|undefined} includeInstance Deprecated. Whether to include\n *     the JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @param {!proto.proto.NotificationReply} msg The msg instance to transform.\n * @return {!Object}\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.NotificationReply.toObject = function(includeInstance, msg) {\n  var f, obj = {\nsuccess: jspb.Message.getBooleanFieldWithDefault(msg, 1, false),\ncounts: jspb.Message.getFieldWithDefault(msg, 2, 0)\n  };\n\n  if (includeInstance) {\n    obj.$jspbMessageInstance = msg;\n  }\n  return obj;\n};\n}\n\n\n/**\n * Deserializes binary data (in protobuf wire format).\n * @param {jspb.ByteSource} bytes The bytes to deserialize.\n * @return {!proto.proto.NotificationReply}\n */\nproto.proto.NotificationReply.deserializeBinary = function(bytes) {\n  var reader = new jspb.BinaryReader(bytes);\n  var msg = new proto.proto.NotificationReply;\n  return proto.proto.NotificationReply.deserializeBinaryFromReader(msg, reader);\n};\n\n\n/**\n * Deserializes binary data (in protobuf wire format) from the\n * given reader into the given message object.\n * @param {!proto.proto.NotificationReply} msg The message object to deserialize into.\n * @param {!jspb.BinaryReader} reader The BinaryReader to use.\n * @return {!proto.proto.NotificationReply}\n */\nproto.proto.NotificationReply.deserializeBinaryFromReader = function(msg, reader) {\n  while (reader.nextField()) {\n    if (reader.isEndGroup()) {\n      break;\n    }\n    var field = reader.getFieldNumber();\n    switch (field) {\n    case 1:\n      var value = /** @type {boolean} */ (reader.readBool());\n      msg.setSuccess(value);\n      break;\n    case 2:\n      var value = /** @type {number} */ (reader.readInt32());\n      msg.setCounts(value);\n      break;\n    default:\n      reader.skipField();\n      break;\n    }\n  }\n  return msg;\n};\n\n\n/**\n * Serializes the message to binary data (in protobuf wire format).\n * @return {!Uint8Array}\n */\nproto.proto.NotificationReply.prototype.serializeBinary = function() {\n  var writer = new jspb.BinaryWriter();\n  proto.proto.NotificationReply.serializeBinaryToWriter(this, writer);\n  return writer.getResultBuffer();\n};\n\n\n/**\n * Serializes the given message to binary data (in protobuf wire\n * format), writing to the given BinaryWriter.\n * @param {!proto.proto.NotificationReply} message\n * @param {!jspb.BinaryWriter} writer\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.NotificationReply.serializeBinaryToWriter = function(message, writer) {\n  var f = undefined;\n  f = message.getSuccess();\n  if (f) {\n    writer.writeBool(\n      1,\n      f\n    );\n  }\n  f = message.getCounts();\n  if (f !== 0) {\n    writer.writeInt32(\n      2,\n      f\n    );\n  }\n};\n\n\n/**\n * optional bool success = 1;\n * @return {boolean}\n */\nproto.proto.NotificationReply.prototype.getSuccess = function() {\n  return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false));\n};\n\n\n/**\n * @param {boolean} value\n * @return {!proto.proto.NotificationReply} returns this\n */\nproto.proto.NotificationReply.prototype.setSuccess = function(value) {\n  return jspb.Message.setProto3BooleanField(this, 1, value);\n};\n\n\n/**\n * optional int32 counts = 2;\n * @return {number}\n */\nproto.proto.NotificationReply.prototype.getCounts = function() {\n  return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0));\n};\n\n\n/**\n * @param {number} value\n * @return {!proto.proto.NotificationReply} returns this\n */\nproto.proto.NotificationReply.prototype.setCounts = function(value) {\n  return jspb.Message.setProto3IntField(this, 2, value);\n};\n\n\n\n\n\nif (jspb.Message.GENERATE_TO_OBJECT) {\n/**\n * Creates an object representation of this proto.\n * Field names that are reserved in JavaScript and will be renamed to pb_name.\n * Optional fields that are not set will be set to undefined.\n * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.\n * For the list of reserved names please see:\n *     net/proto2/compiler/js/internal/generator.cc#kKeyword.\n * @param {boolean=} opt_includeInstance Deprecated. whether to include the\n *     JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @return {!Object}\n */\nproto.proto.HealthCheckRequest.prototype.toObject = function(opt_includeInstance) {\n  return proto.proto.HealthCheckRequest.toObject(opt_includeInstance, this);\n};\n\n\n/**\n * Static version of the {@see toObject} method.\n * @param {boolean|undefined} includeInstance Deprecated. Whether to include\n *     the JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @param {!proto.proto.HealthCheckRequest} msg The msg instance to transform.\n * @return {!Object}\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.HealthCheckRequest.toObject = function(includeInstance, msg) {\n  var f, obj = {\nservice: jspb.Message.getFieldWithDefault(msg, 1, \"\")\n  };\n\n  if (includeInstance) {\n    obj.$jspbMessageInstance = msg;\n  }\n  return obj;\n};\n}\n\n\n/**\n * Deserializes binary data (in protobuf wire format).\n * @param {jspb.ByteSource} bytes The bytes to deserialize.\n * @return {!proto.proto.HealthCheckRequest}\n */\nproto.proto.HealthCheckRequest.deserializeBinary = function(bytes) {\n  var reader = new jspb.BinaryReader(bytes);\n  var msg = new proto.proto.HealthCheckRequest;\n  return proto.proto.HealthCheckRequest.deserializeBinaryFromReader(msg, reader);\n};\n\n\n/**\n * Deserializes binary data (in protobuf wire format) from the\n * given reader into the given message object.\n * @param {!proto.proto.HealthCheckRequest} msg The message object to deserialize into.\n * @param {!jspb.BinaryReader} reader The BinaryReader to use.\n * @return {!proto.proto.HealthCheckRequest}\n */\nproto.proto.HealthCheckRequest.deserializeBinaryFromReader = function(msg, reader) {\n  while (reader.nextField()) {\n    if (reader.isEndGroup()) {\n      break;\n    }\n    var field = reader.getFieldNumber();\n    switch (field) {\n    case 1:\n      var value = /** @type {string} */ (reader.readString());\n      msg.setService(value);\n      break;\n    default:\n      reader.skipField();\n      break;\n    }\n  }\n  return msg;\n};\n\n\n/**\n * Serializes the message to binary data (in protobuf wire format).\n * @return {!Uint8Array}\n */\nproto.proto.HealthCheckRequest.prototype.serializeBinary = function() {\n  var writer = new jspb.BinaryWriter();\n  proto.proto.HealthCheckRequest.serializeBinaryToWriter(this, writer);\n  return writer.getResultBuffer();\n};\n\n\n/**\n * Serializes the given message to binary data (in protobuf wire\n * format), writing to the given BinaryWriter.\n * @param {!proto.proto.HealthCheckRequest} message\n * @param {!jspb.BinaryWriter} writer\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.HealthCheckRequest.serializeBinaryToWriter = function(message, writer) {\n  var f = undefined;\n  f = message.getService();\n  if (f.length > 0) {\n    writer.writeString(\n      1,\n      f\n    );\n  }\n};\n\n\n/**\n * optional string service = 1;\n * @return {string}\n */\nproto.proto.HealthCheckRequest.prototype.getService = function() {\n  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, \"\"));\n};\n\n\n/**\n * @param {string} value\n * @return {!proto.proto.HealthCheckRequest} returns this\n */\nproto.proto.HealthCheckRequest.prototype.setService = function(value) {\n  return jspb.Message.setProto3StringField(this, 1, value);\n};\n\n\n\n\n\nif (jspb.Message.GENERATE_TO_OBJECT) {\n/**\n * Creates an object representation of this proto.\n * Field names that are reserved in JavaScript and will be renamed to pb_name.\n * Optional fields that are not set will be set to undefined.\n * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.\n * For the list of reserved names please see:\n *     net/proto2/compiler/js/internal/generator.cc#kKeyword.\n * @param {boolean=} opt_includeInstance Deprecated. whether to include the\n *     JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @return {!Object}\n */\nproto.proto.HealthCheckResponse.prototype.toObject = function(opt_includeInstance) {\n  return proto.proto.HealthCheckResponse.toObject(opt_includeInstance, this);\n};\n\n\n/**\n * Static version of the {@see toObject} method.\n * @param {boolean|undefined} includeInstance Deprecated. Whether to include\n *     the JSPB instance for transitional soy proto support:\n *     http://goto/soy-param-migration\n * @param {!proto.proto.HealthCheckResponse} msg The msg instance to transform.\n * @return {!Object}\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.HealthCheckResponse.toObject = function(includeInstance, msg) {\n  var f, obj = {\nstatus: jspb.Message.getFieldWithDefault(msg, 1, 0)\n  };\n\n  if (includeInstance) {\n    obj.$jspbMessageInstance = msg;\n  }\n  return obj;\n};\n}\n\n\n/**\n * Deserializes binary data (in protobuf wire format).\n * @param {jspb.ByteSource} bytes The bytes to deserialize.\n * @return {!proto.proto.HealthCheckResponse}\n */\nproto.proto.HealthCheckResponse.deserializeBinary = function(bytes) {\n  var reader = new jspb.BinaryReader(bytes);\n  var msg = new proto.proto.HealthCheckResponse;\n  return proto.proto.HealthCheckResponse.deserializeBinaryFromReader(msg, reader);\n};\n\n\n/**\n * Deserializes binary data (in protobuf wire format) from the\n * given reader into the given message object.\n * @param {!proto.proto.HealthCheckResponse} msg The message object to deserialize into.\n * @param {!jspb.BinaryReader} reader The BinaryReader to use.\n * @return {!proto.proto.HealthCheckResponse}\n */\nproto.proto.HealthCheckResponse.deserializeBinaryFromReader = function(msg, reader) {\n  while (reader.nextField()) {\n    if (reader.isEndGroup()) {\n      break;\n    }\n    var field = reader.getFieldNumber();\n    switch (field) {\n    case 1:\n      var value = /** @type {!proto.proto.HealthCheckResponse.ServingStatus} */ (reader.readEnum());\n      msg.setStatus(value);\n      break;\n    default:\n      reader.skipField();\n      break;\n    }\n  }\n  return msg;\n};\n\n\n/**\n * Serializes the message to binary data (in protobuf wire format).\n * @return {!Uint8Array}\n */\nproto.proto.HealthCheckResponse.prototype.serializeBinary = function() {\n  var writer = new jspb.BinaryWriter();\n  proto.proto.HealthCheckResponse.serializeBinaryToWriter(this, writer);\n  return writer.getResultBuffer();\n};\n\n\n/**\n * Serializes the given message to binary data (in protobuf wire\n * format), writing to the given BinaryWriter.\n * @param {!proto.proto.HealthCheckResponse} message\n * @param {!jspb.BinaryWriter} writer\n * @suppress {unusedLocalVariables} f is only used for nested messages\n */\nproto.proto.HealthCheckResponse.serializeBinaryToWriter = function(message, writer) {\n  var f = undefined;\n  f = message.getStatus();\n  if (f !== 0.0) {\n    writer.writeEnum(\n      1,\n      f\n    );\n  }\n};\n\n\n/**\n * @enum {number}\n */\nproto.proto.HealthCheckResponse.ServingStatus = {\n  UNKNOWN: 0,\n  SERVING: 1,\n  NOT_SERVING: 2\n};\n\n/**\n * optional ServingStatus status = 1;\n * @return {!proto.proto.HealthCheckResponse.ServingStatus}\n */\nproto.proto.HealthCheckResponse.prototype.getStatus = function() {\n  return /** @type {!proto.proto.HealthCheckResponse.ServingStatus} */ (jspb.Message.getFieldWithDefault(this, 1, 0));\n};\n\n\n/**\n * @param {!proto.proto.HealthCheckResponse.ServingStatus} value\n * @return {!proto.proto.HealthCheckResponse} returns this\n */\nproto.proto.HealthCheckResponse.prototype.setStatus = function(value) {\n  return jspb.Message.setProto3EnumField(this, 1, value);\n};\n\n\ngoog.object.extend(exports, proto.proto);\n"
  },
  {
    "path": "rpc/example/node/package.json",
    "content": "{\n  \"name\": \"gorush-examples\",\n  \"version\": \"0.1.0\",\n  \"dependencies\": {\n    \"async\": \"^3.2.6\",\n    \"@grpc/grpc-js\": \"^1.12.6\",\n    \"google-protobuf\": \"^3.21.4\",\n    \"lodash\": \"^4.17.23\",\n    \"minimist\": \">=1.2.8\"\n  }\n}\n"
  },
  {
    "path": "rpc/proto/gorush.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.6\n// \tprotoc        v5.29.3\n// source: gorush.proto\n\npackage proto\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\tstructpb \"google.golang.org/protobuf/types/known/structpb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype NotificationRequest_Priority int32\n\nconst (\n\tNotificationRequest_NORMAL NotificationRequest_Priority = 0\n\tNotificationRequest_HIGH   NotificationRequest_Priority = 1\n)\n\n// Enum value maps for NotificationRequest_Priority.\nvar (\n\tNotificationRequest_Priority_name = map[int32]string{\n\t\t0: \"NORMAL\",\n\t\t1: \"HIGH\",\n\t}\n\tNotificationRequest_Priority_value = map[string]int32{\n\t\t\"NORMAL\": 0,\n\t\t\"HIGH\":   1,\n\t}\n)\n\nfunc (x NotificationRequest_Priority) Enum() *NotificationRequest_Priority {\n\tp := new(NotificationRequest_Priority)\n\t*p = x\n\treturn p\n}\n\nfunc (x NotificationRequest_Priority) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (NotificationRequest_Priority) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_gorush_proto_enumTypes[0].Descriptor()\n}\n\nfunc (NotificationRequest_Priority) Type() protoreflect.EnumType {\n\treturn &file_gorush_proto_enumTypes[0]\n}\n\nfunc (x NotificationRequest_Priority) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use NotificationRequest_Priority.Descriptor instead.\nfunc (NotificationRequest_Priority) EnumDescriptor() ([]byte, []int) {\n\treturn file_gorush_proto_rawDescGZIP(), []int{1, 0}\n}\n\ntype HealthCheckResponse_ServingStatus int32\n\nconst (\n\tHealthCheckResponse_UNKNOWN     HealthCheckResponse_ServingStatus = 0\n\tHealthCheckResponse_SERVING     HealthCheckResponse_ServingStatus = 1\n\tHealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2\n)\n\n// Enum value maps for HealthCheckResponse_ServingStatus.\nvar (\n\tHealthCheckResponse_ServingStatus_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"SERVING\",\n\t\t2: \"NOT_SERVING\",\n\t}\n\tHealthCheckResponse_ServingStatus_value = map[string]int32{\n\t\t\"UNKNOWN\":     0,\n\t\t\"SERVING\":     1,\n\t\t\"NOT_SERVING\": 2,\n\t}\n)\n\nfunc (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse_ServingStatus {\n\tp := new(HealthCheckResponse_ServingStatus)\n\t*p = x\n\treturn p\n}\n\nfunc (x HealthCheckResponse_ServingStatus) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_gorush_proto_enumTypes[1].Descriptor()\n}\n\nfunc (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType {\n\treturn &file_gorush_proto_enumTypes[1]\n}\n\nfunc (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead.\nfunc (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) {\n\treturn file_gorush_proto_rawDescGZIP(), []int{5, 0}\n}\n\ntype Alert struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTitle         string                 `protobuf:\"bytes,1,opt,name=title,proto3\" json:\"title,omitempty\"`\n\tBody          string                 `protobuf:\"bytes,2,opt,name=body,proto3\" json:\"body,omitempty\"`\n\tSubtitle      string                 `protobuf:\"bytes,3,opt,name=subtitle,proto3\" json:\"subtitle,omitempty\"`\n\tAction        string                 `protobuf:\"bytes,4,opt,name=action,proto3\" json:\"action,omitempty\"`\n\tActionLocKey  string                 `protobuf:\"bytes,5,opt,name=actionLocKey,proto3\" json:\"actionLocKey,omitempty\"`\n\tLaunchImage   string                 `protobuf:\"bytes,6,opt,name=launchImage,proto3\" json:\"launchImage,omitempty\"`\n\tLocKey        string                 `protobuf:\"bytes,7,opt,name=locKey,proto3\" json:\"locKey,omitempty\"`\n\tTitleLocKey   string                 `protobuf:\"bytes,8,opt,name=titleLocKey,proto3\" json:\"titleLocKey,omitempty\"`\n\tLocArgs       []string               `protobuf:\"bytes,9,rep,name=locArgs,proto3\" json:\"locArgs,omitempty\"`\n\tTitleLocArgs  []string               `protobuf:\"bytes,10,rep,name=titleLocArgs,proto3\" json:\"titleLocArgs,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Alert) Reset() {\n\t*x = Alert{}\n\tmi := &file_gorush_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Alert) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Alert) ProtoMessage() {}\n\nfunc (x *Alert) ProtoReflect() protoreflect.Message {\n\tmi := &file_gorush_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Alert.ProtoReflect.Descriptor instead.\nfunc (*Alert) Descriptor() ([]byte, []int) {\n\treturn file_gorush_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Alert) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *Alert) GetBody() string {\n\tif x != nil {\n\t\treturn x.Body\n\t}\n\treturn \"\"\n}\n\nfunc (x *Alert) GetSubtitle() string {\n\tif x != nil {\n\t\treturn x.Subtitle\n\t}\n\treturn \"\"\n}\n\nfunc (x *Alert) GetAction() string {\n\tif x != nil {\n\t\treturn x.Action\n\t}\n\treturn \"\"\n}\n\nfunc (x *Alert) GetActionLocKey() string {\n\tif x != nil {\n\t\treturn x.ActionLocKey\n\t}\n\treturn \"\"\n}\n\nfunc (x *Alert) GetLaunchImage() string {\n\tif x != nil {\n\t\treturn x.LaunchImage\n\t}\n\treturn \"\"\n}\n\nfunc (x *Alert) GetLocKey() string {\n\tif x != nil {\n\t\treturn x.LocKey\n\t}\n\treturn \"\"\n}\n\nfunc (x *Alert) GetTitleLocKey() string {\n\tif x != nil {\n\t\treturn x.TitleLocKey\n\t}\n\treturn \"\"\n}\n\nfunc (x *Alert) GetLocArgs() []string {\n\tif x != nil {\n\t\treturn x.LocArgs\n\t}\n\treturn nil\n}\n\nfunc (x *Alert) GetTitleLocArgs() []string {\n\tif x != nil {\n\t\treturn x.TitleLocArgs\n\t}\n\treturn nil\n}\n\ntype NotificationRequest struct {\n\tstate            protoimpl.MessageState       `protogen:\"open.v1\"`\n\tTokens           []string                     `protobuf:\"bytes,1,rep,name=tokens,proto3\" json:\"tokens,omitempty\"`\n\tPlatform         int32                        `protobuf:\"varint,2,opt,name=platform,proto3\" json:\"platform,omitempty\"`\n\tMessage          string                       `protobuf:\"bytes,3,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tTitle            string                       `protobuf:\"bytes,4,opt,name=title,proto3\" json:\"title,omitempty\"`\n\tTopic            string                       `protobuf:\"bytes,5,opt,name=topic,proto3\" json:\"topic,omitempty\"`\n\tKey              string                       `protobuf:\"bytes,6,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tBadge            int32                        `protobuf:\"varint,7,opt,name=badge,proto3\" json:\"badge,omitempty\"`\n\tCategory         string                       `protobuf:\"bytes,8,opt,name=category,proto3\" json:\"category,omitempty\"`\n\tAlert            *Alert                       `protobuf:\"bytes,9,opt,name=alert,proto3\" json:\"alert,omitempty\"`\n\tSound            string                       `protobuf:\"bytes,10,opt,name=sound,proto3\" json:\"sound,omitempty\"`\n\tContentAvailable bool                         `protobuf:\"varint,11,opt,name=contentAvailable,proto3\" json:\"contentAvailable,omitempty\"`\n\tThreadID         string                       `protobuf:\"bytes,12,opt,name=threadID,proto3\" json:\"threadID,omitempty\"`\n\tMutableContent   bool                         `protobuf:\"varint,13,opt,name=mutableContent,proto3\" json:\"mutableContent,omitempty\"`\n\tData             *structpb.Struct             `protobuf:\"bytes,14,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tImage            string                       `protobuf:\"bytes,15,opt,name=image,proto3\" json:\"image,omitempty\"`\n\tPriority         NotificationRequest_Priority `protobuf:\"varint,16,opt,name=priority,proto3,enum=proto.NotificationRequest_Priority\" json:\"priority,omitempty\"`\n\tID               string                       `protobuf:\"bytes,17,opt,name=ID,proto3\" json:\"ID,omitempty\"`\n\tPushType         string                       `protobuf:\"bytes,18,opt,name=pushType,proto3\" json:\"pushType,omitempty\"`\n\t// default is production\n\tDevelopment   bool        `protobuf:\"varint,19,opt,name=development,proto3\" json:\"development,omitempty\"`\n\tFcmOptions    *FCMOptions `protobuf:\"bytes,20,opt,name=fcmOptions,proto3\" json:\"fcmOptions,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *NotificationRequest) Reset() {\n\t*x = NotificationRequest{}\n\tmi := &file_gorush_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *NotificationRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*NotificationRequest) ProtoMessage() {}\n\nfunc (x *NotificationRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_gorush_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use NotificationRequest.ProtoReflect.Descriptor instead.\nfunc (*NotificationRequest) Descriptor() ([]byte, []int) {\n\treturn file_gorush_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *NotificationRequest) GetTokens() []string {\n\tif x != nil {\n\t\treturn x.Tokens\n\t}\n\treturn nil\n}\n\nfunc (x *NotificationRequest) GetPlatform() int32 {\n\tif x != nil {\n\t\treturn x.Platform\n\t}\n\treturn 0\n}\n\nfunc (x *NotificationRequest) GetMessage() string {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetTopic() string {\n\tif x != nil {\n\t\treturn x.Topic\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetBadge() int32 {\n\tif x != nil {\n\t\treturn x.Badge\n\t}\n\treturn 0\n}\n\nfunc (x *NotificationRequest) GetCategory() string {\n\tif x != nil {\n\t\treturn x.Category\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetAlert() *Alert {\n\tif x != nil {\n\t\treturn x.Alert\n\t}\n\treturn nil\n}\n\nfunc (x *NotificationRequest) GetSound() string {\n\tif x != nil {\n\t\treturn x.Sound\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetContentAvailable() bool {\n\tif x != nil {\n\t\treturn x.ContentAvailable\n\t}\n\treturn false\n}\n\nfunc (x *NotificationRequest) GetThreadID() string {\n\tif x != nil {\n\t\treturn x.ThreadID\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetMutableContent() bool {\n\tif x != nil {\n\t\treturn x.MutableContent\n\t}\n\treturn false\n}\n\nfunc (x *NotificationRequest) GetData() *structpb.Struct {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *NotificationRequest) GetImage() string {\n\tif x != nil {\n\t\treturn x.Image\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetPriority() NotificationRequest_Priority {\n\tif x != nil {\n\t\treturn x.Priority\n\t}\n\treturn NotificationRequest_NORMAL\n}\n\nfunc (x *NotificationRequest) GetID() string {\n\tif x != nil {\n\t\treturn x.ID\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetPushType() string {\n\tif x != nil {\n\t\treturn x.PushType\n\t}\n\treturn \"\"\n}\n\nfunc (x *NotificationRequest) GetDevelopment() bool {\n\tif x != nil {\n\t\treturn x.Development\n\t}\n\treturn false\n}\n\nfunc (x *NotificationRequest) GetFcmOptions() *FCMOptions {\n\tif x != nil {\n\t\treturn x.FcmOptions\n\t}\n\treturn nil\n}\n\ntype FCMOptions struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tAnalyticsLabel string                 `protobuf:\"bytes,1,opt,name=analyticsLabel,proto3\" json:\"analyticsLabel,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *FCMOptions) Reset() {\n\t*x = FCMOptions{}\n\tmi := &file_gorush_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *FCMOptions) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*FCMOptions) ProtoMessage() {}\n\nfunc (x *FCMOptions) ProtoReflect() protoreflect.Message {\n\tmi := &file_gorush_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use FCMOptions.ProtoReflect.Descriptor instead.\nfunc (*FCMOptions) Descriptor() ([]byte, []int) {\n\treturn file_gorush_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *FCMOptions) GetAnalyticsLabel() string {\n\tif x != nil {\n\t\treturn x.AnalyticsLabel\n\t}\n\treturn \"\"\n}\n\ntype NotificationReply struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSuccess       bool                   `protobuf:\"varint,1,opt,name=success,proto3\" json:\"success,omitempty\"`\n\tCounts        int32                  `protobuf:\"varint,2,opt,name=counts,proto3\" json:\"counts,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *NotificationReply) Reset() {\n\t*x = NotificationReply{}\n\tmi := &file_gorush_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *NotificationReply) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*NotificationReply) ProtoMessage() {}\n\nfunc (x *NotificationReply) ProtoReflect() protoreflect.Message {\n\tmi := &file_gorush_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use NotificationReply.ProtoReflect.Descriptor instead.\nfunc (*NotificationReply) Descriptor() ([]byte, []int) {\n\treturn file_gorush_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *NotificationReply) GetSuccess() bool {\n\tif x != nil {\n\t\treturn x.Success\n\t}\n\treturn false\n}\n\nfunc (x *NotificationReply) GetCounts() int32 {\n\tif x != nil {\n\t\treturn x.Counts\n\t}\n\treturn 0\n}\n\ntype HealthCheckRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tService       string                 `protobuf:\"bytes,1,opt,name=service,proto3\" json:\"service,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HealthCheckRequest) Reset() {\n\t*x = HealthCheckRequest{}\n\tmi := &file_gorush_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HealthCheckRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HealthCheckRequest) ProtoMessage() {}\n\nfunc (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_gorush_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead.\nfunc (*HealthCheckRequest) Descriptor() ([]byte, []int) {\n\treturn file_gorush_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *HealthCheckRequest) GetService() string {\n\tif x != nil {\n\t\treturn x.Service\n\t}\n\treturn \"\"\n}\n\ntype HealthCheckResponse struct {\n\tstate         protoimpl.MessageState            `protogen:\"open.v1\"`\n\tStatus        HealthCheckResponse_ServingStatus `protobuf:\"varint,1,opt,name=status,proto3,enum=proto.HealthCheckResponse_ServingStatus\" json:\"status,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HealthCheckResponse) Reset() {\n\t*x = HealthCheckResponse{}\n\tmi := &file_gorush_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HealthCheckResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HealthCheckResponse) ProtoMessage() {}\n\nfunc (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_gorush_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead.\nfunc (*HealthCheckResponse) Descriptor() ([]byte, []int) {\n\treturn file_gorush_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus {\n\tif x != nil {\n\t\treturn x.Status\n\t}\n\treturn HealthCheckResponse_UNKNOWN\n}\n\nvar File_gorush_proto protoreflect.FileDescriptor\n\nconst file_gorush_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\fgorush.proto\\x12\\x05proto\\x1a\\x1cgoogle/protobuf/struct.proto\\\"\\xa3\\x02\\n\" +\n\t\"\\x05Alert\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x01 \\x01(\\tR\\x05title\\x12\\x12\\n\" +\n\t\"\\x04body\\x18\\x02 \\x01(\\tR\\x04body\\x12\\x1a\\n\" +\n\t\"\\bsubtitle\\x18\\x03 \\x01(\\tR\\bsubtitle\\x12\\x16\\n\" +\n\t\"\\x06action\\x18\\x04 \\x01(\\tR\\x06action\\x12\\\"\\n\" +\n\t\"\\factionLocKey\\x18\\x05 \\x01(\\tR\\factionLocKey\\x12 \\n\" +\n\t\"\\vlaunchImage\\x18\\x06 \\x01(\\tR\\vlaunchImage\\x12\\x16\\n\" +\n\t\"\\x06locKey\\x18\\a \\x01(\\tR\\x06locKey\\x12 \\n\" +\n\t\"\\vtitleLocKey\\x18\\b \\x01(\\tR\\vtitleLocKey\\x12\\x18\\n\" +\n\t\"\\alocArgs\\x18\\t \\x03(\\tR\\alocArgs\\x12\\\"\\n\" +\n\t\"\\ftitleLocArgs\\x18\\n\" +\n\t\" \\x03(\\tR\\ftitleLocArgs\\\"\\xa4\\x05\\n\" +\n\t\"\\x13NotificationRequest\\x12\\x16\\n\" +\n\t\"\\x06tokens\\x18\\x01 \\x03(\\tR\\x06tokens\\x12\\x1a\\n\" +\n\t\"\\bplatform\\x18\\x02 \\x01(\\x05R\\bplatform\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\x03 \\x01(\\tR\\amessage\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x04 \\x01(\\tR\\x05title\\x12\\x14\\n\" +\n\t\"\\x05topic\\x18\\x05 \\x01(\\tR\\x05topic\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x06 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05badge\\x18\\a \\x01(\\x05R\\x05badge\\x12\\x1a\\n\" +\n\t\"\\bcategory\\x18\\b \\x01(\\tR\\bcategory\\x12\\\"\\n\" +\n\t\"\\x05alert\\x18\\t \\x01(\\v2\\f.proto.AlertR\\x05alert\\x12\\x14\\n\" +\n\t\"\\x05sound\\x18\\n\" +\n\t\" \\x01(\\tR\\x05sound\\x12*\\n\" +\n\t\"\\x10contentAvailable\\x18\\v \\x01(\\bR\\x10contentAvailable\\x12\\x1a\\n\" +\n\t\"\\bthreadID\\x18\\f \\x01(\\tR\\bthreadID\\x12&\\n\" +\n\t\"\\x0emutableContent\\x18\\r \\x01(\\bR\\x0emutableContent\\x12+\\n\" +\n\t\"\\x04data\\x18\\x0e \\x01(\\v2\\x17.google.protobuf.StructR\\x04data\\x12\\x14\\n\" +\n\t\"\\x05image\\x18\\x0f \\x01(\\tR\\x05image\\x12?\\n\" +\n\t\"\\bpriority\\x18\\x10 \\x01(\\x0e2#.proto.NotificationRequest.PriorityR\\bpriority\\x12\\x0e\\n\" +\n\t\"\\x02ID\\x18\\x11 \\x01(\\tR\\x02ID\\x12\\x1a\\n\" +\n\t\"\\bpushType\\x18\\x12 \\x01(\\tR\\bpushType\\x12 \\n\" +\n\t\"\\vdevelopment\\x18\\x13 \\x01(\\bR\\vdevelopment\\x121\\n\" +\n\t\"\\n\" +\n\t\"fcmOptions\\x18\\x14 \\x01(\\v2\\x11.proto.FCMOptionsR\\n\" +\n\t\"fcmOptions\\\" \\n\" +\n\t\"\\bPriority\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06NORMAL\\x10\\x00\\x12\\b\\n\" +\n\t\"\\x04HIGH\\x10\\x01\\\"4\\n\" +\n\t\"\\n\" +\n\t\"FCMOptions\\x12&\\n\" +\n\t\"\\x0eanalyticsLabel\\x18\\x01 \\x01(\\tR\\x0eanalyticsLabel\\\"E\\n\" +\n\t\"\\x11NotificationReply\\x12\\x18\\n\" +\n\t\"\\asuccess\\x18\\x01 \\x01(\\bR\\asuccess\\x12\\x16\\n\" +\n\t\"\\x06counts\\x18\\x02 \\x01(\\x05R\\x06counts\\\".\\n\" +\n\t\"\\x12HealthCheckRequest\\x12\\x18\\n\" +\n\t\"\\aservice\\x18\\x01 \\x01(\\tR\\aservice\\\"\\x93\\x01\\n\" +\n\t\"\\x13HealthCheckResponse\\x12@\\n\" +\n\t\"\\x06status\\x18\\x01 \\x01(\\x0e2(.proto.HealthCheckResponse.ServingStatusR\\x06status\\\":\\n\" +\n\t\"\\rServingStatus\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\v\\n\" +\n\t\"\\aSERVING\\x10\\x01\\x12\\x0f\\n\" +\n\t\"\\vNOT_SERVING\\x10\\x022H\\n\" +\n\t\"\\x06Gorush\\x12>\\n\" +\n\t\"\\x04Send\\x12\\x1a.proto.NotificationRequest\\x1a\\x18.proto.NotificationReply\\\"\\x002H\\n\" +\n\t\"\\x06Health\\x12>\\n\" +\n\t\"\\x05Check\\x12\\x19.proto.HealthCheckRequest\\x1a\\x1a.proto.HealthCheckResponseB\\n\" +\n\t\"Z\\b./;protob\\x06proto3\"\n\nvar (\n\tfile_gorush_proto_rawDescOnce sync.Once\n\tfile_gorush_proto_rawDescData []byte\n)\n\nfunc file_gorush_proto_rawDescGZIP() []byte {\n\tfile_gorush_proto_rawDescOnce.Do(func() {\n\t\tfile_gorush_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gorush_proto_rawDesc), len(file_gorush_proto_rawDesc)))\n\t})\n\treturn file_gorush_proto_rawDescData\n}\n\nvar file_gorush_proto_enumTypes = make([]protoimpl.EnumInfo, 2)\nvar file_gorush_proto_msgTypes = make([]protoimpl.MessageInfo, 6)\nvar file_gorush_proto_goTypes = []any{\n\t(NotificationRequest_Priority)(0),      // 0: proto.NotificationRequest.Priority\n\t(HealthCheckResponse_ServingStatus)(0), // 1: proto.HealthCheckResponse.ServingStatus\n\t(*Alert)(nil),                          // 2: proto.Alert\n\t(*NotificationRequest)(nil),            // 3: proto.NotificationRequest\n\t(*FCMOptions)(nil),                     // 4: proto.FCMOptions\n\t(*NotificationReply)(nil),              // 5: proto.NotificationReply\n\t(*HealthCheckRequest)(nil),             // 6: proto.HealthCheckRequest\n\t(*HealthCheckResponse)(nil),            // 7: proto.HealthCheckResponse\n\t(*structpb.Struct)(nil),                // 8: google.protobuf.Struct\n}\nvar file_gorush_proto_depIdxs = []int32{\n\t2, // 0: proto.NotificationRequest.alert:type_name -> proto.Alert\n\t8, // 1: proto.NotificationRequest.data:type_name -> google.protobuf.Struct\n\t0, // 2: proto.NotificationRequest.priority:type_name -> proto.NotificationRequest.Priority\n\t4, // 3: proto.NotificationRequest.fcmOptions:type_name -> proto.FCMOptions\n\t1, // 4: proto.HealthCheckResponse.status:type_name -> proto.HealthCheckResponse.ServingStatus\n\t3, // 5: proto.Gorush.Send:input_type -> proto.NotificationRequest\n\t6, // 6: proto.Health.Check:input_type -> proto.HealthCheckRequest\n\t5, // 7: proto.Gorush.Send:output_type -> proto.NotificationReply\n\t7, // 8: proto.Health.Check:output_type -> proto.HealthCheckResponse\n\t7, // [7:9] is the sub-list for method output_type\n\t5, // [5:7] is the sub-list for method input_type\n\t5, // [5:5] is the sub-list for extension type_name\n\t5, // [5:5] is the sub-list for extension extendee\n\t0, // [0:5] is the sub-list for field type_name\n}\n\nfunc init() { file_gorush_proto_init() }\nfunc file_gorush_proto_init() {\n\tif File_gorush_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_gorush_proto_rawDesc), len(file_gorush_proto_rawDesc)),\n\t\t\tNumEnums:      2,\n\t\t\tNumMessages:   6,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   2,\n\t\t},\n\t\tGoTypes:           file_gorush_proto_goTypes,\n\t\tDependencyIndexes: file_gorush_proto_depIdxs,\n\t\tEnumInfos:         file_gorush_proto_enumTypes,\n\t\tMessageInfos:      file_gorush_proto_msgTypes,\n\t}.Build()\n\tFile_gorush_proto = out.File\n\tfile_gorush_proto_goTypes = nil\n\tfile_gorush_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "rpc/proto/gorush.proto",
    "content": "syntax = \"proto3\";\nimport \"google/protobuf/struct.proto\";\n\npackage proto;\noption go_package = \"./;proto\";\n\nmessage Alert {\n  string title = 1;\n  string body = 2;\n  string subtitle = 3;\n  string action = 4;\n  string actionLocKey = 5;\n  string launchImage = 6;\n  string locKey = 7;\n  string titleLocKey = 8;\n  repeated string locArgs = 9;\n  repeated string titleLocArgs = 10;\n}\n\nmessage NotificationRequest {\n  repeated string tokens = 1;\n  int32 platform = 2;\n  string message = 3;\n  string title = 4;\n  string topic = 5;\n  string key = 6;\n  int32 badge = 7;\n  string category = 8;\n  Alert alert = 9;\n  string sound = 10;\n  bool contentAvailable = 11;\n  string threadID = 12;\n  bool mutableContent = 13;\n  google.protobuf.Struct data = 14;\n  string image = 15;\n  enum Priority {\n    NORMAL = 0;\n    HIGH = 1;\n  }\n  Priority priority = 16;\n  string ID = 17;\n  string pushType = 18;\n  // default is production\n  bool development = 19;\n  FCMOptions fcmOptions = 20;\n}\n\nmessage FCMOptions {\n  string analyticsLabel = 1;\n}\n\nmessage NotificationReply {\n  bool success = 1;\n  int32 counts = 2;\n}\n\nservice Gorush {\n  rpc Send (NotificationRequest) returns (NotificationReply) {}\n}\n\nmessage HealthCheckRequest {\n  string service = 1;\n}\n\nmessage HealthCheckResponse {\n  enum ServingStatus {\n    UNKNOWN = 0;\n    SERVING = 1;\n    NOT_SERVING = 2;\n  }\n  ServingStatus status = 1;\n}\n\nservice Health {\n  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);\n}\n"
  },
  {
    "path": "rpc/proto/gorush_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.5.1\n// - protoc             v5.29.3\n// source: gorush.proto\n\npackage proto\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tGorush_Send_FullMethodName = \"/proto.Gorush/Send\"\n)\n\n// GorushClient is the client API for Gorush service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype GorushClient interface {\n\tSend(ctx context.Context, in *NotificationRequest, opts ...grpc.CallOption) (*NotificationReply, error)\n}\n\ntype gorushClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewGorushClient(cc grpc.ClientConnInterface) GorushClient {\n\treturn &gorushClient{cc}\n}\n\nfunc (c *gorushClient) Send(ctx context.Context, in *NotificationRequest, opts ...grpc.CallOption) (*NotificationReply, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(NotificationReply)\n\terr := c.cc.Invoke(ctx, Gorush_Send_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// GorushServer is the server API for Gorush service.\n// All implementations should embed UnimplementedGorushServer\n// for forward compatibility.\ntype GorushServer interface {\n\tSend(context.Context, *NotificationRequest) (*NotificationReply, error)\n}\n\n// UnimplementedGorushServer should be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedGorushServer struct{}\n\nfunc (UnimplementedGorushServer) Send(context.Context, *NotificationRequest) (*NotificationReply, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Send not implemented\")\n}\nfunc (UnimplementedGorushServer) testEmbeddedByValue() {}\n\n// UnsafeGorushServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to GorushServer will\n// result in compilation errors.\ntype UnsafeGorushServer interface {\n\tmustEmbedUnimplementedGorushServer()\n}\n\nfunc RegisterGorushServer(s grpc.ServiceRegistrar, srv GorushServer) {\n\t// If the following call pancis, it indicates UnimplementedGorushServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&Gorush_ServiceDesc, srv)\n}\n\nfunc _Gorush_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(NotificationRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GorushServer).Send(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Gorush_Send_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GorushServer).Send(ctx, req.(*NotificationRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Gorush_ServiceDesc is the grpc.ServiceDesc for Gorush service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Gorush_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"proto.Gorush\",\n\tHandlerType: (*GorushServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Send\",\n\t\t\tHandler:    _Gorush_Send_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"gorush.proto\",\n}\n\nconst (\n\tHealth_Check_FullMethodName = \"/proto.Health/Check\"\n)\n\n// HealthClient is the client API for Health service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype HealthClient interface {\n\tCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)\n}\n\ntype healthClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewHealthClient(cc grpc.ClientConnInterface) HealthClient {\n\treturn &healthClient{cc}\n}\n\nfunc (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(HealthCheckResponse)\n\terr := c.cc.Invoke(ctx, Health_Check_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// HealthServer is the server API for Health service.\n// All implementations should embed UnimplementedHealthServer\n// for forward compatibility.\ntype HealthServer interface {\n\tCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)\n}\n\n// UnimplementedHealthServer should be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedHealthServer struct{}\n\nfunc (UnimplementedHealthServer) Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Check not implemented\")\n}\nfunc (UnimplementedHealthServer) testEmbeddedByValue() {}\n\n// UnsafeHealthServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to HealthServer will\n// result in compilation errors.\ntype UnsafeHealthServer interface {\n\tmustEmbedUnimplementedHealthServer()\n}\n\nfunc RegisterHealthServer(s grpc.ServiceRegistrar, srv HealthServer) {\n\t// If the following call pancis, it indicates UnimplementedHealthServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&Health_ServiceDesc, srv)\n}\n\nfunc _Health_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(HealthCheckRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(HealthServer).Check(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Health_Check_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(HealthServer).Check(ctx, req.(*HealthCheckRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Health_ServiceDesc is the grpc.ServiceDesc for Health service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Health_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"proto.Health\",\n\tHandlerType: (*HealthServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Check\",\n\t\t\tHandler:    _Health_Check_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"gorush.proto\",\n}\n"
  },
  {
    "path": "rpc/server.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"log\"\n\t\"math\"\n\t\"net\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"firebase.google.com/go/v4/messaging\"\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/notify\"\n\t\"github.com/appleboy/gorush/rpc/proto\"\n\n\tgrpc_middleware \"github.com/grpc-ecosystem/go-grpc-middleware\"\n\tgrpc_recovery \"github.com/grpc-ecosystem/go-grpc-middleware/recovery\"\n\tgrpc_prometheus \"github.com/grpc-ecosystem/go-grpc-prometheus\"\n\t\"go.opencensus.io/plugin/ocgrpc\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/reflection\"\n\t\"google.golang.org/grpc/status\"\n)\n\n// Server is used to implement gorush grpc server.\ntype Server struct {\n\tcfg *config.ConfYaml\n\tmu  sync.Mutex\n\t// statusMap stores the serving status of the services this Server monitors.\n\tstatusMap map[string]proto.HealthCheckResponse_ServingStatus\n}\n\n// NewServer returns a new Server.\nfunc NewServer(cfg *config.ConfYaml) *Server {\n\treturn &Server{\n\t\tcfg:       cfg,\n\t\tstatusMap: make(map[string]proto.HealthCheckResponse_ServingStatus),\n\t}\n}\n\n// Check implements `service Health`.\nfunc (s *Server) Check(\n\tctx context.Context,\n\tin *proto.HealthCheckRequest,\n) (*proto.HealthCheckResponse, error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tif in.Service == \"\" {\n\t\t// check the server overall health status.\n\t\treturn &proto.HealthCheckResponse{\n\t\t\tStatus: proto.HealthCheckResponse_SERVING,\n\t\t}, nil\n\t}\n\tif status, ok := s.statusMap[in.Service]; ok {\n\t\treturn &proto.HealthCheckResponse{\n\t\t\tStatus: status,\n\t\t}, nil\n\t}\n\treturn nil, status.Error(codes.NotFound, \"unknown service\")\n}\n\n// Send implements helloworld.GreeterServer\nfunc (s *Server) Send(\n\tctx context.Context,\n\tin *proto.NotificationRequest,\n) (*proto.NotificationReply, error) {\n\tbadge := int(in.Badge)\n\tnotification := notify.PushNotification{\n\t\tID:               in.ID,\n\t\tPlatform:         int(in.Platform),\n\t\tTokens:           in.Tokens,\n\t\tMessage:          in.Message,\n\t\tTitle:            in.Title,\n\t\tTopic:            in.Topic,\n\t\tCategory:         in.Category,\n\t\tSound:            in.Sound,\n\t\tContentAvailable: in.ContentAvailable,\n\t\tThreadID:         in.ThreadID,\n\t\tMutableContent:   in.MutableContent,\n\t\tImage:            in.Image,\n\t\tPriority:         strings.ToLower(in.GetPriority().String()),\n\t\tPushType:         in.PushType,\n\t\tDevelopment:      in.Development,\n\t}\n\n\tif badge > 0 {\n\t\tnotification.Badge = &badge\n\t}\n\n\tif in.Topic != \"\" && in.Platform == core.PlatFormAndroid {\n\t\tnotification.Topic = in.Topic\n\t}\n\n\tif in.Alert != nil {\n\t\tnotification.Alert = notify.Alert{\n\t\t\tTitle:        in.Alert.Title,\n\t\t\tBody:         in.Alert.Body,\n\t\t\tSubtitle:     in.Alert.Subtitle,\n\t\t\tAction:       in.Alert.Action,\n\t\t\tActionLocKey: in.Alert.Action,\n\t\t\tLaunchImage:  in.Alert.LaunchImage,\n\t\t\tLocArgs:      in.Alert.LocArgs,\n\t\t\tLocKey:       in.Alert.LocKey,\n\t\t\tTitleLocArgs: in.Alert.TitleLocArgs,\n\t\t\tTitleLocKey:  in.Alert.TitleLocKey,\n\t\t}\n\t}\n\n\tif in.Data != nil {\n\t\tnotification.Data = in.Data.AsMap()\n\t}\n\n\tif in.FcmOptions != nil {\n\t\tnotification.FCMOptions = &messaging.FCMOptions{\n\t\t\tAnalyticsLabel: in.FcmOptions.AnalyticsLabel,\n\t\t}\n\t}\n\n\tgo func() {\n\t\tctx := context.Background()\n\t\t_, err := notify.SendNotification(ctx, &notification, s.cfg)\n\t\tif err != nil {\n\t\t\tlogx.LogError.Error(err)\n\t\t}\n\t}()\n\n\tcounts, err := safeIntToInt32(len(notification.Tokens))\n\tif err != nil {\n\t\treturn nil, status.Error(codes.InvalidArgument, err.Error())\n\t}\n\n\treturn &proto.NotificationReply{\n\t\tSuccess: true,\n\t\tCounts:  counts,\n\t}, nil\n}\n\n// safeIntToInt32 converts an int to an int32, returning an error if the int is out of range.\nfunc safeIntToInt32(n int) (int32, error) {\n\tif n < math.MinInt32 || n > math.MaxInt32 {\n\t\treturn 0, errors.New(\"integer overflow: value out of int32 range\")\n\t}\n\treturn int32(n), nil\n}\n\n// RunGRPCServer run gorush grpc server\nfunc RunGRPCServer(ctx context.Context, cfg *config.ConfYaml) error {\n\tif !cfg.GRPC.Enabled {\n\t\tlogx.LogAccess.Info(\"gRPC server is disabled.\")\n\t\treturn nil\n\t}\n\n\trecoveryOpt := grpc_recovery.WithRecoveryHandlerContext(\n\t\tfunc(ctx context.Context, p any) error {\n\t\t\tlog.Printf(\"[PANIC] %s\\n%s\", p, string(debug.Stack()))\n\t\t\treturn status.Error(codes.Internal, \"system has been broken\")\n\t\t},\n\t)\n\n\tunaryInterceptors := []grpc.UnaryServerInterceptor{\n\t\tgrpc_prometheus.UnaryServerInterceptor,\n\t\tgrpc_recovery.UnaryServerInterceptor(recoveryOpt),\n\t}\n\n\tvar s *grpc.Server\n\n\tif cfg.Core.SSL && cfg.Core.CertPath != \"\" && cfg.Core.KeyPath != \"\" {\n\t\ttlsCert, err := tls.LoadX509KeyPair(cfg.Core.CertPath, cfg.Core.KeyPath)\n\t\tif err != nil {\n\t\t\tlogx.LogError.Error(\"failed to load tls cert file: \", err)\n\t\t\treturn err\n\t\t}\n\n\t\ttlsConfig := &tls.Config{\n\t\t\tCertificates: []tls.Certificate{tlsCert},\n\t\t\tClientAuth:   tls.NoClientCert,\n\t\t\tMinVersion:   tls.VersionTLS12, // Set minimum TLS version to TLS 1.2\n\t\t}\n\n\t\ts = grpc.NewServer(\n\t\t\tgrpc.Creds(credentials.NewTLS(tlsConfig)),\n\t\t\tgrpc.StatsHandler(&ocgrpc.ServerHandler{}),\n\t\t\tgrpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unaryInterceptors...)),\n\t\t)\n\t} else {\n\t\ts = grpc.NewServer(\n\t\t\tgrpc.StatsHandler(&ocgrpc.ServerHandler{}),\n\t\t\tgrpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unaryInterceptors...)),\n\t\t)\n\t}\n\n\trpcSrv := NewServer(cfg)\n\tproto.RegisterGorushServer(s, rpcSrv)\n\tproto.RegisterHealthServer(s, rpcSrv)\n\n\t// Register reflection service on gRPC server.\n\treflection.Register(s)\n\n\tlc := &net.ListenConfig{}\n\tlis, err := lc.Listen(context.Background(), \"tcp\", \":\"+cfg.GRPC.Port)\n\tif err != nil {\n\t\tlogx.LogError.Fatalln(err)\n\t\treturn err\n\t}\n\tlogx.LogAccess.Info(\"gRPC server is running on \" + cfg.GRPC.Port + \" port.\")\n\tgo func() {\n\t\t<-ctx.Done()\n\t\ts.GracefulStop() // graceful shutdown\n\t\tlogx.LogAccess.Info(\"shutdown the gRPC server\")\n\t}()\n\tif err = s.Serve(lis); err != nil {\n\t\tlogx.LogError.Fatalln(err)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "rpc/server_test.go",
    "content": "package rpc\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\nfunc TestSafeIntToInt32(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinput   int\n\t\twant    int32\n\t\twantErr bool\n\t}{\n\t\t{\"Valid int32\", 123, 123, false},\n\t\t{\"Max int32\", math.MaxInt32, math.MaxInt32, false},\n\t\t{\"Min int32\", math.MinInt32, math.MinInt32, false},\n\t\t{\"Overflow int32\", math.MaxInt32 + 1, 0, true},\n\t\t{\"Underflow int32\", math.MinInt32 - 1, 0, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := safeIntToInt32(tt.input)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"safeIntToInt32() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"safeIntToInt32() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// const gRPCAddr = \"localhost:9000\"\n\n// func initTest() *config.ConfYaml {\n// \tcfg, _ := config.LoadConf()\n// \tcfg.Core.Mode = \"test\"\n// \treturn cfg\n// }\n\n// func TestGracefulShutDownGRPCServer(t *testing.T) {\n// \tcfg := initTest()\n// \tcfg.GRPC.Enabled = true\n// \tcfg.GRPC.Port = \"9000\"\n// \tcfg.Log.Format = \"json\"\n\n// \t// Run gRPC server\n// \tctx, gRPCContextCancel := context.WithCancel(context.Background())\n// \tgo func() {\n// \t\tif err := RunGRPCServer(ctx, cfg); err != nil {\n// \t\t\tpanic(err)\n// \t\t}\n// \t}()\n\n// \t// gRPC client conn\n// \tconn, err := grpc.Dial(\n// \t\tgRPCAddr,\n// \t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n// \t\tgrpc.WithDefaultCallOptions(grpc.WaitForReady(true)),\n// \t) // wait for server ready\n// \tif err != nil {\n// \t\tt.Error(err)\n// \t}\n\n// \t// Stop gRPC server\n// \tgo gRPCContextCancel()\n\n// \t// wait for client connection would be closed\n// \tfor conn.GetState() != connectivity.TransientFailure {\n// \t}\n// \tconn.Close()\n// }\n"
  },
  {
    "path": "status/status.go",
    "content": "package status\n\nimport (\n\t\"errors\"\n\n\t\"github.com/appleboy/gorush/config\"\n\t\"github.com/appleboy/gorush/core\"\n\t\"github.com/appleboy/gorush/logx\"\n\t\"github.com/appleboy/gorush/storage/badger\"\n\t\"github.com/appleboy/gorush/storage/boltdb\"\n\t\"github.com/appleboy/gorush/storage/buntdb\"\n\t\"github.com/appleboy/gorush/storage/leveldb\"\n\t\"github.com/appleboy/gorush/storage/memory\"\n\t\"github.com/appleboy/gorush/storage/redis\"\n\n\t\"github.com/thoas/stats\"\n)\n\n// Stats provide response time, status code count, etc.\nvar Stats *stats.Stats\n\n// StatStorage implements the storage interface\nvar StatStorage *StateStorage\n\n// App is status structure\ntype App struct {\n\tVersion        string        `json:\"version\"`\n\tBusyWorkers    int64         `json:\"busy_workers\"`\n\tSuccessTasks   uint64        `json:\"success_tasks\"`\n\tFailureTasks   uint64        `json:\"failure_tasks\"`\n\tSubmittedTasks uint64        `json:\"submitted_tasks\"`\n\tTotalCount     int64         `json:\"total_count\"`\n\tIos            IosStatus     `json:\"ios\"`\n\tAndroid        AndroidStatus `json:\"android\"`\n\tHuawei         HuaweiStatus  `json:\"huawei\"`\n}\n\n// AndroidStatus is android structure\ntype AndroidStatus struct {\n\tPushSuccess int64 `json:\"push_success\"`\n\tPushError   int64 `json:\"push_error\"`\n}\n\n// IosStatus is iOS structure\ntype IosStatus struct {\n\tPushSuccess int64 `json:\"push_success\"`\n\tPushError   int64 `json:\"push_error\"`\n}\n\n// HuaweiStatus is huawei structure\ntype HuaweiStatus struct {\n\tPushSuccess int64 `json:\"push_success\"`\n\tPushError   int64 `json:\"push_error\"`\n}\n\n// InitAppStatus for initialize app status\nfunc InitAppStatus(conf *config.ConfYaml) error {\n\tlogx.LogAccess.Info(\"Init App Status Engine as \", conf.Stat.Engine)\n\n\tvar store core.Storage\n\tswitch conf.Stat.Engine {\n\tcase \"memory\":\n\t\tstore = memory.New()\n\tcase \"redis\":\n\t\tstore = redis.New(\n\t\t\tconf.Stat.Redis.Addr,\n\t\t\tconf.Stat.Redis.Username,\n\t\t\tconf.Stat.Redis.Password,\n\t\t\tconf.Stat.Redis.DB,\n\t\t\tconf.Stat.Redis.Cluster,\n\t\t)\n\tcase \"boltdb\":\n\t\tstore = boltdb.New(\n\t\t\tconf.Stat.BoltDB.Path,\n\t\t\tconf.Stat.BoltDB.Bucket,\n\t\t)\n\tcase \"buntdb\":\n\t\tstore = buntdb.New(\n\t\t\tconf.Stat.BuntDB.Path,\n\t\t)\n\tcase \"leveldb\":\n\t\tstore = leveldb.New(\n\t\t\tconf.Stat.LevelDB.Path,\n\t\t)\n\tcase \"badger\":\n\t\tstore = badger.New(\n\t\t\tconf.Stat.BadgerDB.Path,\n\t\t)\n\tdefault:\n\t\tlogx.LogError.Error(\"storage error: can't find storage driver\")\n\t\treturn errors.New(\"can't find storage driver\")\n\t}\n\n\tStatStorage = NewStateStorage(store)\n\n\tif err := StatStorage.Init(); err != nil {\n\t\tlogx.LogError.Error(\"storage error: \" + err.Error())\n\n\t\treturn err\n\t}\n\n\tStats = stats.New()\n\n\treturn nil\n}\n"
  },
  {
    "path": "status/status_test.go",
    "content": "package status\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/appleboy/gorush/config\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst redisEngine = \"redis\"\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(m.Run())\n}\n\nfunc TestStorageDriverExist(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tcfg.Stat.Engine = \"Test\"\n\terr := InitAppStatus(cfg)\n\tassert.Error(t, err)\n}\n\nfunc TestStatForMemoryEngine(t *testing.T) {\n\t// wait android push notification response.\n\ttime.Sleep(5 * time.Second)\n\n\tvar val int64\n\tcfg, _ := config.LoadConf()\n\tcfg.Stat.Engine = \"memory\"\n\terr := InitAppStatus(cfg)\n\trequire.NoError(t, err)\n\n\tStatStorage.AddTotalCount(100)\n\tStatStorage.AddIosSuccess(200)\n\tStatStorage.AddIosError(300)\n\tStatStorage.AddAndroidSuccess(400)\n\tStatStorage.AddAndroidError(500)\n\n\tval = StatStorage.GetTotalCount()\n\tassert.Equal(t, int64(100), val)\n\tval = StatStorage.GetIosSuccess()\n\tassert.Equal(t, int64(200), val)\n\tval = StatStorage.GetIosError()\n\tassert.Equal(t, int64(300), val)\n\tval = StatStorage.GetAndroidSuccess()\n\tassert.Equal(t, int64(400), val)\n\tval = StatStorage.GetAndroidError()\n\tassert.Equal(t, int64(500), val)\n}\n\nfunc TestRedisServerSuccess(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tcfg.Stat.Engine = redisEngine\n\tcfg.Stat.Redis.Addr = \"redis:6379\"\n\n\terr := InitAppStatus(cfg)\n\n\trequire.NoError(t, err)\n}\n\nfunc TestRedisServerError(t *testing.T) {\n\tcfg, _ := config.LoadConf()\n\tcfg.Stat.Engine = redisEngine\n\tcfg.Stat.Redis.Addr = \"redis:6370\"\n\n\terr := InitAppStatus(cfg)\n\n\tassert.Error(t, err)\n}\n\nfunc TestStatForRedisEngine(t *testing.T) {\n\tvar val int64\n\tcfg, _ := config.LoadConf()\n\tcfg.Stat.Engine = redisEngine\n\tcfg.Stat.Redis.Addr = \"redis:6379\"\n\terr := InitAppStatus(cfg)\n\trequire.NoError(t, err)\n\n\tassert.NoError(t, StatStorage.Init())\n\tStatStorage.Reset()\n\n\tStatStorage.AddTotalCount(100)\n\tStatStorage.AddIosSuccess(200)\n\tStatStorage.AddIosError(300)\n\tStatStorage.AddAndroidSuccess(400)\n\tStatStorage.AddAndroidError(500)\n\n\tval = StatStorage.GetTotalCount()\n\tassert.Equal(t, int64(100), val)\n\tval = StatStorage.GetIosSuccess()\n\tassert.Equal(t, int64(200), val)\n\tval = StatStorage.GetIosError()\n\tassert.Equal(t, int64(300), val)\n\tval = StatStorage.GetAndroidSuccess()\n\tassert.Equal(t, int64(400), val)\n\tval = StatStorage.GetAndroidError()\n\tassert.Equal(t, int64(500), val)\n}\n\nfunc TestDefaultEngine(t *testing.T) {\n\tvar val int64\n\t// defaul engine as memory\n\tcfg, _ := config.LoadConf()\n\terr := InitAppStatus(cfg)\n\trequire.NoError(t, err)\n\n\tStatStorage.Reset()\n\n\tStatStorage.AddTotalCount(100)\n\tStatStorage.AddIosSuccess(200)\n\tStatStorage.AddIosError(300)\n\tStatStorage.AddAndroidSuccess(400)\n\tStatStorage.AddAndroidError(500)\n\n\tval = StatStorage.GetTotalCount()\n\tassert.Equal(t, int64(100), val)\n\tval = StatStorage.GetIosSuccess()\n\tassert.Equal(t, int64(200), val)\n\tval = StatStorage.GetIosError()\n\tassert.Equal(t, int64(300), val)\n\tval = StatStorage.GetAndroidSuccess()\n\tassert.Equal(t, int64(400), val)\n\tval = StatStorage.GetAndroidError()\n\tassert.Equal(t, int64(500), val)\n}\n\nfunc TestStatForBoltDBEngine(t *testing.T) {\n\tvar val int64\n\tcfg, _ := config.LoadConf()\n\tcfg.Stat.Engine = \"boltdb\"\n\terr := InitAppStatus(cfg)\n\trequire.NoError(t, err)\n\n\tStatStorage.Reset()\n\n\tStatStorage.AddTotalCount(100)\n\tStatStorage.AddIosSuccess(200)\n\tStatStorage.AddIosError(300)\n\tStatStorage.AddAndroidSuccess(400)\n\tStatStorage.AddAndroidError(500)\n\n\tval = StatStorage.GetTotalCount()\n\tassert.Equal(t, int64(100), val)\n\tval = StatStorage.GetIosSuccess()\n\tassert.Equal(t, int64(200), val)\n\tval = StatStorage.GetIosError()\n\tassert.Equal(t, int64(300), val)\n\tval = StatStorage.GetAndroidSuccess()\n\tassert.Equal(t, int64(400), val)\n\tval = StatStorage.GetAndroidError()\n\tassert.Equal(t, int64(500), val)\n}\n\n// func TestStatForBuntDBEngine(t *testing.T) {\n// \tvar val int64\n// \tcfg.Stat.Engine = \"buntdb\"\n// \terr := InitAppStatus()\n// \tassert.Nil(t, err)\n\n// \tStatStorage.Reset()\n\n// \tStatStorage.AddTotalCount(100)\n// \tStatStorage.AddIosSuccess(200)\n// \tStatStorage.AddIosError(300)\n// \tStatStorage.AddAndroidSuccess(400)\n// \tStatStorage.AddAndroidError(500)\n\n// \tval = StatStorage.GetTotalCount()\n// \tassert.Equal(t, int64(100), val)\n// \tval = StatStorage.GetIosSuccess()\n// \tassert.Equal(t, int64(200), val)\n// \tval = StatStorage.GetIosError()\n// \tassert.Equal(t, int64(300), val)\n// \tval = StatStorage.GetAndroidSuccess()\n// \tassert.Equal(t, int64(400), val)\n// \tval = StatStorage.GetAndroidError()\n// \tassert.Equal(t, int64(500), val)\n// }\n\n// func TestStatForLevelDBEngine(t *testing.T) {\n// \tvar val int64\n// \tcfg.Stat.Engine = \"leveldb\"\n// \terr := InitAppStatus()\n// \tassert.Nil(t, err)\n\n// \tStatStorage.Reset()\n\n// \tStatStorage.AddTotalCount(100)\n// \tStatStorage.AddIosSuccess(200)\n// \tStatStorage.AddIosError(300)\n// \tStatStorage.AddAndroidSuccess(400)\n// \tStatStorage.AddAndroidError(500)\n\n// \tval = StatStorage.GetTotalCount()\n// \tassert.Equal(t, int64(100), val)\n// \tval = StatStorage.GetIosSuccess()\n// \tassert.Equal(t, int64(200), val)\n// \tval = StatStorage.GetIosError()\n// \tassert.Equal(t, int64(300), val)\n// \tval = StatStorage.GetAndroidSuccess()\n// \tassert.Equal(t, int64(400), val)\n// \tval = StatStorage.GetAndroidError()\n// \tassert.Equal(t, int64(500), val)\n// }\n\n// func TestStatForBadgerEngine(t *testing.T) {\n// \tvar val int64\n// \tcfg.Stat.Engine = \"badger\"\n// \terr := InitAppStatus()\n// \tassert.Nil(t, err)\n\n// \tStatStorage.Reset()\n\n// \tStatStorage.AddTotalCount(100)\n// \tStatStorage.AddIosSuccess(200)\n// \tStatStorage.AddIosError(300)\n// \tStatStorage.AddAndroidSuccess(400)\n// \tStatStorage.AddAndroidError(500)\n\n// \tval = StatStorage.GetTotalCount()\n// \tassert.Equal(t, int64(100), val)\n// \tval = StatStorage.GetIosSuccess()\n// \tassert.Equal(t, int64(200), val)\n// \tval = StatStorage.GetIosError()\n// \tassert.Equal(t, int64(300), val)\n// \tval = StatStorage.GetAndroidSuccess()\n// \tassert.Equal(t, int64(400), val)\n// \tval = StatStorage.GetAndroidError()\n// \tassert.Equal(t, int64(500), val)\n// }\n"
  },
  {
    "path": "status/storage.go",
    "content": "package status\n\nimport (\n\t\"github.com/appleboy/gorush/core\"\n)\n\ntype StateStorage struct {\n\tstore core.Storage\n}\n\nfunc NewStateStorage(store core.Storage) *StateStorage {\n\treturn &StateStorage{\n\t\tstore: store,\n\t}\n}\n\nfunc (s *StateStorage) Init() error {\n\treturn s.store.Init()\n}\n\nfunc (s *StateStorage) Close() error {\n\treturn s.store.Close()\n}\n\n// Reset Client storage.\nfunc (s *StateStorage) Reset() {\n\ts.store.Set(core.TotalCountKey, 0)\n\ts.store.Set(core.IosSuccessKey, 0)\n\ts.store.Set(core.IosErrorKey, 0)\n\ts.store.Set(core.AndroidSuccessKey, 0)\n\ts.store.Set(core.AndroidErrorKey, 0)\n\ts.store.Set(core.HuaweiSuccessKey, 0)\n\ts.store.Set(core.HuaweiErrorKey, 0)\n}\n\n// AddTotalCount record push notification count.\nfunc (s *StateStorage) AddTotalCount(count int64) {\n\ts.store.Add(core.TotalCountKey, count)\n}\n\n// AddIosSuccess record counts of success iOS push notification.\nfunc (s *StateStorage) AddIosSuccess(count int64) {\n\ts.store.Add(core.IosSuccessKey, count)\n}\n\n// AddIosError record counts of error iOS push notification.\nfunc (s *StateStorage) AddIosError(count int64) {\n\ts.store.Add(core.IosErrorKey, count)\n}\n\n// AddAndroidSuccess record counts of success Android push notification.\nfunc (s *StateStorage) AddAndroidSuccess(count int64) {\n\ts.store.Add(core.AndroidSuccessKey, count)\n}\n\n// AddAndroidError record counts of error Android push notification.\nfunc (s *StateStorage) AddAndroidError(count int64) {\n\ts.store.Add(core.AndroidErrorKey, count)\n}\n\n// AddHuaweiSuccess record counts of success Huawei push notification.\nfunc (s *StateStorage) AddHuaweiSuccess(count int64) {\n\ts.store.Add(core.HuaweiSuccessKey, count)\n}\n\n// AddHuaweiError record counts of error Huawei push notification.\nfunc (s *StateStorage) AddHuaweiError(count int64) {\n\ts.store.Add(core.HuaweiErrorKey, count)\n}\n\n// GetTotalCount show counts of all notification.\nfunc (s *StateStorage) GetTotalCount() int64 {\n\treturn s.store.Get(core.TotalCountKey)\n}\n\n// GetIosSuccess show success counts of iOS notification.\nfunc (s *StateStorage) GetIosSuccess() int64 {\n\treturn s.store.Get(core.IosSuccessKey)\n}\n\n// GetIosError show error counts of iOS notification.\nfunc (s *StateStorage) GetIosError() int64 {\n\treturn s.store.Get(core.IosErrorKey)\n}\n\n// GetAndroidSuccess show success counts of Android notification.\nfunc (s *StateStorage) GetAndroidSuccess() int64 {\n\treturn s.store.Get(core.AndroidSuccessKey)\n}\n\n// GetAndroidError show error counts of Android notification.\nfunc (s *StateStorage) GetAndroidError() int64 {\n\treturn s.store.Get(core.AndroidErrorKey)\n}\n\n// GetHuaweiSuccess show success counts of Huawei notification.\nfunc (s *StateStorage) GetHuaweiSuccess() int64 {\n\treturn s.store.Get(core.HuaweiSuccessKey)\n}\n\n// GetHuaweiError show error counts of Huawei notification.\nfunc (s *StateStorage) GetHuaweiError() int64 {\n\treturn s.store.Get(core.HuaweiErrorKey)\n}\n"
  },
  {
    "path": "storage/badger/badger.go",
    "content": "package badger\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/dgraph-io/badger/v4\"\n)\n\nvar _ core.Storage = (*Storage)(nil)\n\n// New func implements the storage interface for gorush (https://github.com/appleboy/gorush)\nfunc New(dbPath string) *Storage {\n\treturn &Storage{\n\t\tdbPath: dbPath,\n\t}\n}\n\n// Storage is interface structure\ntype Storage struct {\n\tdbPath string\n\topts   badger.Options\n\tname   string\n\tdb     *badger.DB\n\n\tsync.RWMutex\n}\n\nfunc (s *Storage) Add(key string, count int64) {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.setBadger(key, s.getBadger(key)+count)\n}\n\nfunc (s *Storage) Set(key string, count int64) {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.setBadger(key, count)\n}\n\nfunc (s *Storage) Get(key string) int64 {\n\ts.RLock()\n\tdefer s.RUnlock()\n\treturn s.getBadger(key)\n}\n\n// Init client storage.\nfunc (s *Storage) Init() error {\n\tvar err error\n\ts.name = \"badger\"\n\tif s.dbPath == \"\" {\n\t\ts.dbPath = os.TempDir() + \"badger\"\n\t}\n\ts.opts = badger.DefaultOptions(s.dbPath)\n\n\ts.db, err = badger.Open(s.opts)\n\n\treturn err\n}\n\n// Close the storage connection\nfunc (s *Storage) Close() error {\n\tif s.db == nil {\n\t\treturn nil\n\t}\n\n\treturn s.db.Close()\n}\n\nfunc (s *Storage) setBadger(key string, count int64) {\n\terr := s.db.Update(func(txn *badger.Txn) error {\n\t\tvalue := strconv.FormatInt(count, 10)\n\t\treturn txn.Set([]byte(key), []byte(value))\n\t})\n\tif err != nil {\n\t\tlog.Println(s.name, \"update error:\", err.Error())\n\t}\n}\n\nfunc (s *Storage) getBadger(key string) int64 {\n\tvar count int64\n\terr := s.db.View(func(txn *badger.Txn) error {\n\t\titem, err := txn.Get([]byte(key))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar dst []byte\n\t\tval, err := item.ValueCopy(dst)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcount, err = strconv.ParseInt(string(val), 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlog.Println(s.name, \"get error:\", err.Error())\n\t}\n\treturn count\n}\n"
  },
  {
    "path": "storage/badger/badger_test.go",
    "content": "package badger\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBadgerEngine(t *testing.T) {\n\tvar val int64\n\n\tbadger := New(\"\")\n\terr := badger.Init()\n\trequire.NoError(t, err)\n\n\t// reset the value of the key to 0\n\tbadger.Set(core.HuaweiSuccessKey, 0)\n\tval = badger.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\tbadger.Add(core.HuaweiSuccessKey, 10)\n\tval = badger.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\tbadger.Add(core.HuaweiSuccessKey, 10)\n\tval = badger.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(20), val)\n\n\tbadger.Set(core.HuaweiSuccessKey, 0)\n\tval = badger.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\t// test concurrency issues\n\tvar wg sync.WaitGroup\n\tfor range 100 {\n\t\twg.Go(func() {\n\t\t\tbadger.Add(core.HuaweiSuccessKey, 1)\n\t\t})\n\t}\n\twg.Wait()\n\tval = badger.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(100), val)\n\n\tassert.NoError(t, badger.Close())\n}\n"
  },
  {
    "path": "storage/boltdb/boltdb.go",
    "content": "package boltdb\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/asdine/storm/v3\"\n)\n\nvar _ core.Storage = (*Storage)(nil)\n\n// New func implements the storage interface for gorush (https://github.com/appleboy/gorush)\nfunc New(dbPath, bucket string) *Storage {\n\treturn &Storage{\n\t\tdbPath: dbPath,\n\t\tbucket: bucket,\n\t}\n}\n\n// Storage is interface structure\ntype Storage struct {\n\tdbPath string\n\tbucket string\n\tdb     *storm.DB\n\tsync.RWMutex\n}\n\nfunc (s *Storage) Add(key string, count int64) {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.setBoltDB(key, s.getBoltDB(key)+count)\n}\n\nfunc (s *Storage) Set(key string, count int64) {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.setBoltDB(key, count)\n}\n\nfunc (s *Storage) Get(key string) int64 {\n\ts.RLock()\n\tdefer s.RUnlock()\n\treturn s.getBoltDB(key)\n}\n\n// Init client storage.\nfunc (s *Storage) Init() error {\n\tvar err error\n\tif s.dbPath == \"\" {\n\t\ts.dbPath = os.TempDir() + \"boltdb.db\"\n\t}\n\ts.db, err = storm.Open(s.dbPath)\n\treturn err\n}\n\n// Close the storage connection\nfunc (s *Storage) Close() error {\n\tif s.db == nil {\n\t\treturn nil\n\t}\n\n\treturn s.db.Close()\n}\n\nfunc (s *Storage) setBoltDB(key string, count int64) {\n\terr := s.db.Set(s.bucket, key, count)\n\tif err != nil {\n\t\tlog.Println(\"BoltDB set error:\", err.Error())\n\t}\n}\n\nfunc (s *Storage) getBoltDB(key string) int64 {\n\tvar count int64\n\terr := s.db.Get(s.bucket, key, &count)\n\tif err != nil {\n\t\tlog.Println(\"BoltDB get error:\", err.Error())\n\t}\n\treturn count\n}\n"
  },
  {
    "path": "storage/boltdb/boltdb_test.go",
    "content": "package boltdb\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBoltDBEngine(t *testing.T) {\n\tvar val int64\n\n\tboltDB := New(\"\", \"gorush\")\n\terr := boltDB.Init()\n\trequire.NoError(t, err)\n\n\t// reset the value of the key to 0\n\tboltDB.Set(core.HuaweiSuccessKey, 0)\n\tval = boltDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\tboltDB.Add(core.HuaweiSuccessKey, 10)\n\tval = boltDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\tboltDB.Add(core.HuaweiSuccessKey, 10)\n\tval = boltDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(20), val)\n\n\tboltDB.Set(core.HuaweiSuccessKey, 0)\n\tval = boltDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\t// test concurrency issues\n\tvar wg sync.WaitGroup\n\tfor range 10 {\n\t\twg.Go(func() {\n\t\t\tboltDB.Add(core.HuaweiSuccessKey, 1)\n\t\t})\n\t}\n\twg.Wait()\n\tval = boltDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\n\tassert.NoError(t, boltDB.Close())\n}\n"
  },
  {
    "path": "storage/buntdb/buntdb.go",
    "content": "package buntdb\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/tidwall/buntdb\"\n)\n\nvar _ core.Storage = (*Storage)(nil)\n\n// New func implements the storage interface for gorush (https://github.com/appleboy/gorush)\nfunc New(dbPath string) *Storage {\n\treturn &Storage{\n\t\tdbPath: dbPath,\n\t}\n}\n\n// Storage is interface structure\ntype Storage struct {\n\tdbPath string\n\tdb     *buntdb.DB\n\tsync.RWMutex\n}\n\nfunc (s *Storage) Add(key string, count int64) {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.setBuntDB(key, s.getBuntDB(key)+count)\n}\n\nfunc (s *Storage) Set(key string, count int64) {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.setBuntDB(key, count)\n}\n\nfunc (s *Storage) Get(key string) int64 {\n\ts.RLock()\n\tdefer s.RUnlock()\n\treturn s.getBuntDB(key)\n}\n\n// Init client storage.\nfunc (s *Storage) Init() error {\n\tvar err error\n\tif s.dbPath == \"\" {\n\t\ts.dbPath = os.TempDir() + \"buntdb.db\"\n\t}\n\ts.db, err = buntdb.Open(s.dbPath)\n\treturn err\n}\n\n// Close the storage connection\nfunc (s *Storage) Close() error {\n\tif s.db == nil {\n\t\treturn nil\n\t}\n\n\treturn s.db.Close()\n}\n\nfunc (s *Storage) setBuntDB(key string, count int64) {\n\terr := s.db.Update(func(tx *buntdb.Tx) error {\n\t\tif _, _, err := tx.Set(key, strconv.FormatInt(count, 10), nil); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlog.Println(\"BuntDB update error:\", err.Error())\n\t}\n}\n\nfunc (s *Storage) getBuntDB(key string) int64 {\n\tvar count int64\n\terr := s.db.View(func(tx *buntdb.Tx) error {\n\t\tval, _ := tx.Get(key)\n\t\tcount, _ = strconv.ParseInt(val, 10, 64)\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlog.Println(\"BuntDB get error:\", err.Error())\n\t}\n\n\treturn count\n}\n"
  },
  {
    "path": "storage/buntdb/buntdb_test.go",
    "content": "package buntdb\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBuntDBEngine(t *testing.T) {\n\tvar val int64\n\n\tbuntDB := New(\"\")\n\terr := buntDB.Init()\n\trequire.NoError(t, err)\n\n\t// reset the value of the key to 0\n\tbuntDB.Set(core.HuaweiSuccessKey, 0)\n\tval = buntDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\tbuntDB.Add(core.HuaweiSuccessKey, 10)\n\tval = buntDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\tbuntDB.Add(core.HuaweiSuccessKey, 10)\n\tval = buntDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(20), val)\n\n\tbuntDB.Set(core.HuaweiSuccessKey, 0)\n\tval = buntDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\t// test concurrency issues\n\tvar wg sync.WaitGroup\n\tfor range 10 {\n\t\twg.Go(func() {\n\t\t\tbuntDB.Add(core.HuaweiSuccessKey, 1)\n\t\t})\n\t}\n\twg.Wait()\n\tval = buntDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\n\tassert.NoError(t, buntDB.Close())\n}\n"
  },
  {
    "path": "storage/leveldb/leveldb.go",
    "content": "package leveldb\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/syndtr/goleveldb/leveldb\"\n)\n\nvar _ core.Storage = (*Storage)(nil)\n\nfunc (s *Storage) setLevelDB(key string, count int64) {\n\tvalue := strconv.FormatInt(count, 10)\n\t_ = s.db.Put([]byte(key), []byte(value), nil)\n}\n\nfunc (s *Storage) getLevelDB(key string) int64 {\n\tdata, _ := s.db.Get([]byte(key), nil)\n\tcount, _ := strconv.ParseInt(string(data), 10, 64)\n\treturn count\n}\n\n// New func implements the storage interface for gorush (https://github.com/appleboy/gorush)\nfunc New(dbPath string) *Storage {\n\treturn &Storage{\n\t\tdbPath: dbPath,\n\t}\n}\n\n// Storage is interface structure\ntype Storage struct {\n\tdbPath string\n\tdb     *leveldb.DB\n\tsync.RWMutex\n}\n\nfunc (s *Storage) Add(key string, count int64) {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.setLevelDB(key, s.getLevelDB(key)+count)\n}\n\nfunc (s *Storage) Set(key string, count int64) {\n\ts.Lock()\n\tdefer s.Unlock()\n\ts.setLevelDB(key, count)\n}\n\nfunc (s *Storage) Get(key string) int64 {\n\ts.RLock()\n\tdefer s.RUnlock()\n\treturn s.getLevelDB(key)\n}\n\n// Init client storage.\nfunc (s *Storage) Init() error {\n\tvar err error\n\tif s.dbPath == \"\" {\n\t\ts.dbPath = os.TempDir() + \"leveldb.db\"\n\t}\n\ts.db, err = leveldb.OpenFile(s.dbPath, nil)\n\treturn err\n}\n\n// Close the storage connection\nfunc (s *Storage) Close() error {\n\tif s.db == nil {\n\t\treturn nil\n\t}\n\n\treturn s.db.Close()\n}\n"
  },
  {
    "path": "storage/leveldb/leveldb_test.go",
    "content": "package leveldb\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLevelDBEngine(t *testing.T) {\n\tvar val int64\n\n\tlevelDB := New(\"\")\n\terr := levelDB.Init()\n\trequire.NoError(t, err)\n\n\t// reset the value of the key to 0\n\tlevelDB.Set(core.HuaweiSuccessKey, 0)\n\tval = levelDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\tlevelDB.Add(core.HuaweiSuccessKey, 10)\n\tval = levelDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\tlevelDB.Add(core.HuaweiSuccessKey, 10)\n\tval = levelDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(20), val)\n\n\tlevelDB.Set(core.HuaweiSuccessKey, 0)\n\tval = levelDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\t// test concurrency issues\n\tvar wg sync.WaitGroup\n\tfor range 10 {\n\t\twg.Go(func() {\n\t\t\tlevelDB.Add(core.HuaweiSuccessKey, 1)\n\t\t})\n\t}\n\twg.Wait()\n\tval = levelDB.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\n\tassert.NoError(t, levelDB.Close())\n}\n"
  },
  {
    "path": "storage/memory/memory.go",
    "content": "package memory\n\nimport (\n\t\"sync\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"go.uber.org/atomic\"\n)\n\nvar _ core.Storage = (*Storage)(nil)\n\n// New func implements the storage interface for gorush (https://github.com/appleboy/gorush)\nfunc New() *Storage {\n\treturn &Storage{}\n}\n\n// Storage is interface structure\ntype Storage struct {\n\tmem sync.Map\n}\n\nfunc (s *Storage) getValueBtKey(key string) *atomic.Int64 {\n\tif val, ok := s.mem.Load(key); ok {\n\t\treturn val.(*atomic.Int64)\n\t}\n\tval := atomic.NewInt64(0)\n\ts.mem.Store(key, val)\n\treturn val\n}\n\nfunc (s *Storage) Add(key string, count int64) {\n\ts.getValueBtKey(key).Add(count)\n}\n\nfunc (s *Storage) Set(key string, count int64) {\n\ts.getValueBtKey(key).Store(count)\n}\n\nfunc (s *Storage) Get(key string) int64 {\n\treturn s.getValueBtKey(key).Load()\n}\n\n// Init client storage.\nfunc (*Storage) Init() error {\n\treturn nil\n}\n\n// Close the storage connection\nfunc (*Storage) Close() error {\n\treturn nil\n}\n"
  },
  {
    "path": "storage/memory/memory_test.go",
    "content": "package memory\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMemoryEngine(t *testing.T) {\n\tvar val int64\n\n\tmemory := New()\n\terr := memory.Init()\n\trequire.NoError(t, err)\n\n\t// reset the value of the key to 0\n\tmemory.Set(core.HuaweiSuccessKey, 0)\n\tval = memory.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\tmemory.Add(core.HuaweiSuccessKey, 10)\n\tval = memory.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\tmemory.Add(core.HuaweiSuccessKey, 10)\n\tval = memory.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(20), val)\n\n\tmemory.Set(core.HuaweiSuccessKey, 0)\n\tval = memory.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\t// test concurrency issues\n\tvar wg sync.WaitGroup\n\tfor range 10 {\n\t\twg.Go(func() {\n\t\t\tmemory.Add(core.HuaweiSuccessKey, 1)\n\t\t})\n\t}\n\twg.Wait()\n\tval = memory.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\n\tassert.NoError(t, memory.Close())\n}\n"
  },
  {
    "path": "storage/redis/redis.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar _ core.Storage = (*Storage)(nil)\n\n// New func implements the storage interface for gorush (https://github.com/appleboy/gorush)\nfunc New(\n\taddr string,\n\tusername string,\n\tpassword string,\n\tdb int,\n\tisCluster bool,\n) *Storage {\n\treturn &Storage{\n\t\tctx:       context.Background(),\n\t\taddr:      addr,\n\t\tusername:  username,\n\t\tpassword:  password,\n\t\tdb:        db,\n\t\tisCluster: isCluster,\n\t}\n}\n\n// Storage is interface structure\ntype Storage struct {\n\tctx       context.Context\n\tclient    redis.Cmdable\n\taddr      string\n\tusername  string\n\tpassword  string\n\tdb        int\n\tisCluster bool\n}\n\nfunc (s *Storage) Add(key string, count int64) {\n\ts.client.IncrBy(s.ctx, key, count)\n}\n\nfunc (s *Storage) Set(key string, count int64) {\n\ts.client.Set(s.ctx, key, count, 0)\n}\n\nfunc (s *Storage) Get(key string) int64 {\n\tval, _ := s.client.Get(s.ctx, key).Result()\n\tcount, _ := strconv.ParseInt(val, 10, 64)\n\treturn count\n}\n\n// Init client storage.\nfunc (s *Storage) Init() error {\n\tif s.isCluster {\n\t\ts.client = redis.NewClusterClient(&redis.ClusterOptions{\n\t\t\tAddrs:    strings.Split(s.addr, \",\"),\n\t\t\tUsername: s.username,\n\t\t\tPassword: s.password,\n\t\t})\n\t} else {\n\t\ts.client = redis.NewClient(&redis.Options{\n\t\t\tAddr:     s.addr,\n\t\t\tPassword: s.password,\n\t\t\tDB:       s.db,\n\t\t})\n\t}\n\n\treturn s.client.Ping(s.ctx).Err()\n}\n\n// Close the storage connection\nfunc (s *Storage) Close() error {\n\tswitch v := s.client.(type) {\n\tcase *redis.Client:\n\t\treturn v.Close()\n\tcase *redis.ClusterClient:\n\t\treturn v.Close()\n\tcase nil:\n\t\treturn nil\n\tdefault:\n\t\t// this will not happen anyway, unless we mishandle it on `Init`\n\t\tpanic(fmt.Sprintf(\"invalid redis client: %v\", reflect.TypeOf(v)))\n\t}\n}\n"
  },
  {
    "path": "storage/redis/redis_test.go",
    "content": "package redis\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/appleboy/gorush/core\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRedisServerError(t *testing.T) {\n\tredis := New(\n\t\t\"redis:6370\", // addr\n\t\t\"\",           // username\n\t\t\"\",           // password\n\t\t0,            // db\n\t\tfalse,        // cluster\n\t)\n\terr := redis.Init()\n\n\tassert.Error(t, err)\n}\n\nfunc TestRedisEngine(t *testing.T) {\n\tvar val int64\n\n\tredis := New(\n\t\t\"redis:6379\", // addr\n\t\t\"\",           // username\n\t\t\"\",           // password\n\t\t0,            // db\n\t\tfalse,        // cluster\n\t)\n\terr := redis.Init()\n\trequire.NoError(t, err)\n\n\t// reset the value of the key to 0\n\tredis.Set(core.HuaweiSuccessKey, 0)\n\tval = redis.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\tredis.Add(core.HuaweiSuccessKey, 10)\n\tval = redis.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\tredis.Add(core.HuaweiSuccessKey, 10)\n\tval = redis.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(20), val)\n\n\tredis.Set(core.HuaweiSuccessKey, 0)\n\tval = redis.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(0), val)\n\n\t// test concurrency issues\n\tvar wg sync.WaitGroup\n\tfor range 10 {\n\t\twg.Go(func() {\n\t\t\tredis.Add(core.HuaweiSuccessKey, 1)\n\t\t})\n\t}\n\twg.Wait()\n\tval = redis.Get(core.HuaweiSuccessKey)\n\tassert.Equal(t, int64(10), val)\n\n\tassert.NoError(t, redis.Close())\n}\n"
  },
  {
    "path": "storage/storage.go",
    "content": "package storage\n"
  },
  {
    "path": "tests/README.md",
    "content": "# Testing\n\nHow to test gorush with http request?\n\n## download bat tool\n\nDownload [cURL-like tool for humans](https://github.com/astaxie/bat).\n\n## testing\n\nsee the JSON format:\n\n```json\n{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n      \"message\": \"Hello World iOS!\"\n    },\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 2,\n      \"message\": \"Hello World Android!\"\n    }\n  ]\n}\n```\n\nrun the following command.\n\n```sh\nbat POST localhost:8088/api/push < tests/test.json\n```\n\nHere is a sample shell code to calculate factorial using while loop:\n\n```sh\n#!/bin/bash\ncounter=$1\nwhile [ $counter -gt 0 ]\ndo\n  bat POST https://gorush.netlify.app/api/push < tests/test.json\n  counter=$(( $counter - 1 ))\n  echo $counter\ndone\n```\n"
  },
  {
    "path": "tests/test.json",
    "content": "{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n      \"message\": \"Hello World iOS!\",\n      \"title\": \"Gorush with iOS\"\n    },\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 2,\n      \"message\": \"Hello World Android!\",\n      \"title\": \"Gorush with Android\"\n    },\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 3,\n      \"message\": \"Hello World Huawei!\",\n      \"title\": \"Gorush with HMS\"\n    }\n  ]\n}\n"
  },
  {
    "path": "trivy.yaml",
    "content": "# Trivy configuration file for gorush project\n# Docs: https://aquasecurity.github.io/trivy/latest/configuration/config-file/\n\nscan:\n  skip-dirs:\n    - certificate\n\n# You can add more config options below as needed.\n"
  }
]