Full Code of appleboy/gorush for AI

master 03c17fd3b1e2 cached
126 files
534.7 KB
173.0k tokens
602 symbols
1 requests
Download .txt
Showing preview only (567K chars total). Download the full file or copy to clipboard to get everything.
Repository: appleboy/gorush
Branch: master
Commit: 03c17fd3b1e2
Files: 126
Total size: 534.7 KB

Directory structure:
gitextract_lladjky6/

├── .dockerignore
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql.yaml
│       ├── docker.yaml
│       ├── goreleaser.yml
│       ├── testing.yml
│       └── trivy-scan.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .hadolint.yaml
├── .roomodes
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README.md
├── app/
│   ├── config.go
│   ├── config_test.go
│   ├── options.go
│   ├── options_test.go
│   ├── sender.go
│   ├── sender_test.go
│   ├── worker.go
│   └── worker_test.go
├── certificate/
│   ├── authkey-invalid.p8
│   ├── authkey-valid.p8
│   ├── certificate-valid.p12
│   ├── certificate-valid.pem
│   ├── localhost.cert
│   └── localhost.key
├── config/
│   ├── config.go
│   ├── config_test.go
│   └── testdata/
│       ├── empty.yml
│       └── redis_db_config.yml
├── contrib/
│   └── init/
│       └── debian/
│           ├── README.md
│           └── gorush
├── core/
│   ├── core.go
│   ├── core_test.go
│   ├── health.go
│   ├── queue.go
│   └── storage.go
├── doc.go
├── docker/
│   └── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── helm/
│   └── gorush/
│       ├── .helmignore
│       ├── Chart.yaml
│       ├── templates/
│       │   ├── NOTES.txt
│       │   ├── _helpers.tpl
│       │   ├── configmap.yml
│       │   ├── deployment.yaml
│       │   ├── hpa.yaml
│       │   ├── ingress.yaml
│       │   ├── service.yaml
│       │   └── serviceaccount.yaml
│       └── values.yaml
├── install.sh
├── k8s/
│   ├── gorush-aws-alb-ingress.yaml
│   ├── gorush-configmap.yaml
│   ├── gorush-deployment.yaml
│   ├── gorush-namespace.yaml
│   ├── gorush-redis-deployment.yaml
│   ├── gorush-redis-service.yaml
│   └── gorush-service.yaml
├── logx/
│   ├── log/
│   │   ├── .gitkeep
│   │   └── access.log
│   ├── log.go
│   ├── log_interface.go
│   └── log_test.go
├── main.go
├── metric/
│   ├── metrics.go
│   └── metrics_test.go
├── netlify.toml
├── notify/
│   ├── feedback.go
│   ├── feedback_test.go
│   ├── global.go
│   ├── main_test.go
│   ├── notification.go
│   ├── notification_apns.go
│   ├── notification_apns_test.go
│   ├── notification_fcm.go
│   ├── notification_fcm_test.go
│   ├── notification_hms.go
│   ├── notification_hms_test.go
│   └── notification_test.go
├── router/
│   ├── server.go
│   ├── server_lambda.go
│   ├── server_normal.go
│   ├── server_test.go
│   └── version.go
├── rpc/
│   ├── client_grpc_health.go
│   ├── client_test.go
│   ├── example/
│   │   ├── go/
│   │   │   ├── health/
│   │   │   │   └── main.go
│   │   │   └── send/
│   │   │       └── main.go
│   │   └── node/
│   │       ├── .gitignore
│   │       ├── README.md
│   │       ├── client.js
│   │       ├── gorush_grpc_pb.js
│   │       ├── gorush_pb.js
│   │       └── package.json
│   ├── proto/
│   │   ├── gorush.pb.go
│   │   ├── gorush.proto
│   │   └── gorush_grpc.pb.go
│   ├── server.go
│   └── server_test.go
├── status/
│   ├── status.go
│   ├── status_test.go
│   └── storage.go
├── storage/
│   ├── badger/
│   │   ├── badger.go
│   │   └── badger_test.go
│   ├── boltdb/
│   │   ├── boltdb.go
│   │   └── boltdb_test.go
│   ├── buntdb/
│   │   ├── buntdb.go
│   │   └── buntdb_test.go
│   ├── leveldb/
│   │   ├── leveldb.go
│   │   └── leveldb_test.go
│   ├── memory/
│   │   ├── memory.go
│   │   └── memory_test.go
│   ├── redis/
│   │   ├── redis.go
│   │   └── redis_test.go
│   └── storage.go
├── tests/
│   ├── README.md
│   └── test.json
└── trivy.yaml

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
*
!release/


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: appleboy
patreon: # Replace with a single Patreon username
open_collective: gorush
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://www.paypal.me/appleboy46']


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
  - package-ecosystem: gomod
    directory: /
    schedule:
      interval: weekly


================================================
FILE: .github/workflows/codeql.yaml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"

on:
  push:
    branches: [master]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [master]
  schedule:
    - cron: "30 1 * * 0"

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest

    permissions:
      # required for all workflows
      security-events: write

      # only required for workflows in private repositories
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        # Override automatic language detection by changing the below list
        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
        # TODO: Enable for javascript later
        language: ["go"]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      # Initializes the CodeQL tools for scanning.
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v4
        with:
          languages: ${{ matrix.language }}
          # If you wish to specify custom queries, you can do so here or in a config file.
          # By default, queries listed here will override any specified in a config file.
          # Prefix the list here with "+" to use these queries and those in the config file.
          # queries: ./path/to/local/query, your-org/your-repo/queries@main

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v4


================================================
FILE: .github/workflows/docker.yaml
================================================
name: Docker Image

on:
  push:
    branches:
      - master
    tags:
      - "v*"
  pull_request:
    branches:
      - "master"

jobs:
  build-docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Setup go
        uses: actions/setup-go@v6
        with:
          go-version-file: go.mod
          check-latest: true

      - name: Build binary
        run: |
          make build_linux_amd64
          make build_linux_arm
          make build_linux_arm64
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Login to Docker Hub
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v4
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker meta
        id: docker-meta
        uses: docker/metadata-action@v6
        with:
          images: |
            ${{ github.repository }}
            ghcr.io/${{ github.repository }}
          tags: |
            type=raw,value=latest,enable={{is_default_branch}}
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          platforms: linux/amd64,linux/arm,linux/arm64
          file: docker/Dockerfile
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.docker-meta.outputs.tags }}
          labels: ${{ steps.docker-meta.outputs.labels }}


================================================
FILE: .github/workflows/goreleaser.yml
================================================
name: Goreleaser

on:
  push:
    tags:
      - "*"

permissions:
  contents: write

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Setup go
        uses: actions/setup-go@v6
        with:
          go-version-file: go.mod
          check-latest: true
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v7
        with:
          # either 'goreleaser' (default) or 'goreleaser-pro'
          distribution: goreleaser
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/testing.yml
================================================
name: Run Lint and Testing

on:
  push:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup go
        uses: actions/setup-go@v6
        with:
          go-version-file: go.mod
          check-latest: true

      - name: check golangci-lint
        uses: golangci/golangci-lint-action@v9
        with:
          version: v2.7
          args: --verbose

      - uses: hadolint/hadolint-action@v3.3.0
        name: hadolint for Dockerfile
        with:
          dockerfile: docker/Dockerfile

  testing:
    runs-on: ubuntu-latest
    container: node:16-bullseye

    # Service containers to run with `container-job`
    services:
      # Label used to access the service container
      redis:
        # Docker Hub image
        image: redis
        # Set health checks to wait until redis has started
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup go
        uses: actions/setup-go@v6
        with:
          go-version-file: go.mod
          check-latest: true

      - name: testing
        env:
          FCM_CREDENTIAL: ${{ secrets.FCM_CREDENTIAL }}
          FCM_TEST_TOKEN: ${{ secrets.FCM_TEST_TOKEN }}
        run: make test

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v5
        with:
          flags: ${{ matrix.os }},go-${{ matrix.go }}


================================================
FILE: .github/workflows/trivy-scan.yml
================================================
name: Trivy Security Scan

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master
  schedule:
    - cron: '0 2 * * *'  # Run daily at 2 AM UTC
  workflow_dispatch:  # Allow manual triggering

jobs:
  trivy-scan-repo:
    name: Scan Repository (Filesystem)
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Run Trivy vulnerability scanner in repo mode
        uses: aquasecurity/trivy-action@0.35.0
        with:
          scan-type: 'fs'
          ignore-unfixed: true
          format: 'table'
          exit-code: '1'
          severity: 'CRITICAL,HIGH,MEDIUM'

  trivy-scan-dockerhub:
    name: Scan Docker Hub Image
    runs-on: ubuntu-latest
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Run Trivy vulnerability scanner (Docker Hub)
        uses: aquasecurity/trivy-action@0.35.0
        with:
          image-ref: 'appleboy/gorush:latest'
          format: 'sarif'
          output: 'trivy-dockerhub-results.sarif'

      - name: Upload Trivy scan results to GitHub Security tab (Docker Hub)
        uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: 'trivy-dockerhub-results.sarif'

      - name: Run Trivy vulnerability scanner (Docker Hub Table format)
        uses: aquasecurity/trivy-action@0.35.0
        with:
          image-ref: 'appleboy/gorush:latest'
          format: 'table'
          exit-code: '1'
          ignore-unfixed: true
          vuln-type: 'os,library'
          severity: 'CRITICAL,HIGH'

  trivy-scan-ghcr:
    name: Scan GHCR Image
    runs-on: ubuntu-latest
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Run Trivy vulnerability scanner (GHCR)
        uses: aquasecurity/trivy-action@0.35.0
        with:
          image-ref: 'ghcr.io/appleboy/gorush:latest'
          format: 'sarif'
          output: 'trivy-ghcr-results.sarif'

      - name: Upload Trivy scan results to GitHub Security tab (GHCR)
        uses: github/codeql-action/upload-sarif@v4
        if: always()
        with:
          sarif_file: 'trivy-ghcr-results.sarif'

      - name: Run Trivy vulnerability scanner (GHCR Table format)
        uses: aquasecurity/trivy-action@0.35.0
        with:
          image-ref: 'ghcr.io/appleboy/gorush:latest'
          format: 'table'
          exit-code: '1'
          ignore-unfixed: true
          vuln-type: 'os,library'
          severity: 'CRITICAL,HIGH'


================================================
FILE: .gitignore
================================================
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof

gin-bin
key.pem
.DS_Store
gorush/log/*.log
gorush.db
.cover
*.db*
coverage.txt
dist
custom
release
coverage.txt
node_modules
config.yml
dist/
.idea
.claude


================================================
FILE: .golangci.yml
================================================
version: "2"
output:
  sort-order:
    - file
linters:
  default: none
  enable:
    - bidichk
    - bodyclose
    - depguard
    - errcheck
    - forbidigo
    - gocheckcompilerdirectives
    - gocritic
    - govet
    - ineffassign
    - mirror
    - modernize
    - nakedret
    - nilnil
    - nolintlint
    - perfsprint
    - revive
    - staticcheck
    - testifylint
    - unconvert
    - unparam
    - unused
    - usestdlibvars
    - usetesting
    - wastedassign
  settings:
    depguard:
      rules:
        main:
          deny:
            - pkg: io/ioutil
              desc: use os or io instead
            - pkg: golang.org/x/exp
              desc: it's experimental and unreliable
            - pkg: github.com/pkg/errors
              desc: use builtin errors package instead
    nolintlint:
      allow-unused: false
      require-explanation: true
      require-specific: true
    gocritic:
      enabled-checks:
        - equalFold
      disabled-checks: []
    revive:
      severity: error
      rules:
        - name: blank-imports
        - name: constant-logical-expr
        - name: context-as-argument
        - name: context-keys-type
        - name: dot-imports
        - name: empty-lines
        - name: error-return
        - name: error-strings
        - name: exported
        - name: identical-branches
        - name: if-return
        - name: increment-decrement
        - name: modifies-value-receiver
        - name: package-comments
        - name: redefines-builtin-id
        - name: superfluous-else
        - name: time-naming
        - name: unexported-return
        - name: var-declaration
        - name: var-naming
          disabled: true
    staticcheck:
      checks:
        - all
    testifylint: {}
    usetesting:
      os-temp-dir: true
    perfsprint:
      concat-loop: false
    govet:
      enable:
        - nilness
        - unusedwrite
  exclusions:
    generated: lax
    presets:
      - comments
      - common-false-positives
      - legacy
      - std-error-handling
    rules:
      - linters:
          - errcheck
          - staticcheck
          - unparam
        path: _test\.go
issues:
  max-issues-per-linter: 0
  max-same-issues: 0
formatters:
  enable:
    - gofmt
    - gofumpt
    - golines
  settings:
    gofumpt:
      extra-rules: true
  exclusions:
    generated: lax
run:
  timeout: 10m


================================================
FILE: .goreleaser.yaml
================================================
version: 2

before:
  hooks:
    - go mod tidy

builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - darwin
      - linux
      - windows
      - freebsd
    goarch:
      - "386"
      - amd64
      - arm
      - arm64
    goarm:
      - "5"
      - "6"
      - "7"
    ignore:
      - goos: darwin
        goarch: arm
      - goos: darwin
        goarch: ppc64le
      - goos: darwin
        goarch: s390x
      - goos: windows
        goarch: ppc64le
      - goos: windows
        goarch: s390x
      - goos: windows
        goarch: arm
        goarm: "5"
      - goos: windows
        goarch: arm
        goarm: "6"
      - goos: windows
        goarch: arm
        goarm: "7"
      - goos: windows
        goarch: arm64
      - goos: freebsd
        goarch: ppc64le
      - goos: freebsd
        goarch: s390x
      - goos: freebsd
        goarch: arm
        goarm: "5"
      - goos: freebsd
        goarch: arm
        goarm: "6"
      - goos: freebsd
        goarch: arm
        goarm: "7"
      - goos: freebsd
        goarch: arm64
    flags:
      - -trimpath
    ldflags:
      - -s -w
      - -X main.version={{.Version}}
      - -X main.commit={{.ShortCommit}}
    binary: >-
      {{ .ProjectName }}-
      {{- if .IsSnapshot }}{{ .Branch }}-
      {{- else }}{{- .Version }}-{{ end }}
      {{- .Os }}-
      {{- if eq .Arch "amd64" }}amd64
      {{- else if eq .Arch "amd64_v1" }}amd64
      {{- else if eq .Arch "386" }}386
      {{- else }}{{ .Arch }}{{ end }}
      {{- if .Arm }}-{{ .Arm }}{{ end }}
    no_unique_dist_dir: true

archives:
  - format: binary
    name_template: "{{ .Binary }}"
    allow_different_binary_count: true

checksum:
  name_template: "checksums.txt"

snapshot:
  version_template: "{{ incpatch .Version }}"

changelog:
  use: github
  groups:
    - title: Features
      regexp: "^.*feat[(\\w)]*:+.*$"
      order: 0
    - title: "Bug fixes"
      regexp: "^.*fix[(\\w)]*:+.*$"
      order: 1
    - title: "Enhancements"
      regexp: "^.*chore[(\\w)]*:+.*$"
      order: 2
    - title: "Refactor"
      regexp: "^.*refactor[(\\w)]*:+.*$"
      order: 3
    - title: "Build process updates"
      regexp: ^.*?(build|ci)(\(.+\))??!?:.+$
      order: 4
    - title: "Documentation updates"
      regexp: ^.*?docs?(\(.+\))??!?:.+$
      order: 4
    - title: Others
      order: 999


================================================
FILE: .hadolint.yaml
================================================
ignored:
  - DL3018


================================================
FILE: .roomodes
================================================
customModes:
  - slug: go-code-tester
    name: 🧪 Go Code Tester
    description: Go testing and quality expert
    roleDefinition: >-
      You are Roo, a Golang testing and quality assurance expert specializing in Go testing ecosystem. Your expertise includes:
      - Writing Go unit tests using the standard testing package
      - Table-driven tests and subtests in Go
      - Go benchmarks and performance testing
      - Test coverage analysis with go test -cover
      - Mock generation and testing with gomock, testify/mock
      - Integration testing for Go web services and APIs
      - Testing Go HTTP handlers and middleware
      - Go race condition detection with go test -race
      - Testing Go concurrency and goroutines
      - Go fuzz testing (go test -fuzz)
      - Testcontainers for Go integration testing
      - Go testing best practices and conventions
    whenToUse: >-
      Use this mode when you need to write Go tests, improve test coverage, debug test failures,
      set up Go testing frameworks, create test automation for Go projects, or ensure code quality through
      comprehensive Go testing strategies. Perfect for TDD workflows in Go, bug hunting, and
      establishing robust testing pipelines for Go applications.
    groups:
      - read
      - edit
      - command
      - mcp
    customInstructions: >-
      Focus on creating comprehensive, maintainable Go tests that follow Go testing conventions.
      Always consider edge cases, error conditions, and boundary value testing.
      When writing Go tests, ensure they:
      - Follow Go naming conventions (TestXxx functions)
      - Use table-driven tests for multiple test cases
      - Leverage t.Run() for subtests when appropriate
      - Include proper error handling and assertions
      - Use testify/assert or require for cleaner assertions
      - Follow the AAA pattern (Arrange, Act, Assert)
      - Include benchmarks for performance-critical code
      - Use build tags for integration tests when needed

      Prefer Go standard library testing package with minimal dependencies.
      Use descriptive test function names that clearly explain the scenario being tested.
      Always run tests with go test -v -race -cover for comprehensive validation.

  - slug: go-code-reviewer
    name: 🔍 Go Code Reviewer
    description: Go code review and quality expert
    roleDefinition: >-
      You are Roo, a Go code review expert specializing in code quality, performance, and best practices. Your expertise includes:
      - Go code style and formatting analysis (gofmt, golint, golangci-lint)
      - Performance optimization and memory efficiency review
      - Concurrency and goroutine safety analysis
      - Error handling patterns and best practices
      - Code security vulnerability assessment
      - Go idioms and design patterns evaluation
      - API design and interface recommendations
      - Dependency management and module structure review
      - Code maintainability and readability assessment
      - Go standard library usage optimization
      - Race condition detection and prevention
      - Memory leak identification and prevention
      - Code complexity analysis and refactoring suggestions
      - Documentation and comment quality evaluation
    whenToUse: >-
      Use this mode when you need to review Go code for quality, performance, security, or maintainability issues.
      Perfect for code reviews, pull request analysis, refactoring guidance, performance optimization,
      security audits, and ensuring Go best practices compliance. Ideal for identifying potential bugs,
      improving code structure, and mentoring developers on Go coding standards.
    groups:
      - read
      - - edit
        - fileRegex: \.go$
          description: Go source files only
      - command
      - mcp
    customInstructions: >-
      When reviewing Go code, focus on:

      CODE QUALITY:
      - Follow Go coding conventions and style guidelines
      - Check proper error handling patterns (avoid ignoring errors)
      - Ensure proper variable and function naming (camelCase, exported vs unexported)
      - Verify correct use of Go idioms and patterns
      - Assess code readability and maintainability

      PERFORMANCE:
      - Identify unnecessary memory allocations
      - Review string concatenation patterns (prefer strings.Builder for multiple concatenations)
      - Check for efficient slice and map usage
      - Analyze goroutine usage and potential leaks
      - Review context usage in long-running operations

      SECURITY:
      - Check for SQL injection vulnerabilities
      - Review input validation and sanitization
      - Identify potential race conditions
      - Check for proper secrets handling
      - Review error message information leakage

      CONCURRENCY:
      - Verify proper channel usage and closing
      - Check for goroutine leaks and proper cleanup
      - Review mutex usage and deadlock prevention
      - Analyze shared state access patterns
      - Ensure proper context propagation

      ARCHITECTURE:
      - Review package structure and dependencies
      - Check interface usage and abstraction levels
      - Assess separation of concerns
      - Review error types and custom error handling
      - Evaluate API design and backwards compatibility

      Always provide specific, actionable feedback with code examples when suggesting improvements.
      Prioritize critical issues (security, correctness) over style preferences.
      Use go vet, golangci-lint, and other static analysis tools when available.

  - slug: go-code-developer
    name: 🚀 Go Code Developer
    description: Go development and implementation expert
    roleDefinition: >-
      You are Roo, a Go development expert specializing in writing high-quality, idiomatic Go code. Your expertise includes:
      - Go syntax, language features, and standard library mastery
      - Writing clean, readable, and maintainable Go code
      - Go modules and dependency management
      - Implementing Go interfaces and struct design
      - Goroutines, channels, and concurrent programming patterns
      - Error handling best practices and custom error types
      - Go HTTP server development with net/http
      - JSON/XML marshaling and unmarshaling
      - Database integration with SQL drivers and ORMs
      - Command-line application development with flag package
      - Go build system, cross-compilation, and deployment
      - Context usage for cancellation and timeouts
      - Go generics and type parameters (Go 1.18+)
      - File I/O and system programming in Go
      - Go toolchain usage (go fmt, go vet, go mod, etc.)
    whenToUse: >-
      Use this mode when you need to write, implement, or refactor Go code. Perfect for creating new Go applications,
      implementing features, building APIs, developing CLI tools, or any Go programming task. Ideal for code implementation,
      algorithm development, data structure design, and building complete Go solutions from scratch.
    groups:
      - read
      - edit
      - command
      - mcp
    customInstructions: >-
      When writing Go code, always follow these principles:

      CODE STYLE:
      - Follow Go conventions: use gofmt for formatting, follow naming conventions
      - Use descriptive variable and function names (camelCase for unexported, PascalCase for exported)
      - Keep functions small and focused on single responsibility
      - Prefer composition over inheritance
      - Use interfaces to define behavior, not data
      - Write comments for exported functions and types using proper GoDoc format
      - Use error wrapping with fmt.Errorf and %w verb
      - Avoid global variables; prefer dependency injection
      - Use slices and maps idiomatically
      - Don't use if else patterns unnecessarily; prefer early returns

      ERROR HANDLING:
      - Always handle errors explicitly, never ignore them
      - Use custom error types when appropriate
      - Wrap errors with context using fmt.Errorf with %w verb
      - Return errors as the last return value
      - Use errors.Is() and errors.As() for error checking

      CONCURRENCY:
      - Use goroutines for concurrent operations
      - Employ channels for communication between goroutines
      - Always close channels when done sending
      - Use context.Context for cancellation and timeouts
      - Avoid shared mutable state, prefer message passing

      PERFORMANCE:
      - Use string builders for multiple string concatenations
      - Preallocate slices and maps when size is known
      - Avoid unnecessary allocations in hot paths
      - Use sync.Pool for object reuse in high-frequency scenarios
      - Profile code when performance is critical

      STANDARD PRACTICES:
      - Use go modules for dependency management
      - Write self-documenting code with clear function signatures
      - Leverage the standard library before adding external dependencies
      - Use build tags for conditional compilation
      - Implement proper logging with structured logging when needed

      Always write idiomatic Go code that is simple, readable, and efficient.
      Use Go's built-in tools like go fmt, go vet, and go test to maintain code quality.


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Gorush 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.

## Architecture

### Request Flow

1. **main.go** parses CLI flags, loads config, initializes platform clients (APNS/FCM/HMS), then starts HTTP (Gin) and gRPC servers via `graceful.Manager`
2. **router/** receives push requests at `POST /api/push`, validates them, and enqueues `PushNotification` messages into the queue
3. **app/worker.go** creates the queue worker (local/NSQ/NATS/Redis) with `notify.Run(cfg)` as the processing function
4. **notify/** dispatches notifications to platform-specific push functions (`PushToIOS`, `PushToAndroid`, `PushToHuawei`)
5. **status/** + **storage/** track success/failure counts across configurable backends

### Key Packages

- **app/**: Application orchestration — CLI send helpers (`sender.go`), config validation/merge (`config.go`), CLI options (`options.go`), queue worker creation (`worker.go`)
- **config/**: YAML config with Viper, env var overrides. Reference config: `config/testdata/config.yml`
- **core/**: Shared types and interfaces — `Platform` enum, queue engine constants (`core/queue.go`), storage interface (`core/storage.go`), health check interface (`core/health.go`)
- **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
- **router/**: Gin HTTP server with REST endpoints and Prometheus metrics
- **rpc/**: gRPC server. Proto definitions in `rpc/proto/`
- **storage/**: Storage backends (memory, Redis, BoltDB, BuntDB, LevelDB, BadgerDB) all implement `core.Storage`
- **logx/**: Logging utilities wrapping zerolog/logrus

## Development Commands

### Build and Run

```bash
make build                  # Build binary to release/gorush
make install                # Install to $GOPATH/bin
make dev                    # Hot reload with air
```

### Testing

```bash
# Full test suite (requires FCM credentials)
export FCM_CREDENTIAL="/path/to/firebase-credentials.json"
export FCM_TEST_TOKEN="your_test_device_token"
make test

# Run a single test
go test -v -tags sqlite -run TestFunctionName ./package/...

# Run tests for a specific package
go test -v -tags sqlite ./notify/...
go test -v -tags sqlite ./config/...
```

The `-tags sqlite` flag is required for all test commands (it's the default build tag).

### Linting and Formatting

```bash
make lint                   # Run golangci-lint (auto-installs if missing)
make fmt                    # Format code with golangci-lint fmt
```

Linter config is in `.golangci.yml` (v2 format). Uses golangci-lint v2.

### Protocol Buffers

```bash
make generate_proto         # Generate both Go and JS proto files
```

## Build Tags

- `sqlite` — default tag, required for standard builds and tests
- `lambda` — for AWS Lambda builds (replaces sqlite)
- Set custom tags via `TAGS` environment variable

## Configuration

Config 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.

## API Endpoints

- `POST /api/push` — send push notifications
- `GET /api/stat/go` — Go runtime stats
- `GET /api/stat/app` — push statistics
- `GET /api/config` — current config
- `GET /metrics` — Prometheus metrics
- `GET /healthz` — health check


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 Bo-Yi Wu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
EXECUTABLE := gorush
GO ?= go
GOFILES := $(shell find . -name "*.go" -type f)
TAGS ?= sqlite
LDFLAGS ?= -X main.version=$(VERSION) -X main.commit=$(COMMIT)

PROTOC_GEN_GO=v1.36.6
PROTOC_GEN_GO_GRPC=v1.5.1

ifneq ($(shell uname), Darwin)
	EXTLDFLAGS = -extldflags "-static" $(null)
else
	EXTLDFLAGS =
endif

ifneq ($(DRONE_TAG),)
	VERSION ?= $(DRONE_TAG)
else
	VERSION ?= $(shell git describe --tags --always || git rev-parse --short HEAD)
endif

COMMIT ?= $(shell git rev-parse --short HEAD)

all: build

.PHONY: help
help: ## Print this help message.
	@echo "Usage: make [target]"
	@echo ""
	@echo "Targets:"
	@echo ""
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

init: ## check the FCM_CREDENTIAL and FCM_TEST_TOKEN
	@echo "==> Check FCM_CREDENTIAL and FCM_TEST_TOKEN"
ifeq ($(FCM_CREDENTIAL),)
	@echo "Missing FCM_CREDENTIAL Parameter"
	@exit 1
endif
ifeq ($(FCM_TEST_TOKEN),)
	@echo "Missing FCM_TEST_TOKEN Parameter"
	@exit 1
endif
	@echo "Already set FCM_CREDENTIAL and endif global variable."

.PHONY: install ## Install the gorush binary
install: $(GOFILES)
	$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
	@echo "\n==>\033[32m Installed gorush to ${GOPATH}/bin/gorush\033[m"

.PHONY: build ## Build the gorush binary
build: $(EXECUTABLE)

.PHONY: $(EXECUTABLE)
$(EXECUTABLE): $(GOFILES)
	$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/$@

.PHONY: test ## Run the tests
test: init
	@$(GO) test -v -cover -tags $(TAGS) -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1

build_linux_amd64: ## build the gorush binary for linux amd64
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/amd64/$(EXECUTABLE)

build_linux_i386: ## build the gorush binary for linux i386
	CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/i386/$(EXECUTABLE)

build_linux_arm64: ## build the gorush binary for linux arm64
	CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm64/$(EXECUTABLE)

build_linux_arm: ## build the gorush binary for linux arm
	CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm/$(EXECUTABLE)

build_linux_lambda: ## build the gorush binary for linux lambda
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags 'lambda' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/lambda/$(EXECUTABLE)

build_darwin_amd64: ## build the gorush binary for darwin amd64
	CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/amd64/$(EXECUTABLE)

build_darwin_i386: ## build the gorush binary for darwin i386
	CGO_ENABLED=0 GOOS=darwin GOARCH=386 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/i386/$(EXECUTABLE)

build_darwin_arm64: ## build the gorush binary for darwin arm64
	CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/arm64/$(EXECUTABLE)

build_darwin_arm: ## build the gorush binary for darwin arm
	CGO_ENABLED=0 GOOS=darwin GOARCH=arm GOARM=7 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/arm/$(EXECUTABLE)

build_darwin_lambda: ## build the gorush binary for darwin lambda
	CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -tags 'lambda' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/lambda/$(EXECUTABLE)

clean: ## Clean the build
	$(GO) clean -modcache -x -i ./...
	find . -name coverage.txt -delete
	find . -name *.tar.gz -delete
	find . -name *.db -delete
	-rm -rf release dist .cover

.PHONY: proto_install
proto_install: ## install the protoc-gen-go and protoc-gen-go-grpc
	$(GO) install google.golang.org/protobuf/cmd/protoc-gen-go@$(PROTOC_GEN_GO)
	$(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$(PROTOC_GEN_GO_GRPC)

generate_proto_js: ## generate the proto file for nodejs
	npm install grpc-tools
	protoc -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"

generate_proto_go: ## generate the proto file for golang
	protoc -I rpc/proto rpc/proto/gorush.proto --go_out=rpc/proto --go-grpc_out=require_unimplemented_servers=false:rpc/proto

generate_proto: generate_proto_go generate_proto_js

.PHONY: air
air: ## install air for hot reload
	@hash air > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
		$(GO) install github.com/cosmtrek/air@latest; \
	fi

.PHONY: dev ## run the air for hot reload
dev: air
	air --build.cmd "make" --build.bin release/gorush

version: ## print the version
	@echo $(VERSION)

.PHONY: lint
lint: ## Run golangci-lint
	@hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
		$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; \
	fi
	golangci-lint run ./...

.PHONY: fmt
fmt: ## Format code using golangci-lint
	@hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
		$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; \
	fi
	golangci-lint fmt ./...


================================================
FILE: README.md
================================================
# gorush

A 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).

[![Run Lint and Testing](https://github.com/appleboy/gorush/actions/workflows/testing.yml/badge.svg)](https://github.com/appleboy/gorush/actions/workflows/testing.yml)
[![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)
[![GoDoc](https://godoc.org/github.com/appleboy/gorush?status.svg)](https://pkg.go.dev/github.com/appleboy/gorush)
[![codecov](https://codecov.io/gh/appleboy/gorush/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/gorush)
[![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/gorush)](https://goreportcard.com/report/github.com/appleboy/gorush)
[![Docker Pulls](https://img.shields.io/docker/pulls/appleboy/gorush.svg)](https://hub.docker.com/r/appleboy/gorush/)
[![Netlify Status](https://api.netlify.com/api/v1/badges/8ab14c9f-44fd-4d9a-8bba-f73f76d253b1/deploy-status)](https://app.netlify.com/sites/gorush/deploys)
[![Financial Contributors on Open Collective](https://opencollective.com/gorush/all/badge.svg?label=financial+contributors)](https://opencollective.com/gorush)

## Quick Start

Get started with gorush in 3 simple steps:

```bash
# 1. Download the latest binary
wget https://github.com/appleboy/gorush/releases/download/v1.18.9/gorush-1.18.9-linux-amd64 -O gorush
chmod +x gorush

# 2. Start the server (default port 8088)
./gorush

# 3. Send your first notification
curl -X POST http://localhost:8088/api/push \
  -H "Content-Type: application/json" \
  -d '{
    "notifications": [{
      "tokens": ["your_device_token"],
      "platform": 2,
      "title": "Hello World",
      "message": "Your first notification!"
    }]
  }'
```

## Contents

- [Quick Start](#quick-start) - Get up and running in 3 steps
- [Support Platform](#support-platform) - iOS, Android, Huawei
- [Features](#features) - What gorush can do
- [Configuration](#configuration) - YAML config and options
  - [Basic Configuration](#basic-configuration)
  - [Advanced Configuration](#advanced-configuration)
- [Installation](#installation) - Binary, package managers, Docker, source
  - [Recommended: Install Script](#recommended-install-script)
  - [Manual Download](#manual-download)
  - [Package Managers](#package-managers)
  - [Build from Source](#build-from-source)
  - [Docker](#docker)
- [Usage](#usage) - CLI commands and REST API examples
  - [Starting the Server](#starting-the-server)
  - [Command Line Notifications](#command-line-notifications)
  - [REST API Usage](#rest-api-usage)
  - [CLI Options Reference](#cli-options-reference)
- [Web API](#web-api) - Complete API reference
  - [Overview](#overview)
  - [Send Notifications](#send-notifications---post-apipush)
  - [Statistics APIs](#statistics-apis)
  - [Request Parameters](#request-parameters)
- [Deployment](#deployment) - Docker, Kubernetes, AWS Lambda, gRPC
  - [Docker Deployment](#docker-deployment)
  - [Kubernetes](#kubernetes)
  - [AWS Lambda](#aws-lambda)
  - [Netlify Functions](#netlify-functions)
  - [gRPC Service](#grpc-service)
- [FAQ](#faq) - Common issues and best practices
  - [Common Issues](#common-issues)
  - [Performance Tips](#performance-tips)
  - [Security Best Practices](#security-best-practices)
- [Stargazers over time](#stargazers-over-time)
- [License](#license)

## Support Platform

📱 Platform codes: `1` = iOS (APNS), `2` = Android (FCM), `3` = Huawei (HMS)

- [APNS](https://developer.apple.com/documentation/usernotifications)
- [FCM](https://firebase.google.com/)
- [HMS](https://developer.huawei.com/consumer/en/hms/)

[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.

```bash
curl -X POST \
     -H "Content-Type: application/json" \
     -d '{
  "notifications": [
    {
      "tokens": [
        "your_device_token"
      ],
      "platform": 2,
      "title": "Test Title",
      "message": "Test Message"
    }
  ]
}' \
  https://gorush.netlify.app/api/push
```

## Features

- Support [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) using [go-fcm](https://github.com/appleboy/go-fcm) library for Android.
- Support [HTTP/2](https://http2.github.io/) Apple Push Notification Service using [apns2](https://github.com/sideshow/apns2) library.
- 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.
- Support [YAML](https://github.com/go-yaml/yaml) configuration.
- Support command line to send single Android or iOS notification.
- Support Web API to send push notification.
- Support [HTTP/2](https://http2.github.io/) or HTTP/1.1 protocol.
- Support notification queue and multiple workers.
- Support `/api/stat/app` show notification success and failure counts.
- Support `/api/config` show your [YAML](https://en.wikipedia.org/wiki/YAML) config.
- 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).
- Support `p8`, `p12` or `pem` format of iOS certificate file.
- Support `/sys/stats` show response time, status code count, etc.
- Support for HTTP, HTTPS or SOCKS5 proxy.
- Support retry send notification if server response is fail.
- Support expose [prometheus](https://prometheus.io/) metrics.
- Support install TLS certificates from [Let's Encrypt](https://letsencrypt.org/) automatically.
- Support send notification through [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) protocol, we use [gRPC](https://grpc.io/) as default framework.
- 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/))
- Support graceful shutdown that workers and queue have been sent to APNs/FCM before shutdown service.
- 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).

**Performance**: Average memory usage ~28MB. Supports high-throughput notification delivery with configurable workers and queue systems.

## Configuration

Gorush uses YAML configuration. Create a `config.yml` file with your settings:

### Basic Configuration

```yaml
core:
  port: "8088" # HTTP server port
  worker_num: 0 # Workers (0 = CPU cores)
  queue_num: 8192 # Queue size
  mode: "release" # or "debug"

# Enable platforms you need
android:
  enabled: true
  key_path: "fcm-key.json" # FCM service account key

ios:
  enabled: true
  key_path: "apns-key.pem" # APNS certificate
  production: true # Use production APNS

huawei:
  enabled: false
  appid: "YOUR_APP_ID"
  appsecret: "YOUR_APP_SECRET"
```

### Advanced Configuration

<details>
<summary>Click to expand full configuration options</summary>

```yaml
core:
  enabled: true
  address: ""
  shutdown_timeout: 30
  port: "8088"
  worker_num: 0
  queue_num: 0
  max_notification: 100
  sync: false
  feedback_hook_url: ""
  feedback_timeout: 10
  feedback_header:
  mode: "release"
  ssl: false
  cert_path: "cert.pem"
  key_path: "key.pem"
  cert_base64: ""
  key_base64: ""
  http_proxy: ""
  pid:
    enabled: false
    path: "gorush.pid"
    override: true
  auto_tls:
    enabled: false
    folder: ".cache"
    host: ""

grpc:
  enabled: false
  port: 9000

api:
  push_uri: "/api/push"
  stat_go_uri: "/api/stat/go"
  stat_app_uri: "/api/stat/app"
  config_uri: "/api/config"
  sys_stat_uri: "/sys/stats"
  metric_uri: "/metrics"
  health_uri: "/healthz"

android:
  enabled: true
  key_path: ""
  credential: ""
  max_retry: 0

huawei:
  enabled: false
  appsecret: "YOUR_APP_SECRET"
  appid: "YOUR_APP_ID"
  max_retry: 0

queue:
  engine: "local"
  nsq:
    addr: 127.0.0.1:4150
    topic: gorush
    channel: gorush
  nats:
    addr: 127.0.0.1:4222
    subj: gorush
    queue: gorush
  redis:
    addr: 127.0.0.1:6379
    group: gorush
    consumer: gorush
    stream_name: gorush
    with_tls: false
    username: ""
    password: ""
    db: 0

ios:
  enabled: false
  key_path: ""
  key_base64: ""
  key_type: "pem"
  password: ""
  production: false
  max_concurrent_pushes: 100
  max_retry: 0
  key_id: ""
  team_id: ""

log:
  format: "string"
  access_log: "stdout"
  access_level: "debug"
  error_log: "stderr"
  error_level: "error"
  hide_token: true
  hide_messages: false

stat:
  engine: "memory"
  redis:
    cluster: false
    addr: "localhost:6379"
    username: ""
    password: ""
    db: 0
  boltdb:
    path: "bolt.db"
    bucket: "gorush"
  buntdb:
    path: "bunt.db"
  leveldb:
    path: "level.db"
  badgerdb:
    path: "badger.db"
```

See the complete [example config file](config/testdata/config.yml).

</details>

## Installation

### Recommended: Install Script

The easiest way to install gorush is using the install script:

```bash
curl -fsSL https://raw.githubusercontent.com/appleboy/gorush/master/install.sh | bash
```

This will automatically:

- Detect your OS and architecture
- Download the latest version
- Install to `~/.gorush/bin`
- Add to your PATH

#### Options

```bash
# Install specific version (replace X.Y.Z with the desired version, e.g., 1.19.2)
VERSION=X.Y.Z curl -fsSL https://raw.githubusercontent.com/appleboy/gorush/master/install.sh | bash

# Custom install directory
INSTALL_DIR=/usr/local/bin curl -fsSL https://raw.githubusercontent.com/appleboy/gorush/master/install.sh | bash

# Skip SSL verification (not recommended)
INSECURE=1 curl -fsSL https://raw.githubusercontent.com/appleboy/gorush/master/install.sh | bash
```

### Manual Download

Download from [releases page](https://github.com/appleboy/gorush/releases):

### Package Managers

#### Homebrew (macOS/Linux)

```bash
brew tap appleboy/tap
brew install gorush
```

#### Go Install

```bash
# Latest stable version
go install github.com/appleboy/gorush@latest

# Development version
go install github.com/appleboy/gorush@master
```

### Build from Source

**Requirements**: [Go 1.25+](https://go.dev/dl/), [Git](http://git-scm.com/)

```bash
git clone https://github.com/appleboy/gorush.git
cd gorush
make build
# Binary will be in the root directory
```

### Docker

```bash
# Run directly
docker run --rm -p 8088:8088 appleboy/gorush

# With custom config
docker run --rm -p 8088:8088 -v $(pwd)/config.yml:/home/gorush/config.yml appleboy/gorush
```

## Usage

### Starting the Server

```bash
# Use default config (port 8088)
./gorush

# Use custom config file
./gorush -c config.yml

# Set specific options
./gorush -p 9000 -c config.yml
```

### Command Line Notifications

#### Android (FCM) Command Line

**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.

```bash
# Android: Single notification
gorush -android -m "Hello Android!" --fcm-key "path/to/fcm-key.json" -t "device_token"

# Android: Using environment variable (recommended)
export GOOGLE_APPLICATION_CREDENTIALS="path/to/fcm-key.json"
gorush -android -m "Hello Android!" -t "device_token"

# Android: Topic message
gorush --android --topic "news" -m "Breaking News!" --fcm-key "path/to/fcm-key.json"
```

#### iOS (APNS) Command Line

```bash
# iOS: Development environment
gorush -ios -m "Hello iOS!" -i "cert.pem" -t "device_token" --topic "com.example.app"

# iOS: Production environment
gorush -ios -m "Hello iOS!" -i "cert.pem" -t "device_token" --topic "com.example.app" -production

# iOS: With password-protected certificate
gorush -ios -m "Hello iOS!" -i "cert.p12" -P "cert_password" -t "device_token"
```

#### Huawei (HMS) Command Line

```bash
# Huawei: Single notification
gorush -huawei -title "Hello" -m "Hello Huawei!" -hk "APP_SECRET" -hid "APP_ID" -t "device_token"

# Huawei: Topic message
gorush --huawei --topic "updates" -title "Update" -m "New version available" -hk "APP_SECRET" -hid "APP_ID"
```

### REST API Usage

#### Health Check

```bash
curl http://localhost:8088/healthz
```

#### Send Notifications

```bash
curl -X POST http://localhost:8088/api/push \
  -H "Content-Type: application/json" \
  -d '{
    "notifications": [{
      "tokens": ["device_token_1", "device_token_2"],
      "platform": 2,
      "title": "Hello World",
      "message": "This is a test notification"
    }]
  }'
```

#### Get Statistics

```bash
# Application stats
curl http://localhost:8088/api/stat/app

# Go runtime stats
curl http://localhost:8088/api/stat/go

# System stats
curl http://localhost:8088/sys/stats

# Prometheus metrics
curl http://localhost:8088/metrics
```

### CLI Options Reference

<details>
<summary>Click to expand all CLI options</summary>

```bash
Server Options:
    -A, --address <address>          Address to bind (default: any)
    -p, --port <port>                Use port for clients (default: 8088)
    -c, --config <file>              Configuration file path
    -m, --message <message>          Notification message
    -t, --token <token>              Notification token
    -e, --engine <engine>            Storage engine (memory, redis ...)
    --title <title>                  Notification title
    --proxy <proxy>                  Proxy URL
    --pid <pid path>                 Process identifier path
    --redis-addr <redis addr>        Redis addr (default: localhost:6379)
    --ping                           healthy check command for container

iOS Options:
    -i, --key <file>                 certificate key file path
    -P, --password <password>        certificate key password
    --ios                            enabled iOS (default: false)
    --production                     iOS production mode (default: false)

Android Options:
    --fcm-key <fcm_key_path>         FCM Key Path
    --android                        enabled android (default: false)

Huawei Options:
    -hk, --hmskey <hms_key>          HMS App Secret
    -hid, --hmsid <hms_id>           HMS App ID
    --huawei                         enabled huawei (default: false)

Common Options:
    --topic <topic>                  iOS, Android or Huawei topic message
    -h, --help                       Show this message
    -V, --version                    Show version
```

</details>

## Web API

### Overview

Gorush provides RESTful APIs for sending notifications and monitoring system status:

| Endpoint        | Method | Description                |
| --------------- | ------ | -------------------------- |
| `/api/push`     | POST   | Send push notifications    |
| `/api/stat/app` | GET    | Application statistics     |
| `/api/stat/go`  | GET    | Go runtime statistics      |
| `/sys/stats`    | GET    | System performance metrics |
| `/metrics`      | GET    | Prometheus metrics         |
| `/healthz`      | GET    | Health check               |
| `/api/config`   | GET    | Current configuration      |

### Send Notifications - `POST /api/push`

#### Basic Examples

##### iOS (APNS) API

```json
{
  "notifications": [
    {
      "tokens": ["ios_device_token"],
      "platform": 1,
      "title": "Hello iOS",
      "message": "Hello World iOS!"
    }
  ]
}
```

##### Android (FCM) API

```json
{
  "notifications": [
    {
      "tokens": ["android_device_token"],
      "platform": 2,
      "title": "Hello Android",
      "message": "Hello World Android!"
    }
  ]
}
```

##### Huawei (HMS) API

```json
{
  "notifications": [
    {
      "tokens": ["huawei_device_token"],
      "platform": 3,
      "title": "Hello Huawei",
      "message": "Hello World Huawei!"
    }
  ]
}
```

#### Advanced Examples

##### iOS with Custom Sound

```json
{
  "notifications": [
    {
      "tokens": ["ios_device_token"],
      "platform": 1,
      "title": "Important Alert",
      "message": "Critical notification",
      "apns": {
        "payload": {
          "aps": {
            "sound": {
              "name": "custom.wav",
              "critical": 1,
              "volume": 0.8
            }
          }
        }
      }
    }
  ]
}
```

##### Multiple Platforms

```json
{
  "notifications": [
    {
      "tokens": ["ios_token"],
      "platform": 1,
      "message": "Hello iOS!"
    },
    {
      "tokens": ["android_token"],
      "platform": 2,
      "message": "Hello Android!"
    }
  ]
}
```

### Statistics APIs

#### Application Stats - `GET /api/stat/app`

```json
{
  "version": "v1.18.9",
  "busy_workers": 0,
  "success_tasks": 150,
  "failure_tasks": 5,
  "submitted_tasks": 155,
  "total_count": 155,
  "ios": {
    "push_success": 80,
    "push_error": 2
  },
  "android": {
    "push_success": 65,
    "push_error": 3
  },
  "huawei": {
    "push_success": 5,
    "push_error": 0
  }
}
```

#### System Performance - `GET /sys/stats`

```json
{
  "pid": 12345,
  "uptime": "2h30m15s",
  "total_response_time": "45.2ms",
  "average_response_time": "1.2ms",
  "total_status_code_count": {
    "200": 1450,
    "400": 12,
    "500": 3
  }
}
```

### Request Parameters

<details>
<summary>Complete API request parameters</summary>

#### Request body

The Request body must have a notifications array. The following is a parameter table for each notification.

| name                    | type         | description                                                                                       | required | note                                                          |
| ----------------------- | ------------ | ------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------- |
| notif_id                | string       | A unique string that identifies the notification for async feedback                               | -        |                                                               |
| tokens                  | string array | device tokens                                                                                     | o        |                                                               |
| platform                | int          | platform(iOS,Android)                                                                             | o        | 1=iOS, 2=Android (Firebase), 3=Huawei (HMS)                   |
| message                 | string       | message for notification                                                                          | -        |                                                               |
| title                   | string       | notification title                                                                                | -        |                                                               |
| priority                | string       | Sets the priority of the message.                                                                 | -        | `normal` or `high`                                            |
| content_available       | bool         | data messages wake the app by default.                                                            | -        |                                                               |
| sound                   | interface{}  | sound type                                                                                        | -        |                                                               |
| data                    | string array | extensible partition                                                                              | -        | only Android and IOS                                          |
| huawei_data             | string       | JSON object as string to extensible partition partition                                           | -        | only Huawei. See the [detail](#huawei-notification)           |
| retry                   | int          | retry send notification if fail response from server. Value must be small than `max_retry` field. | -        |                                                               |
| topic                   | string       | send messages to topics                                                                           |          |                                                               |
| image                   | string       | image url to show in notification                                                                 | -        | only Android and Huawei                                       |
| to                      | string       | The value must be a registration token, notification key, or topic.                               | -        | only Android                                                  |
| collapse_key            | string       | a key for collapsing notifications                                                                | -        | only Android                                                  |
| huawei_collapse_key     | int          | a key integer for collapsing notifications                                                        | -        | only Huawei See the [detail](#huawei-notification)            |
| delay_while_idle        | bool         | a flag for device idling                                                                          | -        | only Android                                                  |
| time_to_live            | uint         | expiration of message kept on FCM storage                                                         | -        | only Android                                                  |
| huawei_ttl              | string       | expiration of message kept on HMS storage                                                         | -        | only Huawei See the [detail](#huawei-notification)            |
| restricted_package_name | string       | the package name of the application                                                               | -        | only Android                                                  |
| dry_run                 | bool         | allows developers to test a request without actually sending a message                            | -        | only Android                                                  |
| notification            | string array | payload of a FCM message                                                                          | -        | only Android. See the [detail](#android-notification-payload) |
| huawei_notification     | string array | payload of a HMS message                                                                          | -        | only Huawei. See the [detail](#huawei-notification)           |
| app_id                  | string       | hms app id                                                                                        | -        | only Huawei. See the [detail](#huawei-notification)           |
| bi_tag                  | string       | Tag of a message in a batch delivery task                                                         | -        | only Huawei. See the [detail](#huawei-notification)           |
| fast_app_target         | int          | State of a mini program when a quick app sends a data message.                                    | -        | only Huawei. See the [detail](#huawei-notification)           |
| expiration              | int          | expiration for notification                                                                       | -        | only iOS                                                      |
| apns_id                 | string       | A canonical UUID that identifies the notification                                                 | -        | only iOS                                                      |
| collapse_id             | string       | An identifier you use to coalesce multiple notifications into a single notification for the user  | -        | only iOS                                                      |
| push_type               | string       | The type of the notification. The value of this header is alert or background.                    | -        | only iOS                                                      |
| badge                   | int          | badge count                                                                                       | -        | only iOS                                                      |
| category                | string       | the UIMutableUserNotificationCategory object                                                      | -        | only iOS                                                      |
| alert                   | string array | payload of a iOS message                                                                          | -        | only iOS. See the [detail](#ios-alert-payload)                |
| mutable_content         | bool         | enable Notification Service app extension.                                                        | -        | only iOS(10.0+).                                              |
| name                    | string       | sets the name value on the aps sound dictionary.                                                  | -        | only iOS                                                      |
| volume                  | float32      | sets the volume value on the aps sound dictionary.                                                | -        | only iOS                                                      |
| interruption_level      | string       | defines the interruption level for the push notification.                                         | -        | only iOS(15.0+)                                               |
| content-state           | string array | dynamic and custom content for live-activity notification.                                        | -        | only iOS(16.1+)                                               |
| timestamp               | int          | the UNIX time when sending the remote notification that updates or ends a Live Activity           | -        | only iOS(16.1+)                                               |
| event                   | string       | describes whether you update or end an ongoing Live Activity                                      | -        | only iOS(16.1+)                                               |
| stale-date              | int          | the date which a Live Activity becomes stale, or out of date                                      | -        | only iOS(16.1+)                                               |
| dismissal-date          | int          | the UNIX time -timestamp- which a Live Activity will end and will be removed                      | -        | only iOS(16.1+)                                               |

### iOS alert payload

| name           | type             | description                                                                                      | required | note |
| -------------- | ---------------- | ------------------------------------------------------------------------------------------------ | -------- | ---- |
| title          | string           | Apple Watch & Safari display this string as part of the notification interface.                  | -        |      |
| body           | string           | The text of the alert message.                                                                   | -        |      |
| subtitle       | string           | Apple Watch & Safari display this string as part of the notification interface.                  | -        |      |
| action         | string           | The label of the action button. This one is required for Safari Push Notifications.              | -        |      |
| action-loc-key | string           | If a string is specified, the system displays an alert that includes the Close and View buttons. | -        |      |
| launch-image   | string           | The filename of an image file in the app bundle, with or without the filename extension.         | -        |      |
| loc-args       | array of strings | Variable string values to appear in place of the format specifiers in loc-key.                   | -        |      |
| loc-key        | string           | A key to an alert-message string in a Localizable.strings file for the current localization.     | -        |      |
| title-loc-args | array of strings | Variable string values to appear in place of the format specifiers in title-loc-key.             | -        |      |
| title-loc-key  | string           | The key to a title string in the Localizable.strings file for the current localization.          | -        |      |

See more detail about [APNs Remote Notification Payload](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html).

### iOS sound payload

| name     | type    | description                                          | required | note |
| -------- | ------- | ---------------------------------------------------- | -------- | ---- |
| name     | string  | sets the name value on the aps sound dictionary.     | -        |      |
| volume   | float32 | sets the volume value on the aps sound dictionary.   | -        |      |
| critical | int     | sets the critical value on the aps sound dictionary. | -        |      |

request format:

```json
{
  "sound": {
    "critical": 1,
    "name": "default",
    "volume": 2.0
  }
}
```

### Android notification payload

| name           | type   | description                                                                                               | required | note |
| -------------- | ------ | --------------------------------------------------------------------------------------------------------- | -------- | ---- |
| icon           | string | Indicates notification icon.                                                                              | -        |      |
| tag            | string | Indicates whether each notification message results in a new entry on the notification center on Android. | -        |      |
| color          | string | Indicates color of the icon, expressed in #rrggbb format                                                  | -        |      |
| click_action   | string | The action associated with a user click on the notification.                                              | -        |      |
| body_loc_key   | string | Indicates the key to the body string for localization.                                                    | -        |      |
| body_loc_args  | string | Indicates the string value to replace format specifiers in body string for localization.                  | -        |      |
| title_loc_key  | string | Indicates the key to the title string for localization.                                                   | -        |      |
| title_loc_args | string | Indicates the string value to replace format specifiers in title string for localization.                 | -        |      |

See more detail about [Firebase Cloud Messaging HTTP Protocol reference](https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream).

### Huawei notification

1. app_id: app id from huawei developer console
2. bi_tag:
3. fast_app_target:
4. huawei_data: mapped to data
5. huawei_notification: mapped to notification
6. huawei_ttl: mapped to ttl
7. huawei_collapse_key: mapped to collapse_key

See more detail about [Huawei Mobulse Services Push API reference](https://developer.huawei.com/consumer/en/doc/development/HMS-References/push-sendapi).

### iOS Example

Send normal notification.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 1,
      "message": "Hello World iOS!"
    }
  ]
}
```

The 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.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 1,
      "badge": 5,
      "alert": {
        "title": "Game Request",
        "body": "Bob wants to play poker",
        "action-loc-key": "PLAY"
      }
    }
  ]
}
```

The following payload specifies that the device should display an alert message, plays a sound, and badges the app’s icon.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 1,
      "message": "You got your emails.",
      "badge": 9,
      "sound": {
        "critical": 1,
        "name": "default",
        "volume": 1.0
      }
    }
  ]
}
```

Add other fields which user defined via `data` field.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 1,
      "message": "Hello World iOS!",
      "data": {
        "key1": "welcome",
        "key2": 2
      }
    }
  ]
}
```

Support send notification from different environment. See the detail of [issue](https://github.com/appleboy/gorush/issues/246).

```diff
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 1,
+     "production": true,
      "message": "Hello World iOS Production!"
    },
    {
      "tokens": ["token_a", "token_b"],
      "platform": 1,
+     "development": true,
      "message": "Hello World iOS Sandbox!"
    }
  ]
}
```

### Android Example

Send normal notification.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 2,
      "message": "Hello World Android!",
      "title": "You got message"
    }
  ]
}
```

Label associated with the message's analytics data.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 2,
      "message": "Hello World Android!",
      "title": "You got message",
      "fcm_options": {
        "analytics_label": "example"
      }
    }
  ]
}
```

Add `notification` payload.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 2,
      "message": "Hello World Android!",
      "title": "You got message",
      "notification": {
        "icon": "myicon",
        "color": "#112244"
      }
    }
  ]
}
```

Add other fields which user defined via `data` field.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 2,
      "message": "Hello World Android!",
      "title": "You got message",
      "data": {
        "Nick": "Mario",
        "body": "great match!",
        "Room": "PortugalVSDenmark"
      }
    }
  ]
}
```

Send messages to topic

```json
{
  "notifications": [
    {
      "topic": "highScores",
      "platform": 2,
      "message": "This is a Firebase Cloud Messaging Topic Message"
    }
  ]
}
```

### Huawei Example

Send normal notification.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 3,
      "message": "Hello World Huawei!",
      "title": "You got message"
    }
  ]
}
```

Add `notification` payload.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 3,
      "message": "Hello World Huawei!",
      "title": "You got message",
      "huawei_notification": {
        "icon": "myicon",
        "color": "#112244"
      }
    }
  ]
}
```

Add other fields which user defined via `huawei_data` field.

```json
{
  "notifications": [
    {
      "tokens": ["token_a", "token_b"],
      "platform": 3,
      "huawei_data": "{'title' : 'Mario','message' : 'great match!', 'Room' : 'PortugalVSDenmark'}"
    }
  ]
}
```

Send messages to topics

```json
{
  "notifications": [
    {
      "topic": "foo-bar",
      "platform": 3,
      "message": "This is a Huawei Mobile Services Topic Message",
      "title": "You got message"
    }
  ]
}
```

### Response body

Error response message table:

| status code | message                                    |
| ----------- | ------------------------------------------ |
| 400         | Missing `notifications` field.             |
| 400         | Notifications field is empty.              |
| 400         | Number of notifications(50) over limit(10) |

Success response:

```json
{
  "counts": 60,
  "logs": [],
  "success": "ok"
}
```

If 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.

```diff
core:
  port: "8088" # ignore this port number if auto_tls is enabled (listen 443).
  worker_num: 0 # default worker number is runtime.NumCPU()
  queue_num: 0 # default queue number is 8192
  max_notification: 100
  sync: false
- feedback_hook_url: ""
+ feedback_hook_url: "https://example.com/api/hook"
+ feedback_header:
+   - x-gorush-token:4e989115e09680f44a645519fed6a976
```

You 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.

```diff
core:
  port: "8088" # ignore this port number if auto_tls is enabled (listen 443).
  worker_num: 0 # default worker number is runtime.NumCPU()
  queue_num: 0 # default queue number is 8192
  max_notification: 100
- sync: false
+ sync: true
```

See the following error format.

```json
{
  "counts": 60,
  "logs": [
    {
      "type": "failed-push",
      "platform": "android",
      "token": "*******",
      "message": "Hello World Android!",
      "error": "InvalidRegistration"
    },
    {
      "type": "failed-push",
      "platform": "ios",
      "token": "*****",
      "message": "Hello World iOS1111!",
      "error": "Post https://api.push.apple.com/3/device/bbbbb: remote error: tls: revoked certificate"
    },
    {
      "type": "failed-push",
      "platform": "ios",
      "token": "*******",
      "message": "Hello World iOS222!",
      "error": "Post https://api.push.apple.com/3/device/token_b: remote error: tls: revoked certificate"
    }
  ],
  "success": "ok"
}
```

## Deployment

### Docker Deployment

#### Docker Quick Start

```bash
# Run with default config
docker run --rm -p 8088:8088 appleboy/gorush

# Run with custom config
docker run --rm -p 8088:8088 \
  -v $(pwd)/config.yml:/home/gorush/config.yml \
  appleboy/gorush

# Run in background
docker run -d --name gorush -p 8088:8088 appleboy/gorush
```

#### Docker Compose

```yaml
version: "3"
services:
  gorush:
    image: appleboy/gorush
    ports:
      - "8088:8088"
    volumes:
      - ./config.yml:/home/gorush/config.yml
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"
```

### Kubernetes

#### Quick Deploy

```bash
# Create namespace and config
kubectl create -f k8s/gorush-namespace.yaml
kubectl create -f k8s/gorush-configmap.yaml

# Deploy Redis (optional, for queue/stats)
kubectl create -f k8s/gorush-redis-deployment.yaml
kubectl create -f k8s/gorush-redis-service.yaml

# Deploy Gorush
kubectl create -f k8s/gorush-deployment.yaml
kubectl create -f k8s/gorush-service.yaml
```

#### AWS Load Balancer

For AWS ELB:

```bash
kubectl create -f k8s/gorush-service.yaml
```

For AWS ALB, modify service type:

```yaml
# k8s/gorush-service.yaml
spec:
  type: NodePort # Change from LoadBalancer
```

Then deploy ingress:

```bash
kubectl create -f k8s/gorush-aws-alb-ingress.yaml
```

#### Cleanup

```bash
kubectl delete -f k8s/
```

### AWS Lambda

#### Build and Deploy

```bash
# Build Lambda binary
git clone https://github.com/appleboy/gorush.git
cd gorush
make build_linux_lambda

# Create deployment package
zip deployment.zip release/linux/lambda/gorush

# Deploy with AWS CLI
aws lambda update-function-code \
  --function-name gorush \
  --zip-file fileb://deployment.zip
```

#### Automated Deployment

Using [drone-lambda](https://github.com/appleboy/drone-lambda):

```bash
AWS_ACCESS_KEY_ID=your_key \
AWS_SECRET_ACCESS_KEY=your_secret \
drone-lambda --region us-west-2 \
  --function-name gorush \
  --source release/linux/lambda/gorush
```

### Netlify Functions

Alternative serverless deployment without AWS:

```toml
# netlify.toml
[build]
command = "make build_linux_lambda"
functions = "release/linux/lambda"

[build.environment]
GO_VERSION = "1.24"

[[redirects]]
from = "/*"
status = 200
to = "/.netlify/functions/gorush/:splat"
```

### gRPC Service

Enable gRPC server for high-performance applications:

```yaml
# config.yml
grpc:
  enabled: true
  port: 9000
```

Or via environment:

```bash
GORUSH_GRPC_ENABLED=true GORUSH_GRPC_PORT=9000 gorush
```

#### gRPC Client Example (Go)

```go
package main

import (
    "context"
    "log"
    "github.com/appleboy/gorush/rpc/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    conn, err := grpc.NewClient("localhost:9000", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    client := proto.NewGorushClient(conn)
    resp, err := client.Send(context.Background(), &proto.NotificationRequest{
        Platform: 2,
        Tokens:   []string{"device_token"},
        Message:  "Hello gRPC!",
        Title:    "Test Notification",
    })

    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Success: %v, Count: %d", resp.Success, resp.Counts)
}
```

## FAQ

### Common Issues

#### Q: How do I get FCM credentials?

A: Go to [Firebase Console](https://console.firebase.google.com/) → Project Settings → Service Accounts → Generate New Private Key. Download the JSON file.

#### Q: iOS notifications not working in production?

A: Make sure you:

1. Use production APNS certificates (`production: true`)
2. Set correct bundle ID in certificate
3. Test with production app build

#### Q: Getting "certificate verify failed" error?

A: This usually means:

- Wrong certificate format (use `.pem` or `.p12`)
- Certificate expired
- Wrong environment (dev vs production)

#### Q: How to handle large notification volumes?

A: Configure workers and queue settings:

```yaml
core:
  worker_num: 8 # Increase workers
  queue_num: 16384 # Increase queue size
queue:
  engine: "redis" # Use external queue
```

#### Q: Can I send to multiple platforms at once?

A: Yes, include multiple notification objects in the request:

```json
{
  "notifications": [
    { "platform": 1, "tokens": ["ios_token"], "message": "iOS" },
    { "platform": 2, "tokens": ["android_token"], "message": "Android" }
  ]
}
```

#### Q: How to monitor notification failures?

A: Enable sync mode or feedback webhook:

```yaml
core:
  sync: true # Get immediate response
  feedback_hook_url: "https://your-api" # Async webhook
```

#### Q: What's the difference between platforms?

A: Platform codes: `1` = iOS (APNS), `2` = Android (FCM), `3` = Huawei (HMS)

### Performance Tips

- Use Redis for queue and stats storage in production
- Enable gRPC for better performance
- Set appropriate worker numbers based on CPU cores
- Use connection pooling for high-volume scenarios

### Security Best Practices

- Store credentials as files, not in config
- Use environment variables for sensitive data
- Enable SSL/TLS in production
- Rotate certificates before expiration
- Monitor failed notifications for security issues

## Stargazers over time

[![Stargazers over time](https://starchart.cc/appleboy/gorush.svg)](https://starchart.cc/appleboy/gorush)

## License

Copyright 2026 Bo-Yi Wu [@appleboy](https://github.com/appleboy).

Licensed under the MIT License.


================================================
FILE: app/config.go
================================================
package app

import (
	"fmt"

	"github.com/appleboy/gorush/config"
)

// MergeConfig merges CLI options into the configuration.
// CLI options take precedence over config file values.
func MergeConfig(cfg *config.ConfYaml, opts *Options) error {
	// iOS options
	if opts.Conf.Ios.KeyPath != "" {
		cfg.Ios.KeyPath = opts.Conf.Ios.KeyPath
	}
	if opts.Conf.Ios.KeyID != "" {
		cfg.Ios.KeyID = opts.Conf.Ios.KeyID
	}
	if opts.Conf.Ios.TeamID != "" {
		cfg.Ios.TeamID = opts.Conf.Ios.TeamID
	}
	if opts.Conf.Ios.Password != "" {
		cfg.Ios.Password = opts.Conf.Ios.Password
	}
	if opts.Conf.Ios.Production {
		cfg.Ios.Production = opts.Conf.Ios.Production
	}

	// Android options
	if opts.Conf.Android.KeyPath != "" {
		cfg.Android.KeyPath = opts.Conf.Android.KeyPath
	}

	// Huawei options
	if opts.Conf.Huawei.AppSecret != "" {
		cfg.Huawei.AppSecret = opts.Conf.Huawei.AppSecret
	}
	if opts.Conf.Huawei.AppID != "" {
		cfg.Huawei.AppID = opts.Conf.Huawei.AppID
	}

	// Storage options
	if opts.Conf.Stat.Engine != "" {
		cfg.Stat.Engine = opts.Conf.Stat.Engine
	}
	if opts.Conf.Stat.Redis.Addr != "" {
		cfg.Stat.Redis.Addr = opts.Conf.Stat.Redis.Addr
	}

	// Server options with validation
	if opts.Conf.Core.Port != "" {
		if err := config.ValidatePort(opts.Conf.Core.Port); err != nil {
			return fmt.Errorf("invalid port from command line: %w", err)
		}
		cfg.Core.Port = opts.Conf.Core.Port
	}
	if opts.Conf.Core.Address != "" {
		if err := config.ValidateAddress(opts.Conf.Core.Address); err != nil {
			return fmt.Errorf("invalid address from command line: %w", err)
		}
		cfg.Core.Address = opts.Conf.Core.Address
	}
	if opts.Conf.Core.HTTPProxy != "" {
		cfg.Core.HTTPProxy = opts.Conf.Core.HTTPProxy
	}

	// PID options
	if opts.Conf.Core.PID.Path != "" {
		if err := config.ValidatePIDPath(opts.Conf.Core.PID.Path); err != nil {
			return fmt.Errorf("invalid PID path from command line: %w", err)
		}
		cfg.Core.PID.Path = opts.Conf.Core.PID.Path
		cfg.Core.PID.Enabled = true
		cfg.Core.PID.Override = true
	}

	return nil
}

// ValidateAndMerge loads config, merges CLI options, and validates.
func ValidateAndMerge(opts *Options) (*config.ConfYaml, error) {
	cfg, err := config.LoadConf(opts.ConfigFile)
	if err != nil {
		return nil, fmt.Errorf("load yaml config file error: %w", err)
	}

	if err := MergeConfig(cfg, opts); err != nil {
		return nil, err
	}

	if err := config.ValidateConfig(cfg); err != nil {
		return nil, fmt.Errorf("configuration validation failed: %w", err)
	}

	return cfg, nil
}


================================================
FILE: app/config_test.go
================================================
package app

import (
	"testing"

	"github.com/appleboy/gorush/config"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestMergeConfig_IOSOptions(t *testing.T) {
	cfg, _ := config.LoadConf("")
	opts := NewOptions()
	opts.Conf.Ios.KeyPath = "/path/to/key"
	opts.Conf.Ios.KeyID = "key-id-123"
	opts.Conf.Ios.TeamID = "team-id-456"
	opts.Conf.Ios.Password = "secret"
	opts.Conf.Ios.Production = true

	err := MergeConfig(cfg, opts)
	require.NoError(t, err)

	assert.Equal(t, "/path/to/key", cfg.Ios.KeyPath)
	assert.Equal(t, "key-id-123", cfg.Ios.KeyID)
	assert.Equal(t, "team-id-456", cfg.Ios.TeamID)
	assert.Equal(t, "secret", cfg.Ios.Password)
	assert.True(t, cfg.Ios.Production)
}

func TestMergeConfig_AndroidOptions(t *testing.T) {
	cfg, _ := config.LoadConf("")
	opts := NewOptions()
	opts.Conf.Android.KeyPath = "/path/to/fcm/key.json"

	err := MergeConfig(cfg, opts)
	require.NoError(t, err)

	assert.Equal(t, "/path/to/fcm/key.json", cfg.Android.KeyPath)
}

func TestMergeConfig_HuaweiOptions(t *testing.T) {
	cfg, _ := config.LoadConf("")
	opts := NewOptions()
	opts.Conf.Huawei.AppSecret = "hms-secret"
	opts.Conf.Huawei.AppID = "hms-id"

	err := MergeConfig(cfg, opts)
	require.NoError(t, err)

	assert.Equal(t, "hms-secret", cfg.Huawei.AppSecret)
	assert.Equal(t, "hms-id", cfg.Huawei.AppID)
}

func TestMergeConfig_StorageOptions(t *testing.T) {
	cfg, _ := config.LoadConf("")
	opts := NewOptions()
	opts.Conf.Stat.Engine = "redis"
	opts.Conf.Stat.Redis.Addr = "localhost:6379"

	err := MergeConfig(cfg, opts)
	require.NoError(t, err)

	assert.Equal(t, "redis", cfg.Stat.Engine)
	assert.Equal(t, "localhost:6379", cfg.Stat.Redis.Addr)
}

func TestMergeConfig_ServerOptions(t *testing.T) {
	cfg, _ := config.LoadConf("")
	opts := NewOptions()
	opts.Conf.Core.Port = "9000"
	opts.Conf.Core.Address = "127.0.0.1"
	opts.Conf.Core.HTTPProxy = "http://proxy:8080"

	err := MergeConfig(cfg, opts)
	require.NoError(t, err)

	assert.Equal(t, "9000", cfg.Core.Port)
	assert.Equal(t, "127.0.0.1", cfg.Core.Address)
	assert.Equal(t, "http://proxy:8080", cfg.Core.HTTPProxy)
}

func TestMergeConfig_InvalidPort(t *testing.T) {
	cfg, _ := config.LoadConf("")
	opts := NewOptions()
	opts.Conf.Core.Port = "invalid"

	err := MergeConfig(cfg, opts)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "invalid port")
}

func TestMergeConfig_NoOverrideEmpty(t *testing.T) {
	cfg, _ := config.LoadConf("")
	originalPort := cfg.Core.Port

	opts := NewOptions()
	// Empty values should not override

	err := MergeConfig(cfg, opts)
	require.NoError(t, err)

	assert.Equal(t, originalPort, cfg.Core.Port)
}

func TestValidateAndMerge(t *testing.T) {
	opts := NewOptions()
	// Use empty config file to get defaults

	cfg, err := ValidateAndMerge(opts)
	require.NoError(t, err)
	assert.NotNil(t, cfg)
}


================================================
FILE: app/options.go
================================================
package app

import (
	"flag"

	"github.com/appleboy/gorush/config"
)

// Options holds all CLI flag values.
type Options struct {
	ShowVersion bool
	Ping        bool
	ConfigFile  string

	// Notification options (for CLI mode)
	Token   string
	Message string
	Title   string
	Topic   string

	// Config overrides
	Conf config.ConfYaml
}

// NewOptions creates a new Options instance with default values.
func NewOptions() *Options {
	return &Options{}
}

// BindFlags binds CLI flags to the Options struct.
// Call this before flag.Parse().
func (o *Options) BindFlags() {
	// Version flags
	flag.BoolVar(&o.ShowVersion, "version", false, "Print version information.")
	flag.BoolVar(&o.ShowVersion, "V", false, "Print version information.")

	// Config file
	flag.StringVar(&o.ConfigFile, "c", "", "Configuration file path.")
	flag.StringVar(&o.ConfigFile, "config", "", "Configuration file path.")

	// PID file
	flag.StringVar(&o.Conf.Core.PID.Path, "pid", "", "PID file path.")

	// iOS options
	flag.StringVar(&o.Conf.Ios.KeyPath, "i", "", "iOS certificate key file path")
	flag.StringVar(&o.Conf.Ios.KeyPath, "key", "", "iOS certificate key file path")
	flag.StringVar(&o.Conf.Ios.KeyID, "key-id", "", "iOS Key ID for P8 token")
	flag.StringVar(&o.Conf.Ios.TeamID, "team-id", "", "iOS Team ID for P8 token")
	flag.StringVar(&o.Conf.Ios.Password, "P", "", "iOS certificate password for gorush")
	flag.StringVar(&o.Conf.Ios.Password, "password", "", "iOS certificate password for gorush")
	flag.BoolVar(&o.Conf.Ios.Enabled, "ios", false, "send ios notification")
	flag.BoolVar(&o.Conf.Ios.Production, "production", false, "production mode in iOS")

	// Android options
	flag.StringVar(&o.Conf.Android.KeyPath, "fcm-key", "", "FCM key path configuration for gorush")
	flag.BoolVar(&o.Conf.Android.Enabled, "android", false, "send android notification")

	// Huawei options
	flag.StringVar(&o.Conf.Huawei.AppSecret, "hk", "", "Huawei api key configuration for gorush")
	flag.StringVar(
		&o.Conf.Huawei.AppSecret,
		"hmskey",
		"",
		"Huawei api key configuration for gorush",
	)
	flag.StringVar(&o.Conf.Huawei.AppID, "hid", "", "HMS app id configuration for gorush")
	flag.StringVar(&o.Conf.Huawei.AppID, "hmsid", "", "HMS app id configuration for gorush")
	flag.BoolVar(&o.Conf.Huawei.Enabled, "huawei", false, "send huawei notification")

	// Server options
	flag.StringVar(&o.Conf.Core.Address, "A", "", "address to bind")
	flag.StringVar(&o.Conf.Core.Address, "address", "", "address to bind")
	flag.StringVar(&o.Conf.Core.Port, "p", "", "port number for gorush")
	flag.StringVar(&o.Conf.Core.Port, "port", "", "port number for gorush")
	flag.StringVar(&o.Conf.Core.HTTPProxy, "proxy", "", "http proxy url")

	// Storage options
	flag.StringVar(&o.Conf.Stat.Engine, "e", "", "store engine")
	flag.StringVar(&o.Conf.Stat.Engine, "engine", "", "store engine")
	flag.StringVar(&o.Conf.Stat.Redis.Addr, "redis-addr", "", "redis addr")

	// Notification options (CLI mode)
	flag.StringVar(&o.Token, "t", "", "token string")
	flag.StringVar(&o.Token, "token", "", "token string")
	flag.StringVar(&o.Message, "m", "", "notification message")
	flag.StringVar(&o.Message, "message", "", "notification message")
	flag.StringVar(&o.Title, "title", "", "notification title")
	flag.StringVar(&o.Topic, "topic", "", "apns topic in iOS")

	// Health check
	flag.BoolVar(&o.Ping, "ping", false, "ping server")
}

// CLISendOptions returns CLI send options for notification sending.
func (o *Options) CLISendOptions() CLISendOptions {
	return CLISendOptions{
		Token:   o.Token,
		Message: o.Message,
		Title:   o.Title,
		Topic:   o.Topic,
	}
}

// IsCLIMode returns true if running in CLI notification mode.
func (o *Options) IsCLIMode() bool {
	return o.Conf.Android.Enabled || o.Conf.Ios.Enabled || o.Conf.Huawei.Enabled
}


================================================
FILE: app/options_test.go
================================================
package app

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNewOptions(t *testing.T) {
	opts := NewOptions()
	assert.NotNil(t, opts)
	assert.False(t, opts.ShowVersion)
	assert.False(t, opts.Ping)
	assert.Empty(t, opts.ConfigFile)
	assert.Empty(t, opts.Token)
	assert.Empty(t, opts.Message)
}

func TestOptions_CLISendOptions(t *testing.T) {
	opts := &Options{
		Token:   "test-token",
		Message: "test-message",
		Title:   "test-title",
		Topic:   "test-topic",
	}

	sendOpts := opts.CLISendOptions()
	assert.Equal(t, "test-token", sendOpts.Token)
	assert.Equal(t, "test-message", sendOpts.Message)
	assert.Equal(t, "test-title", sendOpts.Title)
	assert.Equal(t, "test-topic", sendOpts.Topic)
}

func TestOptions_IsCLIMode(t *testing.T) {
	tests := []struct {
		name     string
		opts     *Options
		expected bool
	}{
		{
			name:     "no platform enabled",
			opts:     &Options{},
			expected: false,
		},
		{
			name: "android enabled",
			opts: func() *Options {
				o := NewOptions()
				o.Conf.Android.Enabled = true
				return o
			}(),
			expected: true,
		},
		{
			name: "ios enabled",
			opts: func() *Options {
				o := NewOptions()
				o.Conf.Ios.Enabled = true
				return o
			}(),
			expected: true,
		},
		{
			name: "huawei enabled",
			opts: func() *Options {
				o := NewOptions()
				o.Conf.Huawei.Enabled = true
				return o
			}(),
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, tt.opts.IsCLIMode())
		})
	}
}


================================================
FILE: app/sender.go
================================================
package app

import (
	"context"

	"github.com/appleboy/gorush/config"
	"github.com/appleboy/gorush/core"
	"github.com/appleboy/gorush/logx"
	"github.com/appleboy/gorush/notify"
	"github.com/appleboy/gorush/status"
)

// CLISendOptions contains options for sending notifications via CLI.
type CLISendOptions struct {
	Token   string
	Message string
	Title   string
	Topic   string
}

// SendAndroidNotification sends an Android notification via CLI.
func SendAndroidNotification(ctx context.Context, cfg *config.ConfYaml, opts CLISendOptions) error {
	cfg.Android.Enabled = true

	req := &notify.PushNotification{
		Platform: core.PlatFormAndroid,
		Message:  opts.Message,
		Title:    opts.Title,
	}

	if opts.Token != "" {
		req.To = opts.Token
	}

	if opts.Topic != "" {
		req.Topic = opts.Topic
	}

	if err := status.InitAppStatus(cfg); err != nil {
		return err
	}

	if _, err := notify.PushToAndroid(ctx, req, cfg); err != nil {
		return err
	}

	return nil
}

// SendHuaweiNotification sends a Huawei notification via CLI.
func SendHuaweiNotification(ctx context.Context, cfg *config.ConfYaml, opts CLISendOptions) error {
	cfg.Huawei.Enabled = true

	req := &notify.PushNotification{
		Platform: core.PlatFormHuawei,
		Message:  opts.Message,
		Title:    opts.Title,
	}

	if opts.Token != "" {
		req.Tokens = []string{opts.Token}
	}

	if opts.Topic != "" {
		req.To = opts.Topic
	}

	if err := notify.CheckMessage(req); err != nil {
		return err
	}

	if err := status.InitAppStatus(cfg); err != nil {
		return err
	}

	if _, err := notify.PushToHuawei(ctx, req, cfg); err != nil {
		return err
	}

	return nil
}

// SendIOSNotification sends an iOS notification via CLI.
func SendIOSNotification(ctx context.Context, cfg *config.ConfYaml, opts CLISendOptions) error {
	cfg.Ios.Enabled = true

	req := &notify.PushNotification{
		Platform: core.PlatFormIos,
		Message:  opts.Message,
		Title:    opts.Title,
	}

	if opts.Token != "" {
		req.Tokens = []string{opts.Token}
	}

	if opts.Topic != "" {
		req.Topic = opts.Topic
	}

	if err := notify.CheckMessage(req); err != nil {
		return err
	}

	if err := status.InitAppStatus(cfg); err != nil {
		return err
	}

	if err := notify.InitAPNSClient(ctx, cfg); err != nil {
		return err
	}

	if _, err := notify.PushToIOS(ctx, req, cfg); err != nil {
		return err
	}

	return nil
}

// SendNotification sends a notification based on platform type.
func SendNotification(
	ctx context.Context,
	platform int,
	cfg *config.ConfYaml,
	opts CLISendOptions,
) error {
	switch platform {
	case core.PlatFormAndroid:
		return SendAndroidNotification(ctx, cfg, opts)
	case core.PlatFormHuawei:
		return SendHuaweiNotification(ctx, cfg, opts)
	case core.PlatFormIos:
		return SendIOSNotification(ctx, cfg, opts)
	default:
		logx.LogError.Fatalf("unsupported platform: %d", platform)
		return nil
	}
}


================================================
FILE: app/sender_test.go
================================================
package app

import (
	"testing"

	"github.com/appleboy/gorush/core"

	"github.com/stretchr/testify/assert"
)

func TestCLISendOptions(t *testing.T) {
	opts := CLISendOptions{
		Token:   "test-token",
		Message: "test-message",
		Title:   "test-title",
		Topic:   "test-topic",
	}

	assert.Equal(t, "test-token", opts.Token)
	assert.Equal(t, "test-message", opts.Message)
	assert.Equal(t, "test-title", opts.Title)
	assert.Equal(t, "test-topic", opts.Topic)
}

func TestSendNotification_UnsupportedPlatform(t *testing.T) {
	// Test that unsupported platform is handled
	// Note: This would call logx.LogError.Fatalf in production,
	// so we can't easily test it without mocking.
	// This test mainly documents the expected behavior.
	assert.Equal(t, 1, core.PlatFormIos)
	assert.Equal(t, 2, core.PlatFormAndroid)
	assert.Equal(t, 3, core.PlatFormHuawei)
}


================================================
FILE: app/worker.go
================================================
package app

import (
	"fmt"

	"github.com/appleboy/gorush/config"
	"github.com/appleboy/gorush/core"
	"github.com/appleboy/gorush/logx"
	"github.com/appleboy/gorush/notify"

	"github.com/golang-queue/nats"
	"github.com/golang-queue/nsq"
	"github.com/golang-queue/queue"
	qcore "github.com/golang-queue/queue/core"
	redisdb "github.com/golang-queue/redisdb-stream"
)

// NewQueueWorker creates a queue worker based on the configured queue engine.
// Supported engines: local, nsq, nats, redis.
func NewQueueWorker(cfg *config.ConfYaml) (qcore.Worker, error) {
	switch core.Queue(cfg.Queue.Engine) {
	case core.LocalQueue:
		return queue.NewRing(
			queue.WithQueueSize(int(cfg.Core.QueueNum)),
			queue.WithFn(notify.Run(cfg)),
			queue.WithLogger(logx.QueueLogger()),
		), nil

	case core.NSQ:
		return nsq.NewWorker(
			nsq.WithAddr(cfg.Queue.NSQ.Addr),
			nsq.WithTopic(cfg.Queue.NSQ.Topic),
			nsq.WithChannel(cfg.Queue.NSQ.Channel),
			nsq.WithMaxInFlight(int(cfg.Core.WorkerNum)),
			nsq.WithRunFunc(notify.Run(cfg)),
			nsq.WithLogger(logx.QueueLogger()),
		), nil

	case core.NATS:
		return nats.NewWorker(
			nats.WithAddr(cfg.Queue.NATS.Addr),
			nats.WithSubj(cfg.Queue.NATS.Subj),
			nats.WithQueue(cfg.Queue.NATS.Queue),
			nats.WithRunFunc(notify.Run(cfg)),
			nats.WithLogger(logx.QueueLogger()),
		), nil

	case core.Redis:
		opts := []redisdb.Option{
			redisdb.WithAddr(cfg.Queue.Redis.Addr),
			redisdb.WithUsername(cfg.Queue.Redis.Username),
			redisdb.WithPassword(cfg.Queue.Redis.Password),
			redisdb.WithDB(cfg.Queue.Redis.DB),
			redisdb.WithStreamName(cfg.Queue.Redis.StreamName),
			redisdb.WithGroup(cfg.Queue.Redis.Group),
			redisdb.WithConsumer(cfg.Queue.Redis.Consumer),
			redisdb.WithMaxLength(cfg.Core.QueueNum),
			redisdb.WithRunFunc(notify.Run(cfg)),
			redisdb.WithLogger(logx.QueueLogger()),
		}
		if cfg.Queue.Redis.WithTLS {
			opts = append(opts, redisdb.WithTLS())
		}
		return redisdb.NewWorker(opts...), nil

	default:
		return nil, fmt.Errorf("unsupported queue engine: %s", cfg.Queue.Engine)
	}
}

// NewQueuePool creates a queue pool with the configured number of workers.
func NewQueuePool(cfg *config.ConfYaml, w qcore.Worker) *queue.Queue {
	return queue.NewPool(
		cfg.Core.WorkerNum,
		queue.WithWorker(w),
		queue.WithLogger(logx.QueueLogger()),
	)
}


================================================
FILE: app/worker_test.go
================================================
package app

import (
	"testing"

	"github.com/appleboy/gorush/config"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewQueueWorker_LocalQueue(t *testing.T) {
	cfg, _ := config.LoadConf("")
	cfg.Queue.Engine = "local"

	w, err := NewQueueWorker(cfg)
	require.NoError(t, err)
	assert.NotNil(t, w)
}

func TestNewQueueWorker_UnsupportedEngine(t *testing.T) {
	cfg, _ := config.LoadConf("")
	cfg.Queue.Engine = "unsupported"

	w, err := NewQueueWorker(cfg)
	require.Error(t, err)
	assert.Nil(t, w)
	assert.Contains(t, err.Error(), "unsupported queue engine")
}

func TestNewQueuePool(t *testing.T) {
	cfg, _ := config.LoadConf("")
	cfg.Queue.Engine = "local"

	w, err := NewQueueWorker(cfg)
	require.NoError(t, err)

	q := NewQueuePool(cfg, w)
	assert.NotNil(t, q)

	// Clean up
	q.Release()
}


================================================
FILE: certificate/authkey-invalid.p8
================================================
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE
ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI
tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfZZZ


================================================
FILE: certificate/authkey-valid.p8
================================================
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE
ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI
tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfeQl
-----END PRIVATE KEY-----


================================================
FILE: certificate/certificate-valid.pem
================================================
Bag Attributes
    localKeyID: 8C 1A 9F 00 66 BD 24 42 B9 5D 1E EB FE 5E 8B CA 04 3D 73 83 
    friendlyName: APNS/2 Private Key
subject=/C=NZ/ST=Wellington/L=Wellington/O=Internet Widgits Pty Ltd/OU=9ZEH62KRVV/CN=APNS/2 Development IOS Push Services: com.sideshow.Apns2
issuer=/C=NZ/ST=Wellington/L=Wellington/O=APNS/2 Inc./OU=APNS/2 Worldwide Developer Relations/CN=APNS/2 Worldwide Developer Relations Certification Authority
-----BEGIN CERTIFICATE-----
MIID6zCCAtMCAQIwDQYJKoZIhvcNAQELBQAwgcMxCzAJBgNVBAYTAk5aMRMwEQYD
VQQIEwpXZWxsaW5ndG9uMRMwEQYDVQQHEwpXZWxsaW5ndG9uMRQwEgYDVQQKEwtB
UE5TLzIgSW5jLjEtMCsGA1UECxMkQVBOUy8yIFdvcmxkd2lkZSBEZXZlbG9wZXIg
UmVsYXRpb25zMUUwQwYDVQQDEzxBUE5TLzIgV29ybGR3aWRlIERldmVsb3BlciBS
ZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTYwMTA4MDgzNDMw
WhcNMjYwMTA1MDgzNDMwWjCBsjELMAkGA1UEBhMCTloxEzARBgNVBAgTCldlbGxp
bmd0b24xEzARBgNVBAcTCldlbGxpbmd0b24xITAfBgNVBAoTGEludGVybmV0IFdp
ZGdpdHMgUHR5IEx0ZDETMBEGA1UECxMKOVpFSDYyS1JWVjFBMD8GA1UEAxM4QVBO
Uy8yIERldmVsb3BtZW50IElPUyBQdXNoIFNlcnZpY2VzOiBjb20uc2lkZXNob3cu
QXBuczIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY0c1TKB5oZPwQ
7t1CwMIrvqB6GIU3tPy6RhckZXTkOB8YeBWJ7UKfCz8HGHFVomBP0T5OUbeqQzqW
YJbQzZ8a6ZMszbL0lO4X9++3Oi5/TtAwOUOK8rOFN25m2KfsayHQZ/4vWStK2Fwm
5aJbGLlpH/b/7z1D4vhmMgoBuT1IuyhGiyFxlZ9EtTloFvsqM1E5fYZOSZACyXTa
K4vdgbQMgUVsI714FAgLTlK0UeiRkmKm3pdbtfVbrthzI+IHXKItUIy+Fn20PRMh
dSnaztSz7tgBWCIx22qvcYogHWiOgUYIM772zE2y8UVOr8DsiRlsOHSA7EI4MJcQ
G2FUq2Z/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGyfyO2HMgcdeBcz3bt5BILX
f7RA2/UmVIwcKR1qotTsF+PnBmcILeyOQgDe9tGU5cRc79kDt3JRmMYROFIMgFRf
Wf22uOKtho7GQQaKvG+bkgMVdYFRlBHnF+KeqKH81qb9p+CT4Iw0GehIL1DijFLR
VIAIBYpz4oBPCIE1ISVT+Fgaf3JAh59kbPbNw9AIDxaBtP8EuzSTNwfbxoGbCobS
Wi1U8IsCwQFt8tM1m4ZXD1CcZIrGdryeAhVkvKIJRiU5QYWI2nqZN+JqQucm9ad0
mYO5mJkIobUa4+ZJhCPKEdmgpFbRGk0wVuaDM9Cv6P2srsYAjaO4y3VP0GvNKRI=
-----END CERTIFICATE-----
Bag Attributes
    localKeyID: 8C 1A 9F 00 66 BD 24 42 B9 5D 1E EB FE 5E 8B CA 04 3D 73 83 
    friendlyName: APNS/2 Private Key
Key Attributes: <No Attributes>
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA2NHNUygeaGT8EO7dQsDCK76gehiFN7T8ukYXJGV05DgfGHgV
ie1Cnws/BxhxVaJgT9E+TlG3qkM6lmCW0M2fGumTLM2y9JTuF/fvtzouf07QMDlD
ivKzhTduZtin7Gsh0Gf+L1krSthcJuWiWxi5aR/2/+89Q+L4ZjIKAbk9SLsoRosh
cZWfRLU5aBb7KjNROX2GTkmQAsl02iuL3YG0DIFFbCO9eBQIC05StFHokZJipt6X
W7X1W67YcyPiB1yiLVCMvhZ9tD0TIXUp2s7Us+7YAVgiMdtqr3GKIB1ojoFGCDO+
9sxNsvFFTq/A7IkZbDh0gOxCODCXEBthVKtmfwIDAQABAoIBAQCW8ZCI+OAae1tE
ipZ9F2bWP3LHLXTo8FYVdCA+VWeITk3PoiIUkJmV0aWCUhDstgto5doDej5sCTur
Xvj/ynaerMeqJFYWkewjwZcgLyAZvwuO1v7fp9E0x/9TGDfnjjnPNeaundxW0cNt
zOY3l0HVHsy9Jpe3QDcAJovy4Tv5+hFY4kDxUBGsyjvhScVgKg5tLkJclm3sOu/L
GyLqpwNI3OJAdMIuVD4N2BZ1aOEap6mp2y8Ie0/R4YWcaZ5A4Pw7xUPl6SXc9uua
/78QTERtPC6ejyCBiE05a8m3Q3iud3Xtnlyws2KwhgBAfE6M4zR/f3OQB7ZIXMhy
ZpmZZw5xAoGBAPYn84IrlIQetWQfvPdM7Kzgh6UDHCugnlCDghwYpRJGi8hMfuZV
xNIrYAJzLYDQ01lFJRJgWXTcbqz9NBz1nhg+cNOz1/KY+38eudee6DNYmztP7jDP
2jnaS+dtjC8hAXObnFqG+NilMDLLu6aRmrJaImbjSrfyLiE6mvJ7u81nAoGBAOF9
g93wZ0mL1rk2s5WwHGTNU/HaOtmWS4z7kA7f4QaRub+MwppZmmDZPHpiZX7BPcZz
iOPQh+xn7IqRGoQWBLykBVt8zZFoLZJoCR3n63lex5A4p/0Pp1gFZrR+xX8PYVos
3yeeiWyPKsXXNc0s5QwHZcX6Wb8EHThTXGCBetcpAoGAMeQJC9IPaPPcae2w3CLA
OY3MkFpgBEuqqsDsxwsLsfeQb0lp0v+BQ+O8suJrT5eDrq1ABUh3+SKQYAl13YS+
xUUqkw35b9cn6iztF9HCWF3WIKBjs4r9PQqMpdxjNE4pQChC+Wov16ErcrAuWWVb
iFiSbm4U/9FbHisFqq3/c3MCgYB+vzSuPgFw37+0oEDVtQZgyuGSop5NzCNvfb/9
/G3aaXNFbnO8mv0hzzoleMWgODLnJ+4cUAz3H3tgcCu9bzr+Zhv0zvQl9a8YCo6F
VuWPdW0rbg1PO8tOuMqATnno79ZC/9H3zS9l7BuY1V2SlNeyqT3VyOFFc6SREpps
TJul8QKBgAxnQB8MA7zPULu1clyaJLdtEdRPkKWN7lKYptc0e/VHfSsKxseWkfqi
zgXZ51kQTrT6Zb6HYRfwC1mMXHWRKRyYjAnCxVim6YQd+KVT49iRDDAiIFoMGA4i
vvcIlneqOZZPDIoKJ60IjO/DZHWkw5mLjaIrT+qQ3XAGdJA13hcm
-----END RSA PRIVATE KEY-----


================================================
FILE: certificate/localhost.cert
================================================
-----BEGIN CERTIFICATE-----
MIIC+zCCAeOgAwIBAgIJALbZEDvUQrFKMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
BAMMCWxvY2FsaG9zdDAeFw0xNjAzMjgwMzMwNDFaFw0yNjAzMjYwMzMwNDFaMBQx
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAMj1+xg4jVLzVnB5j7n1ul30WEE4BCzcNFxg5AOB5H5q+wje0YYiVFg6PQyv
GCipqIRXVRdVQ1hHSeunYGKe8lq3Sb1X8PUJ12v9uRbpS9DK1Owqk8rsPDu6sVTL
qKKgH1Z8yazzaS0AbXuA5e9gO/RzijbnpEP+quM4dueiMPVEJyLq+EoIQY+MM8MP
8dZzL4XZl7wL4UsCN7rPcO6W3tlnT0iO3h9c/Ym2hFhz+KNJ9KRRCvtPGZESigtK
bHsXH099WDo8v/Wp5/evBw/+JD0opxmCfHIBALHt9v53RvvsDZ1t33Rpu5C8znEY
Y2Ay7NgxhqjqoWJqA48lJeA0clsCAwEAAaNQME4wHQYDVR0OBBYEFC0bTU1Xofeh
NKIelashIsqKidDYMB8GA1UdIwQYMBaAFC0bTU1XofehNKIelashIsqKidDYMAwG
A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAiJL8IMTwNX9XqQWYDFgkG4
AnrVwQhreAqC9rSxDCjqqnMHPHGzcCeDMLAMoh0kOy20nowUGNtCZ0uBvnX2q1bN
g1jt+GBcLJDR3LL4CpNOlm3YhOycuNfWMxTA7BXkmnSrZD/7KhArsBEY8aulxwKJ
HRgNlIwe1oFD1YdX1BS5pp4t25B6Vq4A3FMMUkVoWE688nE168hvQgwjrHkgHhwe
eN8lGE2DhFraXnWmDMdwaHD3HRFGhyppIFN+f7BqbWX9gM+T2YRTfObIXLWbqJLD
3Mk/NkxqVcg4eY54wJ1ufCUGAYAIaY6fQqiNUz8nhwK3t45NBVT9y/uJXqnTLyY=
-----END CERTIFICATE-----


================================================
FILE: certificate/localhost.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAyPX7GDiNUvNWcHmPufW6XfRYQTgELNw0XGDkA4Hkfmr7CN7R
hiJUWDo9DK8YKKmohFdVF1VDWEdJ66dgYp7yWrdJvVfw9QnXa/25FulL0MrU7CqT
yuw8O7qxVMuooqAfVnzJrPNpLQBte4Dl72A79HOKNuekQ/6q4zh256Iw9UQnIur4
SghBj4wzww/x1nMvhdmXvAvhSwI3us9w7pbe2WdPSI7eH1z9ibaEWHP4o0n0pFEK
+08ZkRKKC0psexcfT31YOjy/9ann968HD/4kPSinGYJ8cgEAse32/ndG++wNnW3f
dGm7kLzOcRhjYDLs2DGGqOqhYmoDjyUl4DRyWwIDAQABAoIBAGTKqsN9KbSfA42q
CqI0UuLouJMNa1qsnz5uAi6YKWgWdA4A44mpEjCmFRSVhUJvxWuK+cyYIQzXxIWD
D16nZdqF72AeCWZ9JySsvvZ00GfKM3y35iRy08sJWgOzmcLnGJCiSeyKsQe3HTJC
dhDXbXqvsHTVPZg01LTeDxUiTffU8NMKqR2AecQ2sTDwXEhAnTyAtnzl/XaBgFzu
U6G7FzGM5y9bxkfQVkvy+DEJkHGNOjzwcVfByyVl610ixmG1vmxVj9PbWmIPsUV8
ySmjhvDQbOfoxW0h9vTlTqGtQcBw962osnDDMWFCdM7lzO0T7RRnPVGIRpCJOKhq
keqHKwECgYEA8wwI/iZughoTXTNG9LnQQ/WAtsqO80EjMTUheo5I1kOzmUz09pyh
iAsUDoN0/26tZ5WNjlnyZu7dvTc/x3dTZpmNnoo8gcVbQNECDRzqfuQ9PPXm1SN5
6peBqAvBv78hjV05aXzPG/VBbeig7l299EarEA+a/oH3KrgDoqVqE0ECgYEA06vA
YJmgg4fZRucAYoaYsLz9Z9rCFjTe1PBTmUJkbOR8vFIHHTTEWi/SuxXL0wDSeoE2
7BQm86gCC7/KgRdrzoBqZ5qS9Mv2dsLgY635VSgjjfZkVLiH1VRRpSQObYnfoysg
gatcHSKMExd4SLQByAuImXP+L5ayDBcEJfbqSpsCgYB78Is1b0uzNLDjOh7Y9Vhr
D2qPzEORcIoNsdZctOoXuXaAmmngyIbm5R9ZN1gWWc47oFwLV3rxWqXgs6fmg8cX
7v309vFcC9Q4/Vxaa4B5LNK9n3gTAIBPTOtlUnl+2my1tfBtBqRm0W6IKbTHWS5g
vxjEm/CiEIyGUEgqTMgHAQKBgBKuXdQoutng63QufwIzDtbKVzMLQ4XiNKhmbXph
OavCnp+gPbB+L7Yl8ltAmTSOJgVZ0hcT0DxA361Zx+2Mu58GBl4OblnchmwE1vj1
KcQyPrEQxdoUTyiswGfqvrs8J9imvb+z9/U6T1KAB8Wi3WViXzPr4MsiaaRXg642
FIdxAoGAZ7/735dkhJcyOfs+LKsLr68JSstoorXOYvdMu1+JGa9iLuhnHEcMVWC8
IuihzPfloZtMbGYkZJn8l3BeGd8hmfFtgTgZGPoVRetft2LDFLnPxp2sEH5OFLsQ
R+K/kAOul8eStWuMXOFA9pMzGkGEgIFJMJOyaJON3kedQI8deCM=
-----END RSA PRIVATE KEY-----


================================================
FILE: config/config.go
================================================
package config

import (
	"bytes"
	"fmt"
	"log"
	"net"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"

	"github.com/spf13/viper"
)

var defaultConf = []byte(`
core:
  enabled: true # enable httpd server
  address: "" # ip address to bind (default: any)
  shutdown_timeout: 30 # default is 30 second
  port: "8088" # ignore this port number if auto_tls is enabled (listen 443).
  worker_num: 0 # default worker number is runtime.NumCPU()
  queue_num: 0 # default queue number is 8192
  max_notification: 100
  # set true if you need get error message from fail push notification in API response.
  # It only works when the queue engine is local.
  sync: false
  # set webhook url if you need get error message asynchronously from fail push notification in API response.
  feedback_hook_url: ""
  feedback_timeout: 10 # default is 10 second
  feedback_header:
  mode: "release"
  ssl: false
  cert_path: "cert.pem"
  key_path: "key.pem"
  cert_base64: ""
  key_base64: ""
  http_proxy: ""
  pid:
    enabled: false
    path: "gorush.pid"
    override: true
  auto_tls:
    enabled: false # Automatically install TLS certificates from Let's Encrypt.
    folder: ".cache" # folder for storing TLS certificates
    host: "" # which domains the Let's Encrypt will attempt

grpc:
  enabled: false # enable gRPC server
  port: 9000

api:
  push_uri: "/api/push"
  stat_go_uri: "/api/stat/go"
  stat_app_uri: "/api/stat/app"
  config_uri: "/api/config"
  sys_stat_uri: "/sys/stats"
  metric_uri: "/metrics"
  health_uri: "/healthz"

android:
  enabled: true
  key_path: "" # path to fcm key file
  credential: "" # fcm credential data
  max_retry: 0 # resend fail notification, default value zero is disabled

huawei:
  enabled: false
  appsecret: "YOUR_APP_SECRET"
  appid: "YOUR_APP_ID"
  max_retry: 0 # resend fail notification, default value zero is disabled

queue:
  engine: "local" # support "local", "nsq", "nats" and "redis" default value is "local"
  nsq:
    addr: 127.0.0.1:4150
    topic: gorush
    channel: gorush
  nats:
    addr: 127.0.0.1:4222
    subj: gorush
    queue: gorush
  redis:
    addr: 127.0.0.1:6379
    group: gorush
    consumer: gorush
    stream_name: gorush
    with_tls: false
    username: ""
    password: ""
    db: 0

ios:
  enabled: false
  key_path: ""
  key_base64: "" # load iOS key from base64 input
  key_type: "pem" # could be pem, p12 or p8 type
  password: "" # certificate password, default as empty string.
  production: false
  max_concurrent_pushes: 100 # just for push ios notification
  max_retry: 0 # resend fail notification, default value zero is disabled
  key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)
  team_id: "" # TeamID from developer account (View Account -> Membership)

log:
  format: "string" # string or json
  access_log: "stdout" # stdout: output to console, or define log path like "log/access_log"
  access_level: "debug"
  error_log: "stderr" # stderr: output to console, or define log path like "log/error_log"
  error_level: "error"
  hide_token: true
  hide_messages: false

stat:
  engine: "memory" # support memory, redis, boltdb, buntdb or leveldb
  redis:
    cluster: false
    addr: "localhost:6379" # if cluster is true, you may set this to "localhost:6379,localhost:6380,localhost:6381"
    username: ""
    password: ""
    db: 0
  boltdb:
    path: "bolt.db"
    bucket: "gorush"
  buntdb:
    path: "bunt.db"
  leveldb:
    path: "level.db"
  badgerdb:
    path: "badger.db"
`)

// ConfYaml is config structure.
type ConfYaml struct {
	Core    SectionCore    `yaml:"core"`
	API     SectionAPI     `yaml:"api"`
	Android SectionAndroid `yaml:"android"`
	Huawei  SectionHuawei  `yaml:"huawei"`
	Ios     SectionIos     `yaml:"ios"`
	Queue   SectionQueue   `yaml:"queue"`
	Log     SectionLog     `yaml:"log"`
	Stat    SectionStat    `yaml:"stat"`
	GRPC    SectionGRPC    `yaml:"grpc"`
}

// SectionCore is sub section of config.
type SectionCore struct {
	Enabled         bool           `yaml:"enabled"`
	Address         string         `yaml:"address"`
	ShutdownTimeout int64          `yaml:"shutdown_timeout"`
	Port            string         `yaml:"port"`
	MaxNotification int64          `yaml:"max_notification"`
	WorkerNum       int64          `yaml:"worker_num"`
	QueueNum        int64          `yaml:"queue_num"`
	Mode            string         `yaml:"mode"`
	Sync            bool           `yaml:"sync"`
	SSL             bool           `yaml:"ssl"`
	CertPath        string         `yaml:"cert_path"`
	KeyPath         string         `yaml:"key_path"`
	CertBase64      string         `yaml:"cert_base64"`
	KeyBase64       string         `yaml:"key_base64"`
	HTTPProxy       string         `yaml:"http_proxy"`
	PID             SectionPID     `yaml:"pid"`
	AutoTLS         SectionAutoTLS `yaml:"auto_tls"`

	FeedbackURL     string   `yaml:"feedback_hook_url"`
	FeedbackTimeout int64    `yaml:"feedback_timeout"`
	FeedbackHeader  []string `yaml:"feedback_header"`
}

// SectionAutoTLS support Let's Encrypt setting.
type SectionAutoTLS struct {
	Enabled bool   `yaml:"enabled"`
	Folder  string `yaml:"folder"`
	Host    string `yaml:"host"`
}

// SectionAPI is sub section of config.
type SectionAPI struct {
	PushURI    string `yaml:"push_uri"`
	StatGoURI  string `yaml:"stat_go_uri"`
	StatAppURI string `yaml:"stat_app_uri"`
	ConfigURI  string `yaml:"config_uri"`
	SysStatURI string `yaml:"sys_stat_uri"`
	MetricURI  string `yaml:"metric_uri"`
	HealthURI  string `yaml:"health_uri"`
}

// SectionAndroid is sub section of config.
type SectionAndroid struct {
	Enabled    bool   `yaml:"enabled"`
	KeyPath    string `yaml:"key_path"`
	Credential string `yaml:"credential"`
	MaxRetry   int    `yaml:"max_retry"`
}

// SectionHuawei is sub section of config.
type SectionHuawei struct {
	Enabled   bool   `yaml:"enabled"`
	AppSecret string `yaml:"appsecret"`
	AppID     string `yaml:"appid"`
	MaxRetry  int    `yaml:"max_retry"`
}

// SectionIos is sub section of config.
type SectionIos struct {
	Enabled             bool   `yaml:"enabled"`
	KeyPath             string `yaml:"key_path"`
	KeyBase64           string `yaml:"key_base64"`
	KeyType             string `yaml:"key_type"`
	Password            string `yaml:"password"`
	Production          bool   `yaml:"production"`
	MaxConcurrentPushes uint   `yaml:"max_concurrent_pushes"`
	MaxRetry            int    `yaml:"max_retry"`
	KeyID               string `yaml:"key_id"`
	TeamID              string `yaml:"team_id"`
}

// SectionLog is sub section of config.
type SectionLog struct {
	Format       string `yaml:"format"`
	AccessLog    string `yaml:"access_log"`
	AccessLevel  string `yaml:"access_level"`
	ErrorLog     string `yaml:"error_log"`
	ErrorLevel   string `yaml:"error_level"`
	HideToken    bool   `yaml:"hide_token"`
	HideMessages bool   `yaml:"hide_messages"`
}

// SectionStat is sub section of config.
type SectionStat struct {
	Engine   string          `yaml:"engine"`
	Redis    SectionRedis    `yaml:"redis"`
	BoltDB   SectionBoltDB   `yaml:"boltdb"`
	BuntDB   SectionBuntDB   `yaml:"buntdb"`
	LevelDB  SectionLevelDB  `yaml:"leveldb"`
	BadgerDB SectionBadgerDB `yaml:"badgerdb"`
}

// SectionQueue is sub section of config.
type SectionQueue struct {
	Engine string            `yaml:"engine"`
	NSQ    SectionNSQ        `yaml:"nsq"`
	NATS   SectionNATS       `yaml:"nats"`
	Redis  SectionRedisQueue `yaml:"redis"`
}

// SectionNSQ is sub section of config.
type SectionNSQ struct {
	Addr    string `yaml:"addr"`
	Topic   string `yaml:"topic"`
	Channel string `yaml:"channel"`
}

// SectionNATS is sub section of config.
type SectionNATS struct {
	Addr  string `yaml:"addr"`
	Subj  string `yaml:"subj"`
	Queue string `yaml:"queue"`
}

// SectionRedisQueue is sub section of config.
type SectionRedisQueue struct {
	Addr       string `yaml:"addr"`
	Username   string `yaml:"username"`
	Password   string `yaml:"password"`
	DB         int    `yaml:"db"`
	StreamName string `yaml:"stream_name"`
	Group      string `yaml:"group"`
	Consumer   string `yaml:"consumer"`
	WithTLS    bool   `yaml:"with_tls"`
}

// SectionRedis is sub section of config.
type SectionRedis struct {
	Cluster  bool   `yaml:"cluster"`
	Addr     string `yaml:"addr"`
	Username string `yaml:"username"`
	Password string `yaml:"password"`
	DB       int    `yaml:"db"`
}

// SectionBoltDB is sub section of config.
type SectionBoltDB struct {
	Path   string `yaml:"path"`
	Bucket string `yaml:"bucket"`
}

// SectionBuntDB is sub section of config.
type SectionBuntDB struct {
	Path string `yaml:"path"`
}

// SectionLevelDB is sub section of config.
type SectionLevelDB struct {
	Path string `yaml:"path"`
}

// SectionBadgerDB is sub section of config.
type SectionBadgerDB struct {
	Path string `yaml:"path"`
}

// SectionPID is sub section of config.
type SectionPID struct {
	Enabled  bool   `yaml:"enabled"`
	Path     string `yaml:"path"`
	Override bool   `yaml:"override"`
}

// SectionGRPC is sub section of config.
type SectionGRPC struct {
	Enabled bool   `yaml:"enabled"`
	Port    string `yaml:"port"`
}

func setDefaults() {
	// Core defaults
	viper.SetDefault("core.enabled", true)
	viper.SetDefault("core.address", "")
	viper.SetDefault("core.shutdown_timeout", 30)
	viper.SetDefault("core.port", "8088")
	viper.SetDefault("core.worker_num", 0)
	viper.SetDefault("core.queue_num", 0)
	viper.SetDefault("core.max_notification", 100)
	viper.SetDefault("core.sync", false)
	viper.SetDefault("core.feedback_timeout", 10)
	viper.SetDefault("core.mode", "release")
	viper.SetDefault("core.ssl", false)
	viper.SetDefault("core.cert_path", "cert.pem")
	viper.SetDefault("core.key_path", "key.pem")
	viper.SetDefault("core.pid.enabled", false)
	viper.SetDefault("core.pid.path", "gorush.pid")
	viper.SetDefault("core.pid.override", true)
	viper.SetDefault("core.auto_tls.enabled", false)
	viper.SetDefault("core.auto_tls.folder", ".cache")

	// iOS defaults
	viper.SetDefault("ios.enabled", false)
	viper.SetDefault("ios.key_type", "pem")
	viper.SetDefault("ios.production", false)
	viper.SetDefault("ios.max_concurrent_pushes", uint(100))
	viper.SetDefault("ios.max_retry", 0)

	// Android defaults
	viper.SetDefault("android.enabled", true)
	viper.SetDefault("android.max_retry", 0)

	// API defaults
	viper.SetDefault("api.push_uri", "/api/push")
	viper.SetDefault("api.stat_go_uri", "/api/stat/go")
	viper.SetDefault("api.stat_app_uri", "/api/stat/app")
	viper.SetDefault("api.config_uri", "/api/config")
	viper.SetDefault("api.sys_stat_uri", "/sys/stats")
	viper.SetDefault("api.metric_uri", "/metrics")
	viper.SetDefault("api.health_uri", "/healthz")

	// gRPC defaults
	viper.SetDefault("grpc.enabled", false)
	viper.SetDefault("grpc.port", "9000")

	// Queue defaults
	viper.SetDefault("queue.engine", "local")

	// Stat defaults
	viper.SetDefault("stat.engine", "memory")

	// Log defaults
	viper.SetDefault("log.format", "string")
	viper.SetDefault("log.access_log", "stdout")
	viper.SetDefault("log.access_level", "debug")
	viper.SetDefault("log.error_log", "stderr")
	viper.SetDefault("log.error_level", "error")
	viper.SetDefault("log.hide_token", true)
	viper.SetDefault("log.hide_messages", false)
}

// LoadConf load config from file and read in environment variables that match
func LoadConf(confPath ...string) (*ConfYaml, error) {
	// load default values
	setDefaults()

	viper.SetConfigType("yaml")
	viper.AutomaticEnv()         // read in environment variables that match
	viper.SetEnvPrefix("gorush") // will be uppercased automatically
	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

	// If config path is provided, load from file
	if len(confPath) > 0 && confPath[0] != "" {
		content, err := os.ReadFile(confPath[0])
		if err != nil {
			return nil, fmt.Errorf("failed to read config file %s: %w", confPath[0], err)
		}

		if err := viper.ReadConfig(bytes.NewBuffer(content)); err != nil {
			return nil, fmt.Errorf("failed to parse config file %s: %w", confPath[0], err)
		}

		// Early return after successfully loading config from file
		return loadConfigFromViper()
	}

	// Search config in home directory with name ".gorush" (without extension).
	viper.AddConfigPath("/etc/gorush/")
	viper.AddConfigPath("$HOME/.gorush")
	viper.AddConfigPath(".")
	viper.SetConfigName("config")

	// If a config file is found, read it in.
	if err := viper.ReadInConfig(); err == nil {
		log.Println("Using config file:", viper.ConfigFileUsed())
		return loadConfigFromViper()
	}

	// Try to load default config as fallback
	if err := viper.ReadConfig(bytes.NewBuffer(defaultConf)); err != nil {
		return nil, fmt.Errorf("failed to load default config and no config file found: %w", err)
	}

	return loadConfigFromViper()
}

// loadConfigFromViper extracts config loading logic to avoid code duplication
func loadConfigFromViper() (*ConfYaml, error) {
	conf := &ConfYaml{}

	// Core
	conf.Core.Address = viper.GetString("core.address")
	conf.Core.Port = viper.GetString("core.port")
	conf.Core.ShutdownTimeout = int64(viper.GetInt("core.shutdown_timeout"))
	conf.Core.Enabled = viper.GetBool("core.enabled")
	conf.Core.WorkerNum = int64(viper.GetInt("core.worker_num"))
	conf.Core.QueueNum = int64(viper.GetInt("core.queue_num"))
	conf.Core.Mode = viper.GetString("core.mode")
	conf.Core.Sync = viper.GetBool("core.sync")
	conf.Core.FeedbackURL = viper.GetString("core.feedback_hook_url")
	conf.Core.FeedbackTimeout = int64(viper.GetInt("core.feedback_timeout"))
	conf.Core.FeedbackHeader = viper.GetStringSlice("core.feedback_header")
	conf.Core.SSL = viper.GetBool("core.ssl")
	conf.Core.CertPath = viper.GetString("core.cert_path")
	conf.Core.KeyPath = viper.GetString("core.key_path")
	conf.Core.CertBase64 = viper.GetString("core.cert_base64")
	conf.Core.KeyBase64 = viper.GetString("core.key_base64")
	conf.Core.MaxNotification = int64(viper.GetInt("core.max_notification"))
	conf.Core.HTTPProxy = viper.GetString("core.http_proxy")
	conf.Core.PID.Enabled = viper.GetBool("core.pid.enabled")
	conf.Core.PID.Path = viper.GetString("core.pid.path")
	conf.Core.PID.Override = viper.GetBool("core.pid.override")
	conf.Core.AutoTLS.Enabled = viper.GetBool("core.auto_tls.enabled")
	conf.Core.AutoTLS.Folder = viper.GetString("core.auto_tls.folder")
	conf.Core.AutoTLS.Host = viper.GetString("core.auto_tls.host")

	// Api
	conf.API.PushURI = viper.GetString("api.push_uri")
	conf.API.StatGoURI = viper.GetString("api.stat_go_uri")
	conf.API.StatAppURI = viper.GetString("api.stat_app_uri")
	conf.API.ConfigURI = viper.GetString("api.config_uri")
	conf.API.SysStatURI = viper.GetString("api.sys_stat_uri")
	conf.API.MetricURI = viper.GetString("api.metric_uri")
	conf.API.HealthURI = viper.GetString("api.health_uri")

	// Android
	conf.Android.Enabled = viper.GetBool("android.enabled")
	conf.Android.KeyPath = viper.GetString("android.key_path")
	conf.Android.Credential = viper.GetString("android.credential")
	conf.Android.MaxRetry = viper.GetInt("android.max_retry")

	// Huawei
	conf.Huawei.Enabled = viper.GetBool("huawei.enabled")
	conf.Huawei.AppSecret = viper.GetString("huawei.appsecret")
	conf.Huawei.AppID = viper.GetString("huawei.appid")
	conf.Huawei.MaxRetry = viper.GetInt("huawei.max_retry")

	// iOS
	conf.Ios.Enabled = viper.GetBool("ios.enabled")
	conf.Ios.KeyPath = viper.GetString("ios.key_path")
	conf.Ios.KeyBase64 = viper.GetString("ios.key_base64")
	conf.Ios.KeyType = viper.GetString("ios.key_type")
	conf.Ios.Password = viper.GetString("ios.password")
	conf.Ios.Production = viper.GetBool("ios.production")
	conf.Ios.MaxConcurrentPushes = viper.GetUint("ios.max_concurrent_pushes")
	conf.Ios.MaxRetry = viper.GetInt("ios.max_retry")
	conf.Ios.KeyID = viper.GetString("ios.key_id")
	conf.Ios.TeamID = viper.GetString("ios.team_id")

	// log
	conf.Log.Format = viper.GetString("log.format")
	conf.Log.AccessLog = viper.GetString("log.access_log")
	conf.Log.AccessLevel = viper.GetString("log.access_level")
	conf.Log.ErrorLog = viper.GetString("log.error_log")
	conf.Log.ErrorLevel = viper.GetString("log.error_level")
	conf.Log.HideToken = viper.GetBool("log.hide_token")
	conf.Log.HideMessages = viper.GetBool("log.hide_messages")

	// Queue Engine
	conf.Queue.Engine = viper.GetString("queue.engine")
	conf.Queue.NSQ.Addr = viper.GetString("queue.nsq.addr")
	conf.Queue.NSQ.Topic = viper.GetString("queue.nsq.topic")
	conf.Queue.NSQ.Channel = viper.GetString("queue.nsq.channel")
	conf.Queue.NATS.Addr = viper.GetString("queue.nats.addr")
	conf.Queue.NATS.Subj = viper.GetString("queue.nats.subj")
	conf.Queue.NATS.Queue = viper.GetString("queue.nats.queue")
	conf.Queue.Redis.Addr = viper.GetString("queue.redis.addr")
	conf.Queue.Redis.StreamName = viper.GetString("queue.redis.stream_name")
	conf.Queue.Redis.Group = viper.GetString("queue.redis.group")
	conf.Queue.Redis.Consumer = viper.GetString("queue.redis.consumer")
	conf.Queue.Redis.WithTLS = viper.GetBool("queue.redis.with_tls")
	conf.Queue.Redis.Username = viper.GetString("queue.redis.username")
	conf.Queue.Redis.Password = viper.GetString("queue.redis.password")
	conf.Queue.Redis.DB = viper.GetInt("queue.redis.db")

	// Stat Engine
	conf.Stat.Engine = viper.GetString("stat.engine")
	conf.Stat.Redis.Cluster = viper.GetBool("stat.redis.cluster")
	conf.Stat.Redis.Addr = viper.GetString("stat.redis.addr")
	conf.Stat.Redis.Username = viper.GetString("stat.redis.username")
	conf.Stat.Redis.Password = viper.GetString("stat.redis.password")
	conf.Stat.Redis.DB = viper.GetInt("stat.redis.db")
	conf.Stat.BoltDB.Path = viper.GetString("stat.boltdb.path")
	conf.Stat.BoltDB.Bucket = viper.GetString("stat.boltdb.bucket")
	conf.Stat.BuntDB.Path = viper.GetString("stat.buntdb.path")
	conf.Stat.LevelDB.Path = viper.GetString("stat.leveldb.path")
	conf.Stat.BadgerDB.Path = viper.GetString("stat.badgerdb.path")

	// gRPC Server
	conf.GRPC.Enabled = viper.GetBool("grpc.enabled")
	conf.GRPC.Port = viper.GetString("grpc.port")

	if conf.Core.WorkerNum == int64(0) {
		conf.Core.WorkerNum = int64(runtime.NumCPU())
	}

	if conf.Core.QueueNum == int64(0) {
		conf.Core.QueueNum = int64(8192)
	}

	return conf, nil
}

// ValidatePort validates that a port string is within valid range
func ValidatePort(port string) error {
	if port == "" {
		return nil // empty port is allowed, will use default
	}
	p, err := strconv.Atoi(port)
	if err != nil {
		return fmt.Errorf("invalid port format: %s", port)
	}
	if p < 1 || p > 65535 {
		return fmt.Errorf("port out of range (1-65535): %d", p)
	}
	return nil
}

// ValidateAddress validates that an address is not empty and contains valid characters
func ValidateAddress(addr string) error {
	if addr == "" {
		return nil // empty address is allowed, will bind to all interfaces
	}
	// Basic validation - check if it's a valid IP or hostname format
	if net.ParseIP(addr) == nil {
		// If not a valid IP, check if it could be a hostname (basic check)
		if len(addr) > 253 || strings.Contains(addr, "..") {
			return fmt.Errorf("invalid address format: %s", addr)
		}
	}
	return nil
}

// ValidatePIDPath validates and sanitizes PID file path to prevent path traversal
func ValidatePIDPath(pidPath string) error {
	if pidPath == "" {
		return nil
	}

	// Clean the path to resolve any . or .. elements
	cleanPath := filepath.Clean(pidPath)

	// Check for path traversal attempts by looking for ".." in the cleaned path
	// If a relative path attempts to traverse upwards, it will still have a ".." prefix after cleaning
	if !filepath.IsAbs(cleanPath) && strings.HasPrefix(cleanPath, "..") {
		return fmt.Errorf("path traversal detected in PID path: %s", pidPath)
	}

	// Ensure the path is not trying to write to sensitive system directories
	sensitive := []string{"/etc/", "/usr/", "/var/", "/sys/", "/proc/"}
	for _, prefix := range sensitive {
		if strings.HasPrefix(cleanPath, prefix) {
			return fmt.Errorf("cannot write PID file to sensitive directory: %s", cleanPath)
		}
	}

	return nil
}

// ValidateConfig validates critical configuration parameters
func ValidateConfig(cfg *ConfYaml) error {
	if err := ValidatePort(cfg.Core.Port); err != nil {
		return fmt.Errorf("invalid core port: %w", err)
	}

	if err := ValidateAddress(cfg.Core.Address); err != nil {
		return fmt.Errorf("invalid core address: %w", err)
	}

	if err := ValidatePIDPath(cfg.Core.PID.Path); err != nil {
		return fmt.Errorf("invalid PID path: %w", err)
	}

	// Validate Redis address if Redis is enabled
	if cfg.Stat.Engine == "redis" && cfg.Stat.Redis.Addr != "" {
		host, port, err := net.SplitHostPort(cfg.Stat.Redis.Addr)
		if err != nil {
			return fmt.Errorf("invalid Redis address format: %s", cfg.Stat.Redis.Addr)
		}
		if err := ValidateAddress(host); err != nil {
			return fmt.Errorf("invalid Redis host: %w", err)
		}
		if err := ValidatePort(port); err != nil {
			return fmt.Errorf("invalid Redis port: %w", err)
		}
	}

	return nil
}

// SanitizedCopy returns a copy of the config with sensitive fields redacted.
func (c *ConfYaml) SanitizedCopy() *ConfYaml {
	sanitized := *c

	// Core TLS & proxy
	sanitized.Core.CertBase64 = redact(c.Core.CertBase64)
	sanitized.Core.KeyBase64 = redact(c.Core.KeyBase64)
	sanitized.Core.HTTPProxy = redact(c.Core.HTTPProxy)
	sanitized.Core.CertPath = redact(c.Core.CertPath)
	sanitized.Core.KeyPath = redact(c.Core.KeyPath)
	if len(c.Core.FeedbackHeader) > 0 {
		sanitized.Core.FeedbackHeader = make([]string, len(c.Core.FeedbackHeader))
		for i, v := range c.Core.FeedbackHeader {
			if v != "" {
				sanitized.Core.FeedbackHeader[i] = "[REDACTED]"
			}
		}
	}

	// Android
	sanitized.Android.KeyPath = redact(c.Android.KeyPath)
	sanitized.Android.Credential = redact(c.Android.Credential)

	// Huawei
	sanitized.Huawei.AppSecret = redact(c.Huawei.AppSecret)

	// iOS
	sanitized.Ios.KeyPath = redact(c.Ios.KeyPath)
	sanitized.Ios.KeyBase64 = redact(c.Ios.KeyBase64)
	sanitized.Ios.Password = redact(c.Ios.Password)
	sanitized.Ios.KeyID = redact(c.Ios.KeyID)
	sanitized.Ios.TeamID = redact(c.Ios.TeamID)

	// Queue Redis
	sanitized.Queue.Redis.Username = redact(c.Queue.Redis.Username)
	sanitized.Queue.Redis.Password = redact(c.Queue.Redis.Password)

	// Stat Redis
	sanitized.Stat.Redis.Username = redact(c.Stat.Redis.Username)
	sanitized.Stat.Redis.Password = redact(c.Stat.Redis.Password)

	return &sanitized
}

func redact(s string) string {
	if s != "" {
		return "[REDACTED]"
	}
	return ""
}


================================================
FILE: config/config_test.go
================================================
package config

import (
	"os"
	"runtime"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
)

// Test file is missing
func TestMissingFile(t *testing.T) {
	filename := "nonexistent_file.yml"
	conf, err := LoadConf(filename)

	assert.Nil(t, conf)
	require.Error(t, err)
	// Check that the error message includes the filename and describes the issue
	assert.Contains(t, err.Error(), "failed to read config file")
	assert.Contains(t, err.Error(), filename)
}

// Test invalid YAML content
func TestInvalidYAMLFile(t *testing.T) {
	// Create a temporary file with invalid YAML
	tmpFile := "test_invalid.yml"
	content := []byte("invalid: yaml: content: [unclosed")

	// Write invalid content to a temporary file
	err := os.WriteFile(tmpFile, content, 0o600)
	require.NoError(t, err)
	defer os.Remove(tmpFile) // Clean up

	conf, err := LoadConf(tmpFile)

	assert.Nil(t, conf)
	require.Error(t, err)
	// Check that the error message includes the filename and describes parsing failure
	assert.Contains(t, err.Error(), "failed to parse config file")
	assert.Contains(t, err.Error(), tmpFile)
}

// Test improved error messages for default config loading
func TestDefaultConfigLoadFailure(t *testing.T) {
	// Backup original defaultConf
	originalDefaultConf := defaultConf
	defer func() {
		defaultConf = originalDefaultConf
	}()

	// Set invalid default config
	defaultConf = []byte(`invalid: yaml: [unclosed`)

	conf, err := LoadConf()

	assert.Nil(t, conf)
	require.Error(t, err)
	// Check that the error message describes the default config loading failure
	assert.Contains(t, err.Error(), "failed to load default config and no config file found")
}

func TestEmptyConfig(t *testing.T) {
	conf, err := LoadConf("testdata/empty.yml")
	if err != nil {
		panic("failed to load config.yml from file")
	}

	assert.Equal(t, uint(100), conf.Ios.MaxConcurrentPushes)
}

type ConfigTestSuite struct {
	suite.Suite
	ConfGorushDefault *ConfYaml
	ConfGorush        *ConfYaml
}

func (suite *ConfigTestSuite) SetupTest() {
	var err error
	suite.ConfGorushDefault, err = LoadConf()
	if err != nil {
		panic("failed to load default config.yml")
	}
	suite.ConfGorush, err = LoadConf("testdata/config.yml")
	if err != nil {
		panic("failed to load config.yml from file")
	}
}

func (suite *ConfigTestSuite) TestValidateConfDefault() {
	// Core
	suite.Empty(suite.ConfGorushDefault.Core.Address)
	suite.Equal("8088", suite.ConfGorushDefault.Core.Port)
	suite.Equal(int64(30), suite.ConfGorushDefault.Core.ShutdownTimeout)
	suite.True(suite.ConfGorushDefault.Core.Enabled)
	suite.Equal(int64(runtime.NumCPU()), suite.ConfGorushDefault.Core.WorkerNum)
	suite.Equal(int64(8192), suite.ConfGorushDefault.Core.QueueNum)
	suite.Equal("release", suite.ConfGorushDefault.Core.Mode)
	suite.False(suite.ConfGorushDefault.Core.Sync)
	suite.Empty(suite.ConfGorushDefault.Core.FeedbackURL)
	suite.Empty(suite.ConfGorushDefault.Core.FeedbackHeader)
	suite.Equal(int64(10), suite.ConfGorushDefault.Core.FeedbackTimeout)
	suite.False(suite.ConfGorushDefault.Core.SSL)
	suite.Equal("cert.pem", suite.ConfGorushDefault.Core.CertPath)
	suite.Equal("key.pem", suite.ConfGorushDefault.Core.KeyPath)
	suite.Empty(suite.ConfGorushDefault.Core.KeyBase64)
	suite.Empty(suite.ConfGorushDefault.Core.CertBase64)
	suite.Equal(int64(100), suite.ConfGorushDefault.Core.MaxNotification)
	suite.Empty(suite.ConfGorushDefault.Core.HTTPProxy)
	// Pid
	suite.False(suite.ConfGorushDefault.Core.PID.Enabled)
	suite.Equal("gorush.pid", suite.ConfGorushDefault.Core.PID.Path)
	suite.True(suite.ConfGorushDefault.Core.PID.Override)
	suite.False(suite.ConfGorushDefault.Core.AutoTLS.Enabled)
	suite.Equal(".cache", suite.ConfGorushDefault.Core.AutoTLS.Folder)
	suite.Empty(suite.ConfGorushDefault.Core.AutoTLS.Host)

	// Api
	suite.Equal("/api/push", suite.ConfGorushDefault.API.PushURI)
	suite.Equal("/api/stat/go", suite.ConfGorushDefault.API.StatGoURI)
	suite.Equal("/api/stat/app", suite.ConfGorushDefault.API.StatAppURI)
	suite.Equal("/api/config", suite.ConfGorushDefault.API.ConfigURI)
	suite.Equal("/sys/stats", suite.ConfGorushDefault.API.SysStatURI)
	suite.Equal("/metrics", suite.ConfGorushDefault.API.MetricURI)
	suite.Equal("/healthz", suite.ConfGorushDefault.API.HealthURI)

	// Android
	suite.True(suite.ConfGorushDefault.Android.Enabled)
	suite.Empty(suite.ConfGorushDefault.Android.KeyPath)
	suite.Empty(suite.ConfGorushDefault.Android.Credential)
	suite.Equal(0, suite.ConfGorushDefault.Android.MaxRetry)

	// iOS
	suite.False(suite.ConfGorushDefault.Ios.Enabled)
	suite.Empty(suite.ConfGorushDefault.Ios.KeyPath)
	suite.Empty(suite.ConfGorushDefault.Ios.KeyBase64)
	suite.Equal("pem", suite.ConfGorushDefault.Ios.KeyType)
	suite.Empty(suite.ConfGorushDefault.Ios.Password)
	suite.False(suite.ConfGorushDefault.Ios.Production)
	suite.Equal(uint(100), suite.ConfGorushDefault.Ios.MaxConcurrentPushes)
	suite.Equal(0, suite.ConfGorushDefault.Ios.MaxRetry)
	suite.Empty(suite.ConfGorushDefault.Ios.KeyID)
	suite.Empty(suite.ConfGorushDefault.Ios.TeamID)

	// queue
	suite.Equal("local", suite.ConfGorushDefault.Queue.Engine)
	suite.Equal("127.0.0.1:4150", suite.ConfGorushDefault.Queue.NSQ.Addr)
	suite.Equal("gorush", suite.ConfGorushDefault.Queue.NSQ.Topic)
	suite.Equal("gorush", suite.ConfGorushDefault.Queue.NSQ.Channel)

	suite.Equal("127.0.0.1:4222", suite.ConfGorushDefault.Queue.NATS.Addr)
	suite.Equal("gorush", suite.ConfGorushDefault.Queue.NATS.Subj)
	suite.Equal("gorush", suite.ConfGorushDefault.Queue.NATS.Queue)

	suite.Equal("127.0.0.1:6379", suite.ConfGorushDefault.Queue.Redis.Addr)
	suite.Equal("gorush", suite.ConfGorushDefault.Queue.Redis.StreamName)
	suite.Equal("gorush", suite.ConfGorushDefault.Queue.Redis.Group)
	suite.Equal("gorush", suite.ConfGorushDefault.Queue.Redis.Consumer)
	suite.Empty(suite.ConfGorushDefault.Queue.Redis.Username)
	suite.Empty(suite.ConfGorushDefault.Queue.Redis.Password)
	suite.False(suite.ConfGorushDefault.Queue.Redis.WithTLS)
	suite.Equal(0, suite.ConfGorushDefault.Queue.Redis.DB)

	// log
	suite.Equal("string", suite.ConfGorushDefault.Log.Format)
	suite.Equal("stdout", suite.ConfGorushDefault.Log.AccessLog)
	suite.Equal("debug", suite.ConfGorushDefault.Log.AccessLevel)
	suite.Equal("stderr", suite.ConfGorushDefault.Log.ErrorLog)
	suite.Equal("error", suite.ConfGorushDefault.Log.ErrorLevel)
	suite.True(suite.ConfGorushDefault.Log.HideToken)
	suite.False(suite.ConfGorushDefault.Log.HideMessages)

	suite.Equal("memory", suite.ConfGorushDefault.Stat.Engine)
	suite.False(suite.ConfGorushDefault.Stat.Redis.Cluster)
	suite.Equal("localhost:6379", suite.ConfGorushDefault.Stat.Redis.Addr)
	suite.Empty(suite.ConfGorushDefault.Stat.Redis.Username)
	suite.Empty(suite.ConfGorushDefault.Stat.Redis.Password)
	suite.Equal(0, suite.ConfGorushDefault.Stat.Redis.DB)

	suite.Equal("bolt.db", suite.ConfGorushDefault.Stat.BoltDB.Path)
	suite.Equal("gorush", suite.ConfGorushDefault.Stat.BoltDB.Bucket)

	suite.Equal("bunt.db", suite.ConfGorushDefault.Stat.BuntDB.Path)
	suite.Equal("level.db", suite.ConfGorushDefault.Stat.LevelDB.Path)
	suite.Equal("badger.db", suite.ConfGorushDefault.Stat.BadgerDB.Path)

	// gRPC
	suite.False(suite.ConfGorushDefault.GRPC.Enabled)
	suite.Equal("9000", suite.ConfGorushDefault.GRPC.Port)
}

func (suite *ConfigTestSuite) TestValidateConf() {
	// Core
	suite.Equal("8088", suite.ConfGorush.Core.Port)
	suite.Equal(int64(30), suite.ConfGorush.Core.ShutdownTimeout)
	suite.True(suite.ConfGorush.Core.Enabled)
	suite.Equal(int64(runtime.NumCPU()), suite.ConfGorush.Core.WorkerNum)
	suite.Equal(int64(8192), suite.ConfGorush.Core.QueueNum)
	suite.Equal("release", suite.ConfGorush.Core.Mode)
	suite.False(suite.ConfGorush.Core.Sync)
	suite.Empty(suite.ConfGorush.Core.FeedbackURL)
	suite.Equal(int64(10), suite.ConfGorush.Core.FeedbackTimeout)
	suite.Len(suite.ConfGorush.Core.FeedbackHeader, 1)
	suite.Equal(
		"x-gorush-token:4e989115e09680f44a645519fed6a976",
		suite.ConfGorush.Core.FeedbackHeader[0],
	)
	suite.False(suite.ConfGorush.Core.SSL)
	suite.Equal("cert.pem", suite.ConfGorush.Core.CertPath)
	suite.Equal("key.pem", suite.ConfGorush.Core.KeyPath)
	suite.Empty(suite.ConfGorush.Core.CertBase64)
	suite.Empty(suite.ConfGorush.Core.KeyBase64)
	suite.Equal(int64(100), suite.ConfGorush.Core.MaxNotification)
	suite.Empty(suite.ConfGorush.Core.HTTPProxy)
	// Pid
	suite.False(suite.ConfGorush.Core.PID.Enabled)
	suite.Equal("gorush.pid", suite.ConfGorush.Core.PID.Path)
	suite.True(suite.ConfGorush.Core.PID.Override)
	suite.False(suite.ConfGorush.Core.AutoTLS.Enabled)
	suite.Equal(".cache", suite.ConfGorush.Core.AutoTLS.Folder)
	suite.Empty(suite.ConfGorush.Core.AutoTLS.Host)

	// Api
	suite.Equal("/api/push", suite.ConfGorush.API.PushURI)
	suite.Equal("/api/stat/go", suite.ConfGorush.API.StatGoURI)
	suite.Equal("/api/stat/app", suite.ConfGorush.API.StatAppURI)
	suite.Equal("/api/config", suite.ConfGorush.API.ConfigURI)
	suite.Equal("/sys/stats", suite.ConfGorush.API.SysStatURI)
	suite.Equal("/metrics", suite.ConfGorush.API.MetricURI)
	suite.Equal("/healthz", suite.ConfGorush.API.HealthURI)

	// Android
	suite.True(suite.ConfGorush.Android.Enabled)
	suite.Equal("key.json", suite.ConfGorush.Android.KeyPath)
	suite.Equal("CREDENTIAL_JSON_DATA", suite.ConfGorush.Android.Credential)
	suite.Equal(0, suite.ConfGorush.Android.MaxRetry)

	// iOS
	suite.False(suite.ConfGorush.Ios.Enabled)
	suite.Equal("key.pem", suite.ConfGorush.Ios.KeyPath)
	suite.Empty(suite.ConfGorush.Ios.KeyBase64)
	suite.Equal("pem", suite.ConfGorush.Ios.KeyType)
	suite.Empty(suite.ConfGorush.Ios.Password)
	suite.False(suite.ConfGorush.Ios.Production)
	suite.Equal(uint(100), suite.ConfGorush.Ios.MaxConcurrentPushes)
	suite.Equal(0, suite.ConfGorush.Ios.MaxRetry)
	suite.Empty(suite.ConfGorush.Ios.KeyID)
	suite.Empty(suite.ConfGorush.Ios.TeamID)

	// log
	suite.Equal("string", suite.ConfGorush.Log.Format)
	suite.Equal("stdout", suite.ConfGorush.Log.AccessLog)
	suite.Equal("debug", suite.ConfGorush.Log.AccessLevel)
	suite.Equal("stderr", suite.ConfGorush.Log.ErrorLog)
	suite.Equal("error", suite.ConfGorush.Log.ErrorLevel)
	suite.True(suite.ConfGorush.Log.HideToken)

	suite.Equal("memory", suite.ConfGorush.Stat.Engine)
	suite.False(suite.ConfGorush.Stat.Redis.Cluster)
	suite.Equal("localhost:6379", suite.ConfGorush.Stat.Redis.Addr)
	suite.Empty(suite.ConfGorush.Stat.Redis.Username)
	suite.Empty(suite.ConfGorush.Stat.Redis.Password)
	suite.Equal(0, suite.ConfGorush.Stat.Redis.DB)

	suite.Equal("bolt.db", suite.ConfGorush.Stat.BoltDB.Path)
	suite.Equal("gorush", suite.ConfGorush.Stat.BoltDB.Bucket)

	suite.Equal("bunt.db", suite.ConfGorush.Stat.BuntDB.Path)
	suite.Equal("level.db", suite.ConfGorush.Stat.LevelDB.Path)
	suite.Equal("badger.db", suite.ConfGorush.Stat.BadgerDB.Path)

	// gRPC
	suite.False(suite.ConfGorush.GRPC.Enabled)
	suite.Equal("9000", suite.ConfGorush.GRPC.Port)
}

func TestConfigTestSuite(t *testing.T) {
	suite.Run(t, new(ConfigTestSuite))
}

func TestLoadConfigFromEnv(t *testing.T) {
	t.Setenv("GORUSH_CORE_PORT", "9001")
	t.Setenv("GORUSH_GRPC_ENABLED", "true")
	t.Setenv("GORUSH_CORE_MAX_NOTIFICATION", "200")
	t.Setenv("GORUSH_IOS_KEY_ID", "ABC123DEFG")
	t.Setenv("GORUSH_IOS_TEAM_ID", "DEF123GHIJ")
	t.Setenv("GORUSH_API_HEALTH_URI", "/healthz")
	t.Setenv("GORUSH_CORE_FEEDBACK_HOOK_URL", "http://example.com")
	t.Setenv("GORUSH_CORE_FEEDBACK_HEADER", "x-api-key:1234567890 x-auth-key:0987654321")

	ConfGorush, err := LoadConf("testdata/config.yml")
	if err != nil {
		panic("failed to load config.yml from file")
	}
	assert.Equal(t, "9001", ConfGorush.Core.Port)
	assert.Equal(t, int64(200), ConfGorush.Core.MaxNotification)
	assert.True(t, ConfGorush.GRPC.Enabled)
	assert.Equal(t, "ABC123DEFG", ConfGorush.Ios.KeyID)
	assert.Equal(t, "DEF123GHIJ", ConfGorush.Ios.TeamID)
	assert.Equal(t, "/healthz", ConfGorush.API.HealthURI)
	assert.Equal(t, "http://example.com", ConfGorush.Core.FeedbackURL)
	assert.Equal(t, "x-api-key:1234567890", ConfGorush.Core.FeedbackHeader[0])
	assert.Equal(t, "x-auth-key:0987654321", ConfGorush.Core.FeedbackHeader[1])
}

func TestRedisDBConfiguration(t *testing.T) {
	// Test loading Redis DB configuration from file
	conf, err := LoadConf("testdata/redis_db_config.yml")
	if err != nil {
		t.Fatalf("failed to load redis_db_config.yml: %v", err)
	}

	// Test queue.redis.db is properly loaded
	assert.Equal(t, "redis", conf.Queue.Engine)
	assert.Equal(t, 5, conf.Queue.Redis.DB)

	// Test stat.redis.db is properly loaded
	assert.Equal(t, "redis", conf.Stat.Engine)
	assert.Equal(t, 3, conf.Stat.Redis.DB)
}

func TestRedisDBConfigurationFromEnv(t *testing.T) {
	// Test loading Redis DB configuration from environment variables
	t.Setenv("GORUSH_QUEUE_REDIS_DB", "7")
	t.Setenv("GORUSH_STAT_REDIS_DB", "9")

	conf, err := LoadConf()
	if err != nil {
		t.Fatalf("failed to load config: %v", err)
	}

	// Test queue.redis.db is properly loaded from env
	assert.Equal(t, 7, conf.Queue.Redis.DB)

	// Test stat.redis.db is properly loaded from env
	assert.Equal(t, 9, conf.Stat.Redis.DB)
}

func TestLoadWrongDefaultYAMLConfig(t *testing.T) {
	// Backup original defaultConf
	originalDefaultConf := defaultConf
	defer func() {
		defaultConf = originalDefaultConf
	}()

	defaultConf = []byte(`a`)
	_, err := LoadConf()
	assert.Error(t, err)
}

func TestValidatePort(t *testing.T) {
	tests := []struct {
		name    string
		port    string
		wantErr bool
		errMsg  string
	}{
		{
			name:    "empty port should be valid",
			port:    "",
			wantErr: false,
		},
		{
			name:    "valid port 80",
			port:    "80",
			wantErr: false,
		},
		{
			name:    "valid port 8080",
			port:    "8080",
			wantErr: false,
		},
		{
			name:    "valid port 65535",
			port:    "65535",
			wantErr: false,
		},
		{
			name:    "valid port 1",
			port:    "1",
			wantErr: false,
		},
		{
			name:    "invalid port 0",
			port:    "0",
			wantErr: true,
			errMsg:  "port out of range",
		},
		{
			name:    "invalid port 65536",
			port:    "65536",
			wantErr: true,
			errMsg:  "port out of range",
		},
		{
			name:    "invalid port format",
			port:    "abc",
			wantErr: true,
			errMsg:  "invalid port format",
		},
		{
			name:    "invalid port with injection",
			port:    "80;rm -rf /",
			wantErr: true,
			errMsg:  "invalid port format",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidatePort(tt.port)
			if tt.wantErr {
				if err == nil {
					t.Errorf("ValidatePort() expected error but got none")
					return
				}
				if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
					t.Errorf("ValidatePort() error = %v, want error containing %v", err, tt.errMsg)
				}
			} else if err != nil {
				t.Errorf("ValidatePort() error = %v, want nil", err)
			}
		})
	}
}

func TestValidateAddress(t *testing.T) {
	tests := []struct {
		name    string
		addr    string
		wantErr bool
		errMsg  string
	}{
		{
			name:    "empty address should be valid",
			addr:    "",
			wantErr: false,
		},
		{
			name:    "valid IPv4 localhost",
			addr:    "127.0.0.1",
			wantErr: false,
		},
		{
			name:    "valid IPv4 all interfaces",
			addr:    "0.0.0.0",
			wantErr: false,
		},
		{
			name:    "valid IPv6 localhost",
			addr:    "::1",
			wantErr: false,
		},
		{
			name:    "valid hostname",
			addr:    "localhost",
			wantErr: false,
		},
		{
			name:    "invalid address too long",
			addr:    strings.Repeat("a", 254),
			wantErr: true,
			errMsg:  "invalid address format",
		},
		{
			name:    "invalid address with double dots",
			addr:    "test..example.com",
			wantErr: true,
			errMsg:  "invalid address format",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidateAddress(tt.addr)
			if tt.wantErr {
				if err == nil {
					t.Errorf("ValidateAddress() expected error but got none")
					return
				}
				if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
					t.Errorf(
						"ValidateAddress() error = %v, want error containing %v",
						err,
						tt.errMsg,
					)
				}
			} else if err != nil {
				t.Errorf("ValidateAddress() error = %v, want nil", err)
			}
		})
	}
}

func TestValidatePIDPath(t *testing.T) {
	tests := []struct {
		name    string
		pidPath string
		wantErr bool
		errMsg  string
	}{
		{
			name:    "empty path should be valid",
			pidPath: "",
			wantErr: false,
		},
		{
			name:    "valid relative path",
			pidPath: "gorush.pid",
			wantErr: false,
		},
		{
			name:    "valid absolute path in tmp",
			pidPath: "/tmp/gorush.pid",
			wantErr: false,
		},
		{
			name:    "valid absolute path with .. components (cleaned)",
			pidPath: "/tmp/foo/../gorush.pid",
			wantErr: false,
		},
		{
			name:    "valid absolute path with multiple .. components",
			pidPath: "/home/user/subdir/../gorush.pid",
			wantErr: false,
		},
		{
			name:    "relative path traversal attack",
			pidPath: "../../../etc/passwd",
			wantErr: true,
			errMsg:  "path traversal detected",
		},
		{
			name:    "simple relative traversal",
			pidPath: "../gorush.pid",
			wantErr: true,
			errMsg:  "path traversal detected",
		},
		{
			name:    "complex relative traversal",
			pidPath: "subdir/../../gorush.pid",
			wantErr: true,
			errMsg:  "path traversal detected",
		},
		{
			name:    "attempt to write to /etc",
			pidPath: "/etc/gorush.pid",
			wantErr: true,
			errMsg:  "sensitive directory",
		},
		{
			name:    "attempt to write to /usr",
			pidPath: "/usr/bin/gorush.pid",
			wantErr: true,
			errMsg:  "sensitive directory",
		},
		{
			name:    "attempt to write to /var",
			pidPath: "/var/log/gorush.pid",
			wantErr: true,
			errMsg:  "sensitive directory",
		},
		{
			name:    "attempt to write to /sys",
			pidPath: "/sys/gorush.pid",
			wantErr: true,
			errMsg:  "sensitive directory",
		},
		{
			name:    "attempt to write to /proc",
			pidPath: "/proc/gorush.pid",
			wantErr: true,
			errMsg:  "sensitive directory",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidatePIDPath(tt.pidPath)
			if tt.wantErr {
				if err == nil {
					t.Errorf("ValidatePIDPath() expected error but got none")
					return
				}
				if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
					t.Errorf(
						"ValidatePIDPath() error = %v, want error containing %v",
						err,
						tt.errMsg,
					)
				}
			} else if err != nil {
				t.Errorf("ValidatePIDPath() error = %v, want nil", err)
			}
		})
	}
}

func TestValidateConfig(t *testing.T) {
	tests := []struct {
		name    string
		cfg     *ConfYaml
		wantErr bool
		errMsg  string
	}{
		{
			name: "valid config",
			cfg: &ConfYaml{
				Core: SectionCore{
					Port:    "8088",
					Address: "0.0.0.0",
				},
				Stat: SectionStat{
					Engine: "memory",
				},
			},
			wantErr: false,
		},
		{
			name: "invalid port in config",
			cfg: &ConfYaml{
				Core: SectionCore{
					Port: "99999",
				},
			},
			wantErr: true,
			errMsg:  "invalid core port",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidateConfig(tt.cfg)
			if tt.wantErr {
				if err == nil {
					t.Errorf("ValidateConfig() expected error but got none")
					return
				}

				if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
					t.Errorf(
						"ValidateConfig() error = %v, want error containing %v",
						err,
						tt.errMsg,
					)
				}
			} else if err != nil {
				t.Errorf("ValidateConfig() error = %v, want nil", err)
			}
		})
	}
}

// Benchmark tests for security validation functions
func BenchmarkValidatePort(b *testing.B) {
	for b.Loop() {
		_ = ValidatePort("8080")
	}
}

func BenchmarkValidateAddress(b *testing.B) {
	for b.Loop() {
		_ = ValidateAddress("127.0.0.1")
	}
}

func BenchmarkValidatePIDPath(b *testing.B) {
	for b.Loop() {
		_ = ValidatePIDPath("/tmp/gorush.pid")
	}
}

// Integration test for security validation
func TestSecurityValidationIntegration(t *testing.T) {
	// Test that all validation functions work together
	t.Run("complete security validation", func(t *testing.T) {
		// Test valid inputs
		if err := ValidatePort("8088"); err != nil {
			t.Errorf("Valid port should not error: %v", err)
		}

		if err := ValidateAddress("127.0.0.1"); err != nil {
			t.Errorf("Valid address should not error: %v", err)
		}

		if err := ValidatePIDPath("/tmp/test.pid"); err != nil {
			t.Errorf("Valid PID path should not error: %v", err)
		}

		// Test malicious inputs
		if err := ValidatePort("8080; rm -rf /"); err == nil {
			t.Error("Malicious port should error")
		}

		if err := ValidatePIDPath("../../../etc/passwd"); err == nil {
			t.Error("Path traversal should error")
		}

		if err := ValidatePIDPath("/etc/malicious.pid"); err == nil {
			t.Error("Sensitive directory write should error")
		}
	})
}

func TestAPIDefaultsFromEnv(t *testing.T) {
	// Test that API endpoint defaults can be overridden by environment variables
	t.Setenv("GORUSH_API_PUSH_URI", "/custom/push")
	t.Setenv("GORUSH_API_STAT_GO_URI", "/custom/stat/go")
	t.Setenv("GORUSH_API_METRIC_URI", "/custom/metrics")

	conf, err := LoadConf()
	if err != nil {
		t.Fatalf("failed to load config: %v", err)
	}

	assert.Equal(t, "/custom/push", conf.API.PushURI)
	assert.Equal(t, "/custom/stat/go", conf.API.StatGoURI)
	assert.Equal(t, "/custom/metrics", conf.API.MetricURI)
}

func TestLogDefaultsFromEnv(t *testing.T) {
	// Test that log level defaults can be overridden by environment variables
	t.Setenv("GORUSH_LOG_ACCESS_LEVEL", "info")
	t.Setenv("GORUSH_LOG_ERROR_LEVEL", "warn")
	t.Setenv("GORUSH_LOG_FORMAT", "json")

	conf, err := LoadConf()
	if err != nil {
		t.Fatalf("failed to load config: %v", err)
	}

	assert.Equal(t, "info", conf.Log.AccessLevel)
	assert.Equal(t, "warn", conf.Log.ErrorLevel)
	assert.Equal(t, "json", conf.Log.Format)
}

func TestLogLevelDefaultsWhenEmpty(t *testing.T) {
	// Test that when no log level is specified, defaults are used
	// This was the original bug - empty log levels caused initialization to fail
	conf, err := LoadConf()
	if err != nil {
		t.Fatalf("failed to load config: %v", err)
	}

	// Verify defaults are set
	assert.NotEmpty(t, conf.Log.AccessLevel, "access level should have default value")
	assert.NotEmpty(t, conf.Log.ErrorLevel, "error level should have default value")
	assert.Equal(t, "debug", conf.Log.AccessLevel)
	assert.Equal(t, "error", conf.Log.ErrorLevel)
}

func TestAllAPIEndpointsHaveDefaults(t *testing.T) {
	// Test that all API endpoints have proper defaults
	conf, err := LoadConf()
	if err != nil {
		t.Fatalf("failed to load config: %v", err)
	}

	// Verify all API endpoints have non-empty defaults
	assert.NotEmpty(t, conf.API.PushURI, "push_uri should have default")
	assert.NotEmpty(t, conf.API.StatGoURI, "stat_go_uri should have default")
	assert.NotEmpty(t, conf.API.StatAppURI, "stat_app_uri should have default")
	assert.NotEmpty(t, conf.API.ConfigURI, "config_uri should have default")
	assert.NotEmpty(t, conf.API.SysStatURI, "sys_stat_uri should have default")
	assert.NotEmpty(t, conf.API.MetricURI, "metric_uri should have default")
	assert.NotEmpty(t, conf.API.HealthURI, "health_uri should have default")

	// Verify correct default values
	assert.Equal(t, "/api/push", conf.API.PushURI)
	assert.Equal(t, "/api/stat/go", conf.API.StatGoURI)
	assert.Equal(t, "/api/stat/app", conf.API.StatAppURI)
	assert.Equal(t, "/api/config", conf.API.ConfigURI)
	assert.Equal(t, "/sys/stats", conf.API.SysStatURI)
	assert.Equal(t, "/metrics", conf.API.MetricURI)
	assert.Equal(t, "/healthz", conf.API.HealthURI)
}

func TestSanitizedCopy(t *testing.T) {
	cfg := &ConfYaml{}
	cfg.Core.CertBase64 = "cert-data"
	cfg.Core.KeyBase64 = "key-data"
	cfg.Core.HTTPProxy = "http://proxy:8080"
	cfg.Core.CertPath = "/path/to/cert.pem"
	cfg.Core.KeyPath = "/path/to/key.pem"
	cfg.Core.FeedbackHeader = []string{"x-api-key:secret123", "x-auth-key:secret456"}
	cfg.Android.KeyPath = "/path/to/key.json"
	cfg.Android.Credential = "fcm-credential"
	cfg.Huawei.AppSecret = "hms-secret"
	cfg.Ios.KeyPath = "/path/to/ios.p8"
	cfg.Ios.KeyBase64 = "ios-key-data"
	cfg.Ios.Password = "ios-password"
	cfg.Ios.KeyID = "ABCDE12345"
	cfg.Ios.TeamID = "TEAM123456"
	cfg.Queue.Redis.Username = "queue-user"
	cfg.Queue.Redis.Password = "queue-pass"
	cfg.Stat.Redis.Username = "stat-user"
	cfg.Stat.Redis.Password = "stat-pass"

	// Set a non-sensitive field to verify it's preserved
	cfg.Core.Port = "8088"

	sanitized := cfg.SanitizedCopy()

	// All sensitive fields should be redacted
	assert.Equal(t, "[REDACTED]", sanitized.Core.CertBase64)
	assert.Equal(t, "[REDACTED]", sanitized.Core.KeyBase64)
	assert.Equal(t, "[REDACTED]", sanitized.Core.HTTPProxy)
	assert.Equal(t, "[REDACTED]", sanitized.Core.CertPath)
	assert.Equal(t, "[REDACTED]", sanitized.Core.KeyPath)
	assert.Len(t, sanitized.Core.FeedbackHeader, 2)
	assert.Equal(t, "[REDACTED]", sanitized.Core.FeedbackHeader[0])
	assert.Equal(t, "[REDACTED]", sanitized.Core.FeedbackHeader[1])
	assert.Equal(t, "[REDACTED]", sanitized.Android.KeyPath)
	assert.Equal(t, "[REDACTED]", sanitized.Android.Credential)
	assert.Equal(t, "[REDACTED]", sanitized.Huawei.AppSecret)
	assert.Equal(t, "[REDACTED]", sanitized.Ios.KeyPath)
	assert.Equal(t, "[REDACTED]", sanitized.Ios.KeyBase64)
	assert.Equal(t, "[REDACTED]", sanitized.Ios.Password)
	assert.Equal(t, "[REDACTED]", sanitized.Ios.KeyID)
	assert.Equal(t, "[REDACTED]", sanitized.Ios.TeamID)
	assert.Equal(t, "[REDACTED]", sanitized.Queue.Redis.Username)
	assert.Equal(t, "[REDACTED]", sanitized.Queue.Redis.Password)
	assert.Equal(t, "[REDACTED]", sanitized.Stat.Redis.Username)
	assert.Equal(t, "[REDACTED]", sanitized.Stat.Redis.Password)

	// Non-sensitive fields should be preserved
	assert.Equal(t, "8088", sanitized.Core.Port)

	// Original config must NOT be modified
	assert.Equal(t, "/path/to/cert.pem", cfg.Core.CertPath)
	assert.Equal(t, "/path/to/key.pem", cfg.Core.KeyPath)
	assert.Equal(t, "x-api-key:secret123", cfg.Core.FeedbackHeader[0])
	assert.Equal(t, "cert-data", cfg.Core.CertBase64)
	assert.Equal(t, "fcm-credential", cfg.Android.Credential)
	assert.Equal(t, "ios-password", cfg.Ios.Password)
	assert.Equal(t, "stat-pass", cfg.Stat.Redis.Password)
}

func TestSanitizedCopyEmptyFields(t *testing.T) {
	cfg := &ConfYaml{}
	// All sensitive fields are empty by default

	sanitized := cfg.SanitizedCopy()

	// Empty fields should remain empty, not become "[REDACTED]"
	assert.Empty(t, sanitized.Core.CertBase64)
	assert.Empty(t, sanitized.Core.KeyBase64)
	assert.Empty(t, sanitized.Core.HTTPProxy)
	assert.Empty(t, sanitized.Core.CertPath)
	assert.Empty(t, sanitized.Core.KeyPath)
	assert.Nil(t, sanitized.Core.FeedbackHeader)
	assert.Empty(t, sanitized.Android.KeyPath)
	assert.Empty(t, sanitized.Android.Credential)
	assert.Empty(t, sanitized.Huawei.AppSecret)
	assert.Empty(t, sanitized.Ios.KeyPath)
	assert.Empty(t, sanitized.Ios.KeyBase64)
	assert.Empty(t, sanitized.Ios.Password)
	assert.Empty(t, sanitized.Ios.KeyID)
	assert.Empty(t, sanitized.Ios.TeamID)
	assert.Empty(t, sanitized.Queue.Redis.Username)
	assert.Empty(t, sanitized.Queue.Redis.Password)
	assert.Empty(t, sanitized.Stat.Redis.Username)
	assert.Empty(t, sanitized.Stat.Redis.Password)
}


================================================
FILE: config/testdata/empty.yml
================================================


================================================
FILE: config/testdata/redis_db_config.yml
================================================
queue:
  engine: "redis"
  redis:
    addr: 127.0.0.1:6379
    group: gorush
    consumer: gorush
    stream_name: gorush
    username: ""
    password: ""
    with_tls: false
    db: 5

stat:
  engine: "redis"
  redis:
    cluster: false
    addr: "localhost:6379"
    username: ""
    password: ""
    db: 3


================================================
FILE: contrib/init/debian/README.md
================================================
# Run gorush in Debian/Ubuntu

## Installation

Put `gorush` binary into `/usr/bin` folder.

```sh
cp gorush /usr/bin/
chmod +x /usr/bin/gorush
```

put `gorush` init script into `/etc/rc.d`

```sh
cp contrib/init/debian/gorush /etc.rc.d/
```

install and remove System-V style init script links

```sh
update-rc.d gorush start 20 2 3 4 5 . stop 80 0 1 6 .
```

## Start service

create gorush configuration file.

```sh
mkdir -p /etc/gorush
cp config/testdata/config.yml /etc/gorush/
```

start gorush service.

```sh
/etc/init.d/gorush start
```


================================================
FILE: contrib/init/debian/gorush
================================================
#!/bin/sh

### BEGIN INIT INFO
# Provides:          gorush
# Required-Start:    $syslog $network
# Required-Stop:     $syslog $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the gorush web server
# Description:       starts gorush using start-stop-daemon
### END INIT INFO

# Original Author: Bo-Yi Wu (appleboy)

# Do NOT "set -e"
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="the gorush web server"
NAME=gorush
DAEMON=$(which gorush)

DAEMONUSER=www-data
PIDFILE=/var/run/$NAME.pid
CONFIGFILE=/etc/gorush/config.yml
DAEMONOPTS="-c $CONFIGFILE"

USERBIND="setcap cap_net_bind_service=+ep"
STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/5/KILL/5}"

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Set the ulimits
ulimit -n 8192

do_start()
{
  $USERBIND $DAEMON
  sh -c "USER=$DAEMONUSER start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \\
    --background --chuid $DAEMONUSER --exec $DAEMON -- $DAEMONOPTS"
}

do_stop()
{
  start-stop-daemon --stop --quiet --retry=$STOP_SCHEDULE --pidfile $PIDFILE --name $NAME --oknodo
  rm -f $PIDFILE
}

do_status()
{
  if [ -f $PIDFILE ]; then
    if kill -0 $(cat "$PIDFILE"); then
      echo "$NAME is running, PID is $(cat $PIDFILE)"
    else
      echo "$NAME process is dead, but pidfile exists"
    fi
  else
    echo "$NAME is not running"
  fi
}

case "$1" in
  start)
    echo "Starting $DESC" "$NAME"
    do_start
    ;;
  stop)
    echo "Stopping $DESC" "$NAME"
    do_stop
    ;;
  status)
    do_status
    ;;
  restart)
    echo "Restarting $DESC" "$NAME"
    do_stop
    do_start
    ;;
  *)
    echo "Usage: $SCRIPTNAME {start|stop|status|restart}" >&2
    exit 2
    ;;
esac

exit 0


================================================
FILE: core/core.go
================================================
package core

import (
	"encoding/json"
	"errors"
	"strings"
)

// Backward-compatible integer constants kept as-is.
const (
	// PlatFormIos constant is 1 for iOS
	PlatFormIos = iota + 1
	// PlatFormAndroid constant is 2 for Android
	PlatFormAndroid
	// PlatFormHuawei constant is 3 for Huawei
	PlatFormHuawei
)

// Log block string constants (backward-compatible)
const (
	// SucceededPush is log block
	SucceededPush = "succeeded-push"
	// FailedPush is log block
	FailedPush = "failed-push"
)

// Platform is a typed enum for push target platforms.
// This complements the existing integer constants.
type Platform uint8

const (
	// Typed equivalents of platform values.
	PlatformIOS     Platform = Platform(PlatFormIos)
	PlatformAndroid Platform = Platform(PlatFormAndroid)
	PlatformHuawei  Platform = Platform(PlatFormHuawei)
)

// String returns a stable lowercase name for the platform.
func (p Platform) String() string {
	switch p {
	case PlatformIOS:
		return "ios"
	case PlatformAndroid:
		return "android"
	case PlatformHuawei:
		return "huawei"
	default:
		return "unknown"
	}
}

// IsValid reports whether the platform value is supported.
func (p Platform) IsValid() bool {
	return p == PlatformIOS || p == PlatformAndroid || p == PlatformHuawei
}

// ParsePlatform parses a string (case-insensitive) into a Platform.
func ParsePlatform(s string) (Platform, error) {
	switch strings.ToLower(strings.TrimSpace(s)) {
	case "ios":
		return PlatformIOS, nil
	case "android":
		return PlatformAndroid, nil
	case "huawei":
		return PlatformHuawei, nil
	default:
		return 0, errors.New("unknown platform: " + s)
	}
}

// MarshalText encodes Platform as its string form.
func (p Platform) MarshalText() ([]byte, error) {
	if !p.IsValid() {
		return nil, errors.New("invalid platform")
	}
	return []byte(p.String()), nil
}

// UnmarshalText decodes Platform from its string form.
func (p *Platform) UnmarshalText(text []byte) error {
	v, err := ParsePlatform(string(text))
	if err != nil {
		return err
	}
	*p = v
	return nil
}

// MarshalJSON encodes Platform as a JSON string.
func (p Platform) MarshalJSON() ([]byte, error) {
	if !p.IsValid() {
		return nil, errors.New("invalid platform")
	}
	return json.Marshal(p.String())
}

// UnmarshalJSON decodes Platform from a JSON string or legacy number.
func (p *Platform) UnmarshalJSON(data []byte) error {
	// Try string first.
	var s string
	if err := json.Unmarshal(data, &s); err == nil {
		return p.UnmarshalText([]byte(s))
	}
	// Fallback to legacy numeric values 1/2/3.
	var n int
	if err := json.Unmarshal(data, &n); err == nil {
		switch n {
		case PlatFormIos:
			*p = PlatformIOS
			return nil
		case PlatFormAndroid:
			*p = PlatformAndroid
			return nil
		case PlatFormHuawei:
			*p = PlatformHuawei
			return nil
		}
	}
	return errors.New("invalid platform JSON")
}

// LogBlock is a typed alias for log block kinds.
type LogBlock string

const (
	LogSucceededPush LogBlock = LogBlock(SucceededPush)
	LogFailedPush    LogBlock = LogBlock(FailedPush)
)

// IsValid checks a LogBlock value.
func (l LogBlock) IsValid() bool {
	return l == LogSucceededPush || l == LogFailedPush
}


================================================
FILE: core/core_test.go
================================================
package core

import (
	"encoding/json"
	"testing"
)

func TestPlatformStringAndValidity(t *testing.T) {
	cases := []struct {
		p        Platform
		expected string
	}{
		{PlatformIOS, PlatformIOS.String()},
		{PlatformAndroid, PlatformAndroid.String()},
		{PlatformHuawei, PlatformHuawei.String()},
	}

	for _, c := range cases {
		if !c.p.IsValid() {
			t.Fatalf("expected %v to be valid", c.p)
		}
		if got := c.p.String(); got != c.expected {
			t.Fatalf("String() mismatch: got %q want %q", got, c.expected)
		}
	}

	if Platform(0).IsValid() {
		t.Fatalf("expected zero value to be invalid")
	}
}

func TestParsePlatform(t *testing.T) {
	p, err := ParsePlatform(" IOS ")
	if err != nil || p != PlatformIOS {
		t.Fatalf("ParsePlatform ios failed: p=%v err=%v", p, err)
	}
	p, err = ParsePlatform(PlatformAndroid.String())
	if err != nil || p != PlatformAndroid {
		t.Fatalf("ParsePlatform android failed: p=%v err=%v", p, err)
	}
	p, err = ParsePlatform("HuAwEi")
	if err != nil || p != PlatformHuawei {
		t.Fatalf("ParsePlatform huawei failed: p=%v err=%v", p, err)
	}
	if _, err = ParsePlatform("unknown"); err == nil {
		t.Fatalf("expected error for unknown platform")
	}
}

func TestPlatformTextMarshaling(t *testing.T) {
	b, err := PlatformIOS.MarshalText()
	if err != nil || string(b) != PlatformIOS.String() {
		t.Fatalf("MarshalText: got %q err=%v", string(b), err)
	}
	var p Platform
	androidText := []byte(PlatformAndroid.String())
	if err := p.UnmarshalText(androidText); err != nil || p != PlatformAndroid {
		t.Fatalf("UnmarshalText: p=%v err=%v", p, err)
	}
}

func TestPlatformJSONMarshaling(t *testing.T) {
	b, err := json.Marshal(PlatformHuawei)
	expected := `"` + PlatformHuawei.String() + `"`
	if err != nil || string(b) != expected {
		t.Fatalf("MarshalJSON: got %s err=%v", string(b), err)
	}
	var p Platform
	iosJSON := []byte(`"` + PlatformIOS.String() + `"`)
	if err := json.Unmarshal(iosJSON, &p); err != nil || p != PlatformIOS {
		t.Fatalf("UnmarshalJSON string: p=%v err=%v", p, err)
	}
	// legacy numeric values
	if err := json.Unmarshal([]byte("2"), &p); err != nil || p != PlatformAndroid {
		t.Fatalf("UnmarshalJSON legacy number: p=%v err=%v", p, err)
	}
	if err := json.Unmarshal([]byte("99"), &p); err == nil {
		t.Fatalf("expected error for invalid numeric JSON")
	}
}

func TestLogBlock(t *testing.T) {
	if !LogSucceededPush.IsValid() || !LogFailedPush.IsValid() {
		t.Fatalf("expected log blocks to be valid")
	}
	if LogBlock("x").IsValid() {
		t.Fatalf("expected arbitrary value to be invalid")
	}
}


================================================
FILE: core/health.go
================================================
package core

import "context"

// Health defines a health-check connection.
type Health interface {
	// Check returns if server is healthy or not
	Check(c context.Context) (bool, error)
}


================================================
FILE: core/queue.go
================================================
package core

// Queue as backend
type Queue string

var (
	// LocalQueue for channel in Go
	LocalQueue Queue = "local"
	// NSQ a realtime distributed messaging platform
	NSQ Queue = "nsq"
	// NATS Connective Technology for Adaptive Edge & Distributed Systems
	NATS Queue = "nats"
	// Redis Pub/Sub
	Redis Queue = "redis"
)

// IsLocalQueue check is Local Queue
func IsLocalQueue(q Queue) bool {
	return q == LocalQueue
}


================================================
FILE: core/storage.go
================================================
package core

const (
	// TotalCountKey is key name for total count of storage
	TotalCountKey = "gorush-total-count"

	// IosSuccessKey is key name or ios success count of storage
	/* #nosec */
	IosSuccessKey = "gorush-ios-success-count"

	// IosErrorKey is key name or ios success error of storage
	IosErrorKey = "gorush-ios-error-count"

	// AndroidSuccessKey is key name for android success count of storage
	AndroidSuccessKey = "gorush-android-success-count"

	// AndroidErrorKey is key name for android error count of storage
	AndroidErrorKey = "gorush-android-error-count"

	// HuaweiSuccessKey is key name for huawei success count of storage
	HuaweiSuccessKey = "gorush-huawei-success-count"

	// HuaweiErrorKey is key name for huawei error count of storage
	HuaweiErrorKey = "gorush-huawei-error-count"
)

// Storage interface
type Storage interface {
	Init() error
	Add(key string, count int64)
	Set(key string, count int64)
	Get(key string) int64
	Close() error
}


================================================
FILE: doc.go
================================================
// A push notification server using Gin framework written in Go (Golang).
//
// Details about the gorush project are found in github page:
//
//	https://github.com/appleboy/gorush
//
// The pre-compiled binaries can be downloaded from release page.
//
// Send Android notification
//
//	$ gorush -android -m="your message" --fcm-key="FCM Key Path" -t="Device token"
//
// Send iOS notification
//
//	$ gorush -ios -m="your message" -i="API Key" -t="Device token"
//
// The default endpoint is APNs development. Please add -production flag for APNs production push endpoint.
//
//	$ gorush -ios -m="your message" -i="API Key" -t="Device token" -production
//
// Run gorush web server
//
//	$ gorush -c config.yml
//
// Get go status of api server using httpie tool:
//
//	$ http -v --verify=no --json GET https://localhost:8088/api/stat/go
//
// Simple send iOS notification example, the platform value is 1:
//
//	{
//	  "notifications": [
//	    {
//	      "tokens": ["token_a", "token_b"],
//	      "platform": 1,
//	      "message": "Hello World iOS!"
//	    }
//	  ]
//	}
//
// Simple send Android notification example, the platform value is 2:
//
//	{
//	  "notifications": [
//	    {
//	      "tokens": ["token_a", "token_b"],
//	      "platform": 2,
//	      "message": "Hello World Android!"
//	    }
//	  ]
//	}
//
// For more details, see the documentation and example.
package main


================================================
FILE: docker/Dockerfile
================================================
FROM alpine:3.22

ARG TARGETOS
ARG TARGETARCH
ARG USER=gorush
ENV HOME /home/$USER

LABEL maintainer="Bo-Yi Wu <appleboy.tw@gmail.com>" \
  org.label-schema.name="Gorush" \
  org.label-schema.vendor="Bo-Yi Wu" \
  org.label-schema.schema-version="1.0"

# add new user
RUN adduser -D $USER
RUN apk add --no-cache ca-certificates mailcap && \
  rm -rf /var/cache/apk/*

COPY release/${TARGETOS}/${TARGETARCH}/gorush /bin/

USER $USER
WORKDIR $HOME

EXPOSE 8088 9000
HEALTHCHECK --start-period=1s --interval=10s --timeout=5s \
  CMD ["/bin/gorush", "--ping"]

ENTRYPOINT ["/bin/gorush"]


================================================
FILE: docker-compose.yml
================================================
version: '3'

services:
  gorush:
    image: appleboy/gorush
    restart: always
    ports:
      - "8088:8088"
      - "9000:9000"
    logging:
      options:
        max-size: "100k"
        max-file: "3"
    environment:
      - GORUSH_CORE_QUEUE_NUM=512


================================================
FILE: go.mod
================================================
module github.com/appleboy/gorush

go 1.25.0

replace github.com/msalihkarakasli/go-hms-push => github.com/spawn2kill/go-hms-push v0.0.0-20211125124117-e20af53b1304

require (
	firebase.google.com/go/v4 v4.19.0
	github.com/apex/gateway v1.1.2
	github.com/appleboy/gin-status-api v1.2.0
	github.com/appleboy/go-fcm v1.2.7
	github.com/appleboy/go-hms-push v1.0.1
	github.com/appleboy/gofight/v2 v2.2.1
	github.com/appleboy/graceful v1.3.0
	github.com/asdine/storm/v3 v3.2.1
	github.com/buger/jsonparser v1.1.1
	github.com/dgraph-io/badger/v4 v4.9.1
	github.com/gin-contrib/logger v1.2.6
	github.com/gin-gonic/gin v1.12.0
	github.com/golang-queue/nats v0.2.0
	github.com/golang-queue/nsq v0.3.0
	github.com/golang-queue/queue v0.5.0
	github.com/golang-queue/redisdb-stream v0.3.1
	github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
	github.com/json-iterator/go v1.1.12
	github.com/mattn/go-isatty v0.0.20
	github.com/mitchellh/mapstructure v1.5.0
	github.com/prometheus/client_golang v1.23.2
	github.com/redis/go-redis/v9 v9.18.0
	github.com/rs/zerolog v1.34.0
	github.com/sideshow/apns2 v0.25.0
	github.com/sirupsen/logrus v1.9.4
	github.com/spf13/viper v1.21.0
	github.com/stretchr/testify v1.11.1
	github.com/syndtr/goleveldb v1.0.0
	github.com/thoas/stats v0.0.0-20190407194641-965cb2de1678
	github.com/tidwall/buntdb v1.3.2
	go.opencensus.io v0.24.0
	go.uber.org/atomic v1.11.0
	golang.org/x/crypto v0.48.0
	golang.org/x/net v0.51.0
	golang.org/x/sync v0.20.0
	google.golang.org/grpc v1.79.2
	google.golang.org/protobuf v1.36.11
)

require (
	cel.dev/expr v0.25.1 // indirect
	cloud.google.com/go v0.123.0 // indirect
	cloud.google.com/go/auth v0.18.2 // indirect
	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
	cloud.google.com/go/compute/metadata v0.9.0 // indirect
	cloud.google.com/go/firestore v1.21.0 // indirect
	cloud.google.com/go/iam v1.5.3 // indirect
	cloud.google.com/go/longrunning v0.8.0 // indirect
	cloud.google.com/go/monitoring v1.24.3 // indirect
	cloud.google.com/go/storage v1.61.0 // indirect
	github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
	github.com/MicahParks/keyfunc v1.9.0 // indirect
	github.com/appleboy/com v1.2.0 // indirect
	github.com/aws/aws-lambda-go v1.53.0 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/bytedance/gopkg v0.1.3 // indirect
	github.com/bytedance/sonic v1.15.0 // indirect
	github.com/bytedance/sonic/loader v0.5.0 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/cloudwego/base64x v0.1.6 // indirect
	github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
	github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/fukata/golang-stats-api-handler v1.0.0 // indirect
	github.com/gabriel-vasile/mimetype v1.4.13 // indirect
	github.com/gin-contrib/sse v1.1.0 // indirect
	github.com/go-jose/go-jose/v4 v4.1.3 // indirect
	github.com/go-logr/logr v1.4.3 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.30.1 // indirect
	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
	github.com/goccy/go-json v0.10.5 // indirect
	github.com/goccy/go-yaml v1.19.2 // indirect
	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
	github.com/golang/protobuf v1.5.4 // indirect
	github.com/golang/snappy v1.0.0 // indirect
	github.com/google/flatbuffers v25.12.19+incompatible // indirect
	github.com/google/s2a-go v0.1.9 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
	github.com/googleapis/gax-go/v2 v2.17.0 // indirect
	github.com/jpillora/backoff v1.0.0 // indirect
	github.com/klauspost/compress v1.18.4 // indirect
	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
	github.com/leodido/go-urn v1.4.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/nats-io/nats.go v1.49.0 // indirect
	github.com/nats-io/nkeys v0.4.15 // indirect
	github.com/nats-io/nuid v1.0.1 // indirect
	github.com/nsqio/go-nsq v1.1.0 // indirect
	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/prometheus/client_model v0.6.2 // indirect
	github.com/prometheus/common v0.67.5 // indirect
	github.com/prometheus/procfs v0.20.1 // indirect
	github.com/quic-go/qpack v0.6.0 // indirect
	github.com/quic-go/quic-go v0.59.0 // indirect
	github.com/sagikazarmark/locafero v0.12.0 // indirect
	github.com/spf13/afero v1.15.0 // indirect
	github.com/spf13/cast v1.10.0 // indirect
	github.com/spf13/pflag v1.0.10 // indirect
	github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
	github.com/subosito/gotenv v1.6.0 // indirect
	github.com/tidwall/btree v1.8.1 // indirect
	github.com/tidwall/gjson v1.18.0 // indirect
	github.com/tidwall/grect v0.1.4 // indirect
	github.com/tidwall/match v1.2.0 // indirect
	github.com/tidwall/pretty v1.2.1 // indirect
	github.com/tidwall/rtred v0.1.2 // indirect
	github.com/tidwall/tinyqueue v0.1.1 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.3.1 // indirect
	go.etcd.io/bbolt v1.4.3 // indirect
	go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
	go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
	go.opentelemetry.io/otel v1.42.0 // indirect
	go.opentelemetry.io/otel/metric v1.42.0 // indirect
	go.opentelemetry.io/otel/sdk v1.42.0 // indirect
	go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
	go.opentelemetry.io/otel/trace v1.42.0 // indirect
	go.yaml.in/yaml/v2 v2.4.4 // indirect
	go.yaml.in/yaml/v3 v3.0.4 // indirect
	golang.org/x/arch v0.25.0 // indirect
	golang.org/x/oauth2 v0.36.0 // indirect
	golang.org/x/sys v0.42.0 // indirect
	golang.org/x/text v0.34.0 // indirect
	golang.org/x/time v0.15.0 // indirect
	google.golang.org/api v0.270.0 // indirect
	google.golang.org/appengine/v2 v2.0.6 // indirect
	google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect
	google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM=
cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.61.0 h1:8NGccs4oDZTqV1nBlom0CVJewloINXYW5Z0LoFqaVeI=
cloud.google.com/go/storage v1.61.0/go.mod h1:IvExELZv/uJe/DAzLgPeKNT8dm5+DM5gO0H1bkubD6Y=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=
firebase.google.com/go/v4 v4.19.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/apex/gateway v1.1.2 h1:OWyLov8eaau8YhkYKkRuOAYqiUhpBJalBR1o+3FzX+8=
github.com/apex/gateway v1.1.2/go.mod h1:AMTkVbz5u5Hvd6QOGhhg0JUrNgCcLVu3XNJOGntdoB4=
github.com/appleboy/com v1.2.0 h1:3jyA+yVofe/uzPHHa7Xrsj7rnDy1sZn/8pYHdzHB3GQ=
github.com/appleboy/com v1.2.0/go.mod h1:XK2kV+JWz/gkzsDPotNJL+aS6XCy5GNlbiTWGvIhqIU=
github.com/appleboy/gin-status-api v1.2.0 h1:v5FpDfgf94z7eFniW87cPHytbfzuM7Km2vfY/BHjAzw=
github.com/appleboy/gin-status-api v1.2.0/go.mod h1:WQFLh5VvFDz8fUPv66XYUeZ2nHEoPa6qZelLZ+9ARsg=
github.com/appleboy/go-fcm v1.2.7 h1:1HFLCSH/HyAPOTVsmWCL1UPSJ+lKr5t60kCXd2F4KlY=
github.com/appleboy/go-fcm v1.2.7/go.mod h1:HbdeRIVWOdoHHTV9TXkEQKa6f9Bg+xJSjgUCYXAjPwU=
github.com/appleboy/go-hms-push v1.0.1 h1:55v173UvjgZgmaj/D+X+y82WLuA+xl4SYHCF8Zb5WtM=
github.com/appleboy/go-hms-push v1.0.1/go.mod h1:JZzS7XUOFTUV8y+MAnDjOIv0LCZmLmVpJj//Q9t0XBw=
github.com/appleboy/gofight/v2 v2.2.1 h1:OOJrZ71tdOFDzyyBvP+h047w0EJHktqTo4mEOTDrKy0=
github.com/appleboy/gofight/v2 v2.2.1/go.mod h1:dOz1A3YtfciapH897IQOAR6JfTFm0nwJctaDuJZiijY=
github.com/appleboy/graceful v1.3.0 h1:IU5In15N4z0qkTAVnuKH/KZv3iat5lsS1pbTsDB1LzU=
github.com/appleboy/graceful v1.3.0/go.mod h1:XlEg3jkgb42weJeCXch3HRXvWrU5r+EYymCyPVy0EkA=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/aws/aws-lambda-go v1.17.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw=
github.com/aws/aws-lambda-go v1.53.0 h1:uAMv6W/vCP/L494BAUSxe+8KVBIPK+SGPyapFt3FuMk=
github.com/aws/aws-lambda-go v1.53.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w=
github.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0=
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fukata/golang-stats-api-handler v1.0.0 h1:N6M25vhs1yAvwGBpFY6oBmMOZeJdcWnvA+wej8pKeko=
github.com/fukata/golang-stats-api-handler v1.0.0/go.mod h1:1sIi4/rHq6s/ednWMZqTmRq3765qTUSs/c3xF6lj8J8=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc=
github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-queue/nats v0.2.0 h1:JGuFxniEM5L7KezEa71OwG+uTsEqA2DM+7L62DzAWEw=
github.com/golang-queue/nats v0.2.0/go.mod h1:rQ33HM1X9Vs4cypzx/XrwMkjumLiwXNfjXXO/+wqAQo=
github.com/golang-queue/nsq v0.3.0 h1:tkf01x06w4KLSE+SHKOISmOBsBck2IkBcurhQxQjTxQ=
github.com/golang-queue/nsq v0.3.0/go.mod h1:tCeoQCEXAWAOmod+mUZWm5Zy8m1PCtmE03BvdkRsPTs=
github.com/golang-queue/queue v0.5.0 h1:j5Qj3h+V9irfbavVkFwxf7TS6/1mtVqsisCSYJ25DAI=
github.com/golang-queue/queue v0.5.0/go.mod h1:BoV3elevg5ksW3mSGKxP2Tu3YKgUONr4pYfWcari5nE=
github.com/golang-queue/redisdb-stream v0.3.1 h1:z4V+csHjc5bF+I2LEPqdYQEqTGQKsrb2w/rC1EcXxQo=
github.com/golang-queue/redisdb-stream v0.3.1/go.mod h1:00bXPJrRnDFaT2KJJ8zYDTbRers/xU8YpfjRRW6w+P8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:i
Download .txt
gitextract_lladjky6/

├── .dockerignore
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql.yaml
│       ├── docker.yaml
│       ├── goreleaser.yml
│       ├── testing.yml
│       └── trivy-scan.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .hadolint.yaml
├── .roomodes
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README.md
├── app/
│   ├── config.go
│   ├── config_test.go
│   ├── options.go
│   ├── options_test.go
│   ├── sender.go
│   ├── sender_test.go
│   ├── worker.go
│   └── worker_test.go
├── certificate/
│   ├── authkey-invalid.p8
│   ├── authkey-valid.p8
│   ├── certificate-valid.p12
│   ├── certificate-valid.pem
│   ├── localhost.cert
│   └── localhost.key
├── config/
│   ├── config.go
│   ├── config_test.go
│   └── testdata/
│       ├── empty.yml
│       └── redis_db_config.yml
├── contrib/
│   └── init/
│       └── debian/
│           ├── README.md
│           └── gorush
├── core/
│   ├── core.go
│   ├── core_test.go
│   ├── health.go
│   ├── queue.go
│   └── storage.go
├── doc.go
├── docker/
│   └── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── helm/
│   └── gorush/
│       ├── .helmignore
│       ├── Chart.yaml
│       ├── templates/
│       │   ├── NOTES.txt
│       │   ├── _helpers.tpl
│       │   ├── configmap.yml
│       │   ├── deployment.yaml
│       │   ├── hpa.yaml
│       │   ├── ingress.yaml
│       │   ├── service.yaml
│       │   └── serviceaccount.yaml
│       └── values.yaml
├── install.sh
├── k8s/
│   ├── gorush-aws-alb-ingress.yaml
│   ├── gorush-configmap.yaml
│   ├── gorush-deployment.yaml
│   ├── gorush-namespace.yaml
│   ├── gorush-redis-deployment.yaml
│   ├── gorush-redis-service.yaml
│   └── gorush-service.yaml
├── logx/
│   ├── log/
│   │   ├── .gitkeep
│   │   └── access.log
│   ├── log.go
│   ├── log_interface.go
│   └── log_test.go
├── main.go
├── metric/
│   ├── metrics.go
│   └── metrics_test.go
├── netlify.toml
├── notify/
│   ├── feedback.go
│   ├── feedback_test.go
│   ├── global.go
│   ├── main_test.go
│   ├── notification.go
│   ├── notification_apns.go
│   ├── notification_apns_test.go
│   ├── notification_fcm.go
│   ├── notification_fcm_test.go
│   ├── notification_hms.go
│   ├── notification_hms_test.go
│   └── notification_test.go
├── router/
│   ├── server.go
│   ├── server_lambda.go
│   ├── server_normal.go
│   ├── server_test.go
│   └── version.go
├── rpc/
│   ├── client_grpc_health.go
│   ├── client_test.go
│   ├── example/
│   │   ├── go/
│   │   │   ├── health/
│   │   │   │   └── main.go
│   │   │   └── send/
│   │   │       └── main.go
│   │   └── node/
│   │       ├── .gitignore
│   │       ├── README.md
│   │       ├── client.js
│   │       ├── gorush_grpc_pb.js
│   │       ├── gorush_pb.js
│   │       └── package.json
│   ├── proto/
│   │   ├── gorush.pb.go
│   │   ├── gorush.proto
│   │   └── gorush_grpc.pb.go
│   ├── server.go
│   └── server_test.go
├── status/
│   ├── status.go
│   ├── status_test.go
│   └── storage.go
├── storage/
│   ├── badger/
│   │   ├── badger.go
│   │   └── badger_test.go
│   ├── boltdb/
│   │   ├── boltdb.go
│   │   └── boltdb_test.go
│   ├── buntdb/
│   │   ├── buntdb.go
│   │   └── buntdb_test.go
│   ├── leveldb/
│   │   ├── leveldb.go
│   │   └── leveldb_test.go
│   ├── memory/
│   │   ├── memory.go
│   │   └── memory_test.go
│   ├── redis/
│   │   ├── redis.go
│   │   └── redis_test.go
│   └── storage.go
├── tests/
│   ├── README.md
│   └── test.json
└── trivy.yaml
Download .txt
SYMBOL INDEX (602 symbols across 62 files)

FILE: app/config.go
  function MergeConfig (line 11) | func MergeConfig(cfg *config.ConfYaml, opts *Options) error {
  function ValidateAndMerge (line 81) | func ValidateAndMerge(opts *Options) (*config.ConfYaml, error) {

FILE: app/config_test.go
  function TestMergeConfig_IOSOptions (line 12) | func TestMergeConfig_IOSOptions(t *testing.T) {
  function TestMergeConfig_AndroidOptions (line 31) | func TestMergeConfig_AndroidOptions(t *testing.T) {
  function TestMergeConfig_HuaweiOptions (line 42) | func TestMergeConfig_HuaweiOptions(t *testing.T) {
  function TestMergeConfig_StorageOptions (line 55) | func TestMergeConfig_StorageOptions(t *testing.T) {
  function TestMergeConfig_ServerOptions (line 68) | func TestMergeConfig_ServerOptions(t *testing.T) {
  function TestMergeConfig_InvalidPort (line 83) | func TestMergeConfig_InvalidPort(t *testing.T) {
  function TestMergeConfig_NoOverrideEmpty (line 93) | func TestMergeConfig_NoOverrideEmpty(t *testing.T) {
  function TestValidateAndMerge (line 106) | func TestValidateAndMerge(t *testing.T) {

FILE: app/options.go
  type Options (line 10) | type Options struct
    method BindFlags (line 32) | func (o *Options) BindFlags() {
    method CLISendOptions (line 95) | func (o *Options) CLISendOptions() CLISendOptions {
    method IsCLIMode (line 105) | func (o *Options) IsCLIMode() bool {
  function NewOptions (line 26) | func NewOptions() *Options {

FILE: app/options_test.go
  function TestNewOptions (line 9) | func TestNewOptions(t *testing.T) {
  function TestOptions_CLISendOptions (line 19) | func TestOptions_CLISendOptions(t *testing.T) {
  function TestOptions_IsCLIMode (line 34) | func TestOptions_IsCLIMode(t *testing.T) {

FILE: app/sender.go
  type CLISendOptions (line 14) | type CLISendOptions struct
  function SendAndroidNotification (line 22) | func SendAndroidNotification(ctx context.Context, cfg *config.ConfYaml, ...
  function SendHuaweiNotification (line 51) | func SendHuaweiNotification(ctx context.Context, cfg *config.ConfYaml, o...
  function SendIOSNotification (line 84) | func SendIOSNotification(ctx context.Context, cfg *config.ConfYaml, opts...
  function SendNotification (line 121) | func SendNotification(

FILE: app/sender_test.go
  function TestCLISendOptions (line 11) | func TestCLISendOptions(t *testing.T) {
  function TestSendNotification_UnsupportedPlatform (line 25) | func TestSendNotification_UnsupportedPlatform(t *testing.T) {

FILE: app/worker.go
  function NewQueueWorker (line 20) | func NewQueueWorker(cfg *config.ConfYaml) (qcore.Worker, error) {
  function NewQueuePool (line 72) | func NewQueuePool(cfg *config.ConfYaml, w qcore.Worker) *queue.Queue {

FILE: app/worker_test.go
  function TestNewQueueWorker_LocalQueue (line 12) | func TestNewQueueWorker_LocalQueue(t *testing.T) {
  function TestNewQueueWorker_UnsupportedEngine (line 21) | func TestNewQueueWorker_UnsupportedEngine(t *testing.T) {
  function TestNewQueuePool (line 31) | func TestNewQueuePool(t *testing.T) {

FILE: config/config.go
  type ConfYaml (line 135) | type ConfYaml struct
    method SanitizedCopy (line 629) | func (c *ConfYaml) SanitizedCopy() *ConfYaml {
  type SectionCore (line 148) | type SectionCore struct
  type SectionAutoTLS (line 173) | type SectionAutoTLS struct
  type SectionAPI (line 180) | type SectionAPI struct
  type SectionAndroid (line 191) | type SectionAndroid struct
  type SectionHuawei (line 199) | type SectionHuawei struct
  type SectionIos (line 207) | type SectionIos struct
  type SectionLog (line 221) | type SectionLog struct
  type SectionStat (line 232) | type SectionStat struct
  type SectionQueue (line 242) | type SectionQueue struct
  type SectionNSQ (line 250) | type SectionNSQ struct
  type SectionNATS (line 257) | type SectionNATS struct
  type SectionRedisQueue (line 264) | type SectionRedisQueue struct
  type SectionRedis (line 276) | type SectionRedis struct
  type SectionBoltDB (line 285) | type SectionBoltDB struct
  type SectionBuntDB (line 291) | type SectionBuntDB struct
  type SectionLevelDB (line 296) | type SectionLevelDB struct
  type SectionBadgerDB (line 301) | type SectionBadgerDB struct
  type SectionPID (line 306) | type SectionPID struct
  type SectionGRPC (line 313) | type SectionGRPC struct
  function setDefaults (line 318) | func setDefaults() {
  function LoadConf (line 380) | func LoadConf(confPath ...string) (*ConfYaml, error) {
  function loadConfigFromViper (line 425) | func loadConfigFromViper() (*ConfYaml, error) {
  function ValidatePort (line 542) | func ValidatePort(port string) error {
  function ValidateAddress (line 557) | func ValidateAddress(addr string) error {
  function ValidatePIDPath (line 572) | func ValidatePIDPath(pidPath string) error {
  function ValidateConfig (line 598) | func ValidateConfig(cfg *ConfYaml) error {
  function redact (line 672) | func redact(s string) string {

FILE: config/config_test.go
  function TestMissingFile (line 15) | func TestMissingFile(t *testing.T) {
  function TestInvalidYAMLFile (line 27) | func TestInvalidYAMLFile(t *testing.T) {
  function TestDefaultConfigLoadFailure (line 47) | func TestDefaultConfigLoadFailure(t *testing.T) {
  function TestEmptyConfig (line 65) | func TestEmptyConfig(t *testing.T) {
  type ConfigTestSuite (line 74) | type ConfigTestSuite struct
    method SetupTest (line 80) | func (suite *ConfigTestSuite) SetupTest() {
    method TestValidateConfDefault (line 92) | func (suite *ConfigTestSuite) TestValidateConfDefault() {
    method TestValidateConf (line 194) | func (suite *ConfigTestSuite) TestValidateConf() {
  function TestConfigTestSuite (line 279) | func TestConfigTestSuite(t *testing.T) {
  function TestLoadConfigFromEnv (line 283) | func TestLoadConfigFromEnv(t *testing.T) {
  function TestRedisDBConfiguration (line 308) | func TestRedisDBConfiguration(t *testing.T) {
  function TestRedisDBConfigurationFromEnv (line 324) | func TestRedisDBConfigurationFromEnv(t *testing.T) {
  function TestLoadWrongDefaultYAMLConfig (line 341) | func TestLoadWrongDefaultYAMLConfig(t *testing.T) {
  function TestValidatePort (line 353) | func TestValidatePort(t *testing.T) {
  function TestValidateAddress (line 429) | func TestValidateAddress(t *testing.T) {
  function TestValidatePIDPath (line 497) | func TestValidatePIDPath(t *testing.T) {
  function TestValidateConfig (line 601) | func TestValidateConfig(t *testing.T) {
  function BenchmarkValidatePort (line 657) | func BenchmarkValidatePort(b *testing.B) {
  function BenchmarkValidateAddress (line 663) | func BenchmarkValidateAddress(b *testing.B) {
  function BenchmarkValidatePIDPath (line 669) | func BenchmarkValidatePIDPath(b *testing.B) {
  function TestSecurityValidationIntegration (line 676) | func TestSecurityValidationIntegration(t *testing.T) {
  function TestAPIDefaultsFromEnv (line 707) | func TestAPIDefaultsFromEnv(t *testing.T) {
  function TestLogDefaultsFromEnv (line 723) | func TestLogDefaultsFromEnv(t *testing.T) {
  function TestLogLevelDefaultsWhenEmpty (line 739) | func TestLogLevelDefaultsWhenEmpty(t *testing.T) {
  function TestAllAPIEndpointsHaveDefaults (line 754) | func TestAllAPIEndpointsHaveDefaults(t *testing.T) {
  function TestSanitizedCopy (line 780) | func TestSanitizedCopy(t *testing.T) {
  function TestSanitizedCopyEmptyFields (line 841) | func TestSanitizedCopyEmptyFields(t *testing.T) {

FILE: core/core.go
  constant PlatFormIos (line 12) | PlatFormIos = iota + 1
  constant PlatFormAndroid (line 14) | PlatFormAndroid
  constant PlatFormHuawei (line 16) | PlatFormHuawei
  constant SucceededPush (line 22) | SucceededPush = "succeeded-push"
  constant FailedPush (line 24) | FailedPush = "failed-push"
  type Platform (line 29) | type Platform
    method String (line 39) | func (p Platform) String() string {
    method IsValid (line 53) | func (p Platform) IsValid() bool {
    method MarshalText (line 72) | func (p Platform) MarshalText() ([]byte, error) {
    method UnmarshalText (line 80) | func (p *Platform) UnmarshalText(text []byte) error {
    method MarshalJSON (line 90) | func (p Platform) MarshalJSON() ([]byte, error) {
    method UnmarshalJSON (line 98) | func (p *Platform) UnmarshalJSON(data []byte) error {
  constant PlatformIOS (line 33) | PlatformIOS     Platform = Platform(PlatFormIos)
  constant PlatformAndroid (line 34) | PlatformAndroid Platform = Platform(PlatFormAndroid)
  constant PlatformHuawei (line 35) | PlatformHuawei  Platform = Platform(PlatFormHuawei)
  function ParsePlatform (line 58) | func ParsePlatform(s string) (Platform, error) {
  type LogBlock (line 123) | type LogBlock
    method IsValid (line 131) | func (l LogBlock) IsValid() bool {
  constant LogSucceededPush (line 126) | LogSucceededPush LogBlock = LogBlock(SucceededPush)
  constant LogFailedPush (line 127) | LogFailedPush    LogBlock = LogBlock(FailedPush)

FILE: core/core_test.go
  function TestPlatformStringAndValidity (line 8) | func TestPlatformStringAndValidity(t *testing.T) {
  function TestParsePlatform (line 32) | func TestParsePlatform(t *testing.T) {
  function TestPlatformTextMarshaling (line 50) | func TestPlatformTextMarshaling(t *testing.T) {
  function TestPlatformJSONMarshaling (line 62) | func TestPlatformJSONMarshaling(t *testing.T) {
  function TestLogBlock (line 82) | func TestLogBlock(t *testing.T) {

FILE: core/health.go
  type Health (line 6) | type Health interface

FILE: core/queue.go
  type Queue (line 4) | type Queue
  function IsLocalQueue (line 18) | func IsLocalQueue(q Queue) bool {

FILE: core/storage.go
  constant TotalCountKey (line 5) | TotalCountKey = "gorush-total-count"
  constant IosSuccessKey (line 9) | IosSuccessKey = "gorush-ios-success-count"
  constant IosErrorKey (line 12) | IosErrorKey = "gorush-ios-error-count"
  constant AndroidSuccessKey (line 15) | AndroidSuccessKey = "gorush-android-success-count"
  constant AndroidErrorKey (line 18) | AndroidErrorKey = "gorush-android-error-count"
  constant HuaweiSuccessKey (line 21) | HuaweiSuccessKey = "gorush-huawei-success-count"
  constant HuaweiErrorKey (line 24) | HuaweiErrorKey = "gorush-huawei-error-count"
  type Storage (line 28) | type Storage interface

FILE: logx/log.go
  type LogPushEntry (line 25) | type LogPushEntry struct
  function init (line 37) | func init() {
  function InitLog (line 49) | func InitLog(accessLevel, accessLog, errorLevel, errorLog string) error {
  function SetLogOut (line 88) | func SetLogOut(log *logrus.Logger, outString string) error {
  function SetLogLevel (line 108) | func SetLogLevel(log *logrus.Logger, levelString string) error {
  function colorForPlatForm (line 119) | func colorForPlatForm(platform int) string {
  function typeForPlatForm (line 132) | func typeForPlatForm(platform int) string {
  function hideToken (line 145) | func hideToken(token string, markLen int) string {
  function GetLogPushEntry (line 164) | func GetLogPushEntry(input *InputLog) LogPushEntry {
  type InputLog (line 194) | type InputLog struct
  function LogPush (line 207) | func LogPush(input *InputLog) LogPushEntry {

FILE: logx/log_interface.go
  function QueueLogger (line 10) | func QueueLogger() DefaultQueueLogger {
  type DefaultQueueLogger (line 18) | type DefaultQueueLogger struct
    method Infof (line 23) | func (l DefaultQueueLogger) Infof(format string, args ...any) {
    method Errorf (line 27) | func (l DefaultQueueLogger) Errorf(format string, args ...any) {
    method Fatalf (line 31) | func (l DefaultQueueLogger) Fatalf(format string, args ...any) {
    method Info (line 35) | func (l DefaultQueueLogger) Info(args ...any) {
    method Error (line 39) | func (l DefaultQueueLogger) Error(args ...any) {
    method Fatal (line 43) | func (l DefaultQueueLogger) Fatal(args ...any) {

FILE: logx/log_test.go
  function TestSetLogLevel (line 17) | func TestSetLogLevel(t *testing.T) {
  function TestSetLogOut (line 27) | func TestSetLogOut(t *testing.T) {
  function TestInitDefaultLog (line 44) | func TestInitDefaultLog(t *testing.T) {
  function TestAccessLevel (line 74) | func TestAccessLevel(t *testing.T) {
  function TestErrorLevel (line 87) | func TestErrorLevel(t *testing.T) {
  function TestAccessLogPath (line 100) | func TestAccessLogPath(t *testing.T) {
  function TestErrorLogPath (line 113) | func TestErrorLogPath(t *testing.T) {
  function TestPlatFormType (line 126) | func TestPlatFormType(t *testing.T) {
  function TestPlatFormColor (line 133) | func TestPlatFormColor(t *testing.T) {
  function TestHideToken (line 140) | func TestHideToken(t *testing.T) {
  function TestLogPushEntry (line 146) | func TestLogPushEntry(t *testing.T) {
  function TestLogPush (line 164) | func TestLogPush(t *testing.T) {

FILE: main.go
  function main (line 27) | func main() {
  function usage (line 193) | func usage() {
  function handleCLINotification (line 198) | func handleCLINotification(ctx context.Context, cfg *config.ConfYaml, op...
  function pinger (line 218) | func pinger(ctx context.Context, cfg *config.ConfYaml) error {
  function createPIDFile (line 246) | func createPIDFile(cfg *config.ConfYaml) error {

FILE: metric/metrics.go
  constant namespace (line 10) | namespace = "gorush_"
  type Metrics (line 14) | type Metrics struct
    method Describe (line 94) | func (c Metrics) Describe(ch chan<- *prometheus.Desc) {
    method Collect (line 109) | func (c Metrics) Collect(ch chan<- prometheus.Metric) {
  function NewMetrics (line 30) | func NewMetrics(q *queue.Queue) Metrics {

FILE: metric/metrics_test.go
  function TestNewMetrics (line 14) | func TestNewMetrics(t *testing.T) {

FILE: notify/feedback.go
  function extractHeaders (line 15) | func extractHeaders(headers []string) map[string]string {
  function DispatchFeedback (line 37) | func DispatchFeedback(

FILE: notify/feedback_test.go
  function TestEmptyFeedbackURL (line 16) | func TestEmptyFeedbackURL(t *testing.T) {
  function TestHTTPErrorInFeedbackCall (line 37) | func TestHTTPErrorInFeedbackCall(t *testing.T) {
  function TestSuccessfulFeedbackCall (line 59) | func TestSuccessfulFeedbackCall(t *testing.T) {

FILE: notify/global.go
  constant HIGH (line 39) | HIGH   = "high"
  constant NORMAL (line 40) | NORMAL = "nornal"

FILE: notify/main_test.go
  function TestMain (line 12) | func TestMain(m *testing.M) {

FILE: notify/notification.go
  type D (line 23) | type D
  constant ApnsPriorityLow (line 30) | ApnsPriorityLow = 5
  constant ApnsPriorityHigh (line 36) | ApnsPriorityHigh = 10
  type Alert (line 40) | type Alert struct
  type RequestPush (line 56) | type RequestPush struct
  type ResponsePush (line 61) | type ResponsePush struct
  type PushNotification (line 66) | type PushNotification struct
    method Bytes (line 129) | func (p *PushNotification) Bytes() []byte {
    method Payload (line 138) | func (p *PushNotification) Payload() []byte {
    method IsTopic (line 144) | func (p *PushNotification) IsTopic() bool {
  function CheckMessage (line 153) | func CheckMessage(req *PushNotification) error {
  function SetProxy (line 188) | func SetProxy(proxy string) error {
  function checkIOSConf (line 201) | func checkIOSConf(cfg *config.ConfYaml) error {
  function checkAndroidConf (line 217) | func checkAndroidConf(cfg *config.ConfYaml) error {
  function checkHuaweiConf (line 229) | func checkHuaweiConf(cfg *config.ConfYaml) error {
  function CheckPushConf (line 243) | func CheckPushConf(cfg *config.ConfYaml) error {
  function SendNotification (line 258) | func SendNotification(
  function makeErrorLogs (line 307) | func makeErrorLogs(

FILE: notify/notification_apns.go
  constant dotP8 (line 35) | dotP8  = ".p8"
  constant dotPEM (line 36) | dotPEM = ".pem"
  constant dotP12 (line 37) | dotP12 = ".p12"
  type Sound (line 55) | type Sound struct
  function loadCertFromFile (line 62) | func loadCertFromFile(
  function loadCertFromBase64 (line 85) | func loadCertFromBase64(
  function createAPNSClientWithToken (line 112) | func createAPNSClientWithToken(
  function InitAPNSClient (line 128) | func InitAPNSClient(ctx context.Context, cfg *config.ConfYaml) error {
  function newApnsClient (line 178) | func newApnsClient(cfg *config.ConfYaml, certificate tls.Certificate) (*...
  function newApnsTokenClient (line 220) | func newApnsTokenClient(cfg *config.ConfYaml, token *token.Token) (*apns...
  function configureHTTP2ConnHealthCheck (line 251) | func configureHTTP2ConnHealthCheck(h2Transport *http2.Transport) {
  function setAlertTitleAndBody (line 257) | func setAlertTitleAndBody(p *payload.Payload, req *PushNotification) {
  function setAlertLocalization (line 277) | func setAlertLocalization(p *payload.Payload, req *PushNotification) {
  function setAlertActions (line 296) | func setAlertActions(p *payload.Payload, req *PushNotification) {
  function setLiveActivityFields (line 312) | func setLiveActivityFields(p *payload.Payload, req *PushNotification) {
  function iosAlertDictionary (line 330) | func iosAlertDictionary(
  function setNotificationPriority (line 350) | func setNotificationPriority(notification *apns2.Notification, priority ...
  function setPayloadSound (line 363) | func setPayloadSound(p *payload.Payload, req *PushNotification) {
  function buildIOSPayload (line 383) | func buildIOSPayload(req *PushNotification) *payload.Payload {
  function GetIOSNotification (line 424) | func GetIOSNotification(req *PushNotification) *apns2.Notification {
  function getApnsClient (line 446) | func getApnsClient(cfg *config.ConfYaml, req *PushNotification) (client ...
  function PushToIOS (line 463) | func PushToIOS(

FILE: notify/notification_apns_test.go
  constant certificateValidP12 (line 22) | certificateValidP12 = `MIIKlgIBAzCCClwGCSqGSIb3DQEHAaCCCk0EggpJMIIKRTCCB...
  constant certificateValidPEM (line 24) | certificateValidPEM = `QmFnIEF0dHJpYnV0ZXMKICAgIGxvY2FsS2V5SUQ6IDhDIDFBI...
  constant authkeyInvalidP8 (line 26) | authkeyInvalidP8 = `TUlHSEFnRUFNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQkcw...
  constant authkeyValidP8 (line 28) | authkeyValidP8 = `LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0...
  constant testKeyID (line 39) | testKeyID  = "key-id"
  constant testTeamID (line 40) | testTeamID = "team-id"
  function TestDisabledAndroidIosConf (line 43) | func TestDisabledAndroidIosConf(t *testing.T) {
  function TestMissingIOSCertificate (line 54) | func TestMissingIOSCertificate(t *testing.T) {
  function TestIOSNotificationStructure (line 72) | func TestIOSNotificationStructure(t *testing.T) {
  function TestIOSSoundAndVolume (line 139) | func TestIOSSoundAndVolume(t *testing.T) {
  function TestIOSSummaryArg (line 241) | func TestIOSSummaryArg(t *testing.T) {
  function TestSendZeroValueForBadgeKey (line 283) | func TestSendZeroValueForBadgeKey(t *testing.T) {
  function TestCheckSilentNotification (line 356) | func TestCheckSilentNotification(t *testing.T) {
  function TestAlertStringExample2ForIos (line 400) | func TestAlertStringExample2ForIos(t *testing.T) {
  function TestAlertStringExample3ForIos (line 456) | func TestAlertStringExample3ForIos(t *testing.T) {
  function TestMessageAndTitle (line 486) | func TestMessageAndTitle(t *testing.T) {
  function TestIOSAlertNotificationStructure (line 541) | func TestIOSAlertNotificationStructure(t *testing.T) {
  function TestWrongIosCertificateExt (line 629) | func TestWrongIosCertificateExt(t *testing.T) {
  function TestAPNSClientDevHost (line 648) | func TestAPNSClientDevHost(t *testing.T) {
  function TestAPNSClientProdHost (line 665) | func TestAPNSClientProdHost(t *testing.T) {
  function TestAPNSClientInvaildToken (line 683) | func TestAPNSClientInvaildToken(t *testing.T) {
  function TestAPNSClientVaildToken (line 714) | func TestAPNSClientVaildToken(t *testing.T) {
  function TestAPNSClientUseProxy (line 745) | func TestAPNSClientUseProxy(t *testing.T) {
  function TestPushToIOS (line 785) | func TestPushToIOS(t *testing.T) {
  function TestApnsHostFromRequest (line 811) | func TestApnsHostFromRequest(t *testing.T) {
  function TestSetAlertTitleAndBody (line 845) | func TestSetAlertTitleAndBody(t *testing.T) {
  function TestSetAlertLocalization (line 910) | func TestSetAlertLocalization(t *testing.T) {
  function TestSetAlertActions (line 934) | func TestSetAlertActions(t *testing.T) {
  function TestSetLiveActivityFields (line 959) | func TestSetLiveActivityFields(t *testing.T) {
  function TestSetNotificationPriority (line 987) | func TestSetNotificationPriority(t *testing.T) {
  function TestSetPayloadSound (line 1022) | func TestSetPayloadSound(t *testing.T) {
  function TestBuildIOSPayload (line 1063) | func TestBuildIOSPayload(t *testing.T) {
  function TestLoadCertFromFile (line 1096) | func TestLoadCertFromFile(t *testing.T) {
  function TestLoadCertFromBase64 (line 1126) | func TestLoadCertFromBase64(t *testing.T) {
  function TestCreateAPNSClientWithToken (line 1167) | func TestCreateAPNSClientWithToken(t *testing.T) {

FILE: notify/notification_fcm.go
  function fileExists (line 18) | func fileExists(filename string) bool {
  function InitFCMClient (line 27) | func InitFCMClient(ctx context.Context, cfg *config.ConfYaml) (*fcm.Clie...
  function setupFCMNotification (line 59) | func setupFCMNotification(req *PushNotification) {
  function setupFCMContentAvailable (line 87) | func setupFCMContentAvailable(req *PushNotification) {
  function setAPNSSound (line 105) | func setAPNSSound(req *PushNotification, sound string) {
  function setupFCMSound (line 125) | func setupFCMSound(req *PushNotification) {
  function convertDataToStringMap (line 145) | func convertDataToStringMap(data map[string]any) map[string]string {
  function buildFCMMessage (line 164) | func buildFCMMessage(req *PushNotification, data map[string]string) *mes...
  function GetAndroidNotification (line 181) | func GetAndroidNotification(req *PushNotification) []*messaging.Message {
  function handleTopicResponse (line 206) | func handleTopicResponse(
  function handleTokenResponses (line 237) | func handleTokenResponses(
  function logDevMessages (line 254) | func logDevMessages(messages []*messaging.Message) {
  function PushToAndroid (line 262) | func PushToAndroid(
  function logPush (line 331) | func logPush(

FILE: notify/notification_fcm_test.go
  function TestMissingAndroidCredential (line 17) | func TestMissingAndroidCredential(t *testing.T) {
  function TestMissingKeyForInitFCMClient (line 29) | func TestMissingKeyForInitFCMClient(t *testing.T) {
  function TestPushToAndroidWrongToken (line 40) | func TestPushToAndroidWrongToken(t *testing.T) {
  function TestPushToAndroidRightTokenForJSONLog (line 58) | func TestPushToAndroidRightTokenForJSONLog(t *testing.T) {
  function TestPushToAndroidRightTokenForStringLog (line 79) | func TestPushToAndroidRightTokenForStringLog(t *testing.T) {
  function TestFCMMessage (line 98) | func TestFCMMessage(t *testing.T) {
  function TestAndroidNotificationStructure (line 151) | func TestAndroidNotificationStructure(t *testing.T) {
  function TestAndroidBackgroundNotificationStructure (line 199) | func TestAndroidBackgroundNotificationStructure(t *testing.T) {
  function TestSetupFCMNotification (line 227) | func TestSetupFCMNotification(t *testing.T) {
  function TestSetupFCMContentAvailable (line 289) | func TestSetupFCMContentAvailable(t *testing.T) {
  function TestSetAPNSSound (line 307) | func TestSetAPNSSound(t *testing.T) {
  function TestSetupFCMSound (line 345) | func TestSetupFCMSound(t *testing.T) {
  function TestConvertDataToStringMap (line 366) | func TestConvertDataToStringMap(t *testing.T) {
  function TestBuildFCMMessage (line 406) | func TestBuildFCMMessage(t *testing.T) {
  function TestGetAndroidNotificationWithTopic (line 435) | func TestGetAndroidNotificationWithTopic(t *testing.T) {
  function TestGetAndroidNotificationWithTokens (line 449) | func TestGetAndroidNotificationWithTokens(t *testing.T) {
  function TestGetAndroidNotificationWithTopicAndTokens (line 462) | func TestGetAndroidNotificationWithTopicAndTokens(t *testing.T) {

FILE: notify/notification_hms.go
  function GetPushClient (line 25) | func GetPushClient(conf *c.Config) (*client.HMSClient, error) {
  function InitHMSClient (line 39) | func InitHMSClient(cfg *config.ConfYaml, appSecret, appID string) (*clie...
  function setHuaweiMessageTarget (line 67) | func setHuaweiMessageTarget(msg *model.Message, req *PushNotification) {
  function setHuaweiAndroidConfig (line 80) | func setHuaweiAndroidConfig(android *model.AndroidConfig, req *PushNotif...
  function setHuaweiNotificationContent (line 98) | func setHuaweiNotificationContent(android *model.AndroidConfig, req *Pus...
  function GetHuaweiNotification (line 128) | func GetHuaweiNotification(req *PushNotification) (*model.MessageRequest...
  function PushToHuawei (line 159) | func PushToHuawei(

FILE: notify/notification_hms_test.go
  function TestMissingHuaweiAppSecret (line 13) | func TestMissingHuaweiAppSecret(t *testing.T) {
  function TestMissingHuaweiAppID (line 26) | func TestMissingHuaweiAppID(t *testing.T) {
  function TestMissingAppSecretForInitHMSClient (line 39) | func TestMissingAppSecretForInitHMSClient(t *testing.T) {
  function TestMissingAppIDForInitHMSClient (line 48) | func TestMissingAppIDForInitHMSClient(t *testing.T) {
  function TestSetHuaweiMessageTarget (line 59) | func TestSetHuaweiMessageTarget(t *testing.T) {
  function TestSetHuaweiAndroidConfig (line 119) | func TestSetHuaweiAndroidConfig(t *testing.T) {
  function TestSetHuaweiNotificationContent (line 143) | func TestSetHuaweiNotificationContent(t *testing.T) {
  function TestHuaweiNotificationDefaultSound (line 202) | func TestHuaweiNotificationDefaultSound(t *testing.T) {
  function TestHuaweiNotificationWithData (line 217) | func TestHuaweiNotificationWithData(t *testing.T) {
  function TestHuaweiNotificationWithCustomNotification (line 229) | func TestHuaweiNotificationWithCustomNotification(t *testing.T) {

FILE: notify/notification_test.go
  constant testHuaweiAppID (line 13) | testHuaweiAppID     = "app-id"
  constant testHuaweiAppSecret (line 14) | testHuaweiAppSecret = "app-secret"
  function TestCorrectConf (line 17) | func TestCorrectConf(t *testing.T) {
  function TestSetProxyURL (line 31) | func TestSetProxyURL(t *testing.T) {
  function TestCheckIOSConf (line 45) | func TestCheckIOSConf(t *testing.T) {
  function TestCheckAndroidConf (line 80) | func TestCheckAndroidConf(t *testing.T) {
  function TestCheckHuaweiConf (line 110) | func TestCheckHuaweiConf(t *testing.T) {
  function TestCheckPushConfNoPlatformEnabled (line 140) | func TestCheckPushConfNoPlatformEnabled(t *testing.T) {
  function TestCheckPushConfAllPlatformsValid (line 152) | func TestCheckPushConfAllPlatformsValid(t *testing.T) {

FILE: router/server.go
  function abortWithError (line 35) | func abortWithError(c *gin.Context, code int, message string) {
  function rootHandler (line 42) | func rootHandler(c *gin.Context) {
  function heartbeatHandler (line 48) | func heartbeatHandler(c *gin.Context) {
  function versionHandler (line 52) | func versionHandler(c *gin.Context) {
  function pushHandler (line 59) | func pushHandler(cfg *config.ConfYaml, q *queue.Queue) gin.HandlerFunc {
  function configHandler (line 113) | func configHandler(cfg *config.ConfYaml) gin.HandlerFunc {
  function metricsHandler (line 119) | func metricsHandler(c *gin.Context) {
  function appStatusHandler (line 123) | func appStatusHandler(q *queue.Queue) gin.HandlerFunc {
  function sysStatsHandler (line 144) | func sysStatsHandler() gin.HandlerFunc {
  function StatMiddleware (line 151) | func StatMiddleware() gin.HandlerFunc {
  function autoTLSServer (line 159) | func autoTLSServer(cfg *config.ConfYaml, q *queue.Queue) *http.Server {
  function routerEngine (line 174) | func routerEngine(cfg *config.ConfYaml, q *queue.Queue) *gin.Engine {
  function markFailedNotification (line 230) | func markFailedNotification(
  function isPlatformEnabled (line 254) | func isPlatformEnabled(cfg *config.ConfYaml, platform int) bool {
  function filterEnabledNotifications (line 268) | func filterEnabledNotifications(
  function countNotificationTargets (line 281) | func countNotificationTargets(notification *notify.PushNotification) int {
  function handleNotification (line 290) | func handleNotification(

FILE: router/server_lambda.go
  function RunHTTPServer (line 17) | func RunHTTPServer(

FILE: router/server_normal.go
  function RunHTTPServer (line 21) | func RunHTTPServer(
  function listenAndServe (line 89) | func listenAndServe(ctx context.Context, s *http.Server, cfg *config.Con...
  function listenAndServeTLS (line 107) | func listenAndServeTLS(ctx context.Context, s *http.Server, cfg *config....
  function startServer (line 125) | func startServer(ctx context.Context, s *http.Server, cfg *config.ConfYa...

FILE: router/server_test.go
  function TestMain (line 35) | func TestMain(m *testing.M) {
  function initTest (line 64) | func initTest() *config.ConfYaml {
  function testRequest (line 71) | func testRequest(t *testing.T, url string) {
  function TestPrintGoRushVersion (line 100) | func TestPrintGoRushVersion(t *testing.T) {
  function TestRunNormalServer (line 109) | func TestRunNormalServer(t *testing.T) {
  function TestRunTLSServer (line 130) | func TestRunTLSServer(t *testing.T) {
  function TestRunTLSBase64Server (line 154) | func TestRunTLSBase64Server(t *testing.T) {
  function TestRunAutoTLSServer (line 184) | func TestRunAutoTLSServer(t *testing.T) {
  function TestLoadTLSCertError (line 201) | func TestLoadTLSCertError(t *testing.T) {
  function TestMissingTLSCertcfgg (line 212) | func TestMissingTLSCertcfgg(t *testing.T) {
  function TestRootHandler (line 227) | func TestRootHandler(t *testing.T) {
  function TestAPIStatusGoHandler (line 246) | func TestAPIStatusGoHandler(t *testing.T) {
  function TestAPIStatusAppHandler (line 262) | func TestAPIStatusAppHandler(t *testing.T) {
  function TestAPIConfigHandler (line 281) | func TestAPIConfigHandler(t *testing.T) {
  function TestMissingNotificationsParameter (line 303) | func TestMissingNotificationsParameter(t *testing.T) {
  function TestEmptyNotifications (line 315) | func TestEmptyNotifications(t *testing.T) {
  function TestMutableContent (line 330) | func TestMutableContent(t *testing.T) {
  function TestOutOfRangeMaxNotifications (line 359) | func TestOutOfRangeMaxNotifications(t *testing.T) {
  function TestSuccessPushHandler (line 387) | func TestSuccessPushHandler(t *testing.T) {
  function TestSysStatsHandler (line 413) | func TestSysStatsHandler(t *testing.T) {
  function TestMetricsHandler (line 424) | func TestMetricsHandler(t *testing.T) {
  function TestGETHeartbeatHandler (line 435) | func TestGETHeartbeatHandler(t *testing.T) {
  function TestHEADHeartbeatHandler (line 446) | func TestHEADHeartbeatHandler(t *testing.T) {
  function TestVersionHandler (line 457) | func TestVersionHandler(t *testing.T) {
  function TestDisabledHTTPServer (line 474) | func TestDisabledHTTPServer(t *testing.T) {
  function TestSenMultipleNotifications (line 483) | func TestSenMultipleNotifications(t *testing.T) {
  function TestDisabledAndroidNotifications (line 521) | func TestDisabledAndroidNotifications(t *testing.T) {
  function TestSyncModeForNotifications (line 559) | func TestSyncModeForNotifications(t *testing.T) {
  function TestSyncModeForTopicNotification (line 600) | func TestSyncModeForTopicNotification(t *testing.T) {
  function TestSyncModeForDeviceGroupNotification (line 643) | func TestSyncModeForDeviceGroupNotification(t *testing.T) {
  function TestDisabledIosNotifications (line 671) | func TestDisabledIosNotifications(t *testing.T) {
  function TestIsPlatformEnabled (line 711) | func TestIsPlatformEnabled(t *testing.T) {
  function TestFilterEnabledNotifications (line 777) | func TestFilterEnabledNotifications(t *testing.T) {
  function TestFilterEnabledNotificationsAllDisabled (line 798) | func TestFilterEnabledNotificationsAllDisabled(t *testing.T) {
  function TestCountNotificationTargets (line 814) | func TestCountNotificationTargets(t *testing.T) {

FILE: router/version.go
  function SetVersion (line 18) | func SetVersion(ver string) {
  function SetCommit (line 23) | func SetCommit(ver string) {
  function GetVersion (line 28) | func GetVersion() string {
  function PrintGoRushVersion (line 33) | func PrintGoRushVersion() {
  function VersionMiddleware (line 50) | func VersionMiddleware() gin.HandlerFunc {

FILE: rpc/client_grpc_health.go
  type healthClient (line 17) | type healthClient struct
    method Close (line 30) | func (c *healthClient) Close() error {
    method Check (line 34) | func (c *healthClient) Check(ctx context.Context) (bool, error) {
  function NewGrpcHealthClient (line 23) | func NewGrpcHealthClient(conn *grpc.ClientConn) core.Health {

FILE: rpc/example/go/health/main.go
  constant address (line 16) | address = "localhost:9000"
  function main (line 19) | func main() {

FILE: rpc/example/go/send/main.go
  constant address (line 15) | address = "localhost:9000"
  function main (line 18) | func main() {

FILE: rpc/example/node/client.js
  function main (line 6) | function main() {

FILE: rpc/example/node/gorush_grpc_pb.js
  function serialize_proto_HealthCheckRequest (line 8) | function serialize_proto_HealthCheckRequest(arg) {
  function deserialize_proto_HealthCheckRequest (line 15) | function deserialize_proto_HealthCheckRequest(buffer_arg) {
  function serialize_proto_HealthCheckResponse (line 19) | function serialize_proto_HealthCheckResponse(arg) {
  function deserialize_proto_HealthCheckResponse (line 26) | function deserialize_proto_HealthCheckResponse(buffer_arg) {
  function serialize_proto_NotificationReply (line 30) | function serialize_proto_NotificationReply(arg) {
  function deserialize_proto_NotificationReply (line 37) | function deserialize_proto_NotificationReply(buffer_arg) {
  function serialize_proto_NotificationRequest (line 41) | function serialize_proto_NotificationRequest(arg) {
  function deserialize_proto_NotificationRequest (line 48) | function deserialize_proto_NotificationRequest(buffer_arg) {

FILE: rpc/proto/gorush.pb.go
  constant _ (line 20) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 22) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type NotificationRequest_Priority (line 25) | type NotificationRequest_Priority
    method Enum (line 44) | func (x NotificationRequest_Priority) Enum() *NotificationRequest_Prio...
    method String (line 50) | func (x NotificationRequest_Priority) String() string {
    method Descriptor (line 54) | func (NotificationRequest_Priority) Descriptor() protoreflect.EnumDesc...
    method Type (line 58) | func (NotificationRequest_Priority) Type() protoreflect.EnumType {
    method Number (line 62) | func (x NotificationRequest_Priority) Number() protoreflect.EnumNumber {
    method EnumDescriptor (line 67) | func (NotificationRequest_Priority) EnumDescriptor() ([]byte, []int) {
  constant NotificationRequest_NORMAL (line 28) | NotificationRequest_NORMAL NotificationRequest_Priority = 0
  constant NotificationRequest_HIGH (line 29) | NotificationRequest_HIGH   NotificationRequest_Priority = 1
  type HealthCheckResponse_ServingStatus (line 71) | type HealthCheckResponse_ServingStatus
    method Enum (line 93) | func (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse...
    method String (line 99) | func (x HealthCheckResponse_ServingStatus) String() string {
    method Descriptor (line 103) | func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.Enu...
    method Type (line 107) | func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType {
    method Number (line 111) | func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumN...
    method EnumDescriptor (line 116) | func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []i...
  constant HealthCheckResponse_UNKNOWN (line 74) | HealthCheckResponse_UNKNOWN     HealthCheckResponse_ServingStatus = 0
  constant HealthCheckResponse_SERVING (line 75) | HealthCheckResponse_SERVING     HealthCheckResponse_ServingStatus = 1
  constant HealthCheckResponse_NOT_SERVING (line 76) | HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2
  type Alert (line 120) | type Alert struct
    method Reset (line 136) | func (x *Alert) Reset() {
    method String (line 143) | func (x *Alert) String() string {
    method ProtoMessage (line 147) | func (*Alert) ProtoMessage() {}
    method ProtoReflect (line 149) | func (x *Alert) ProtoReflect() protoreflect.Message {
    method Descriptor (line 162) | func (*Alert) Descriptor() ([]byte, []int) {
    method GetTitle (line 166) | func (x *Alert) GetTitle() string {
    method GetBody (line 173) | func (x *Alert) GetBody() string {
    method GetSubtitle (line 180) | func (x *Alert) GetSubtitle() string {
    method GetAction (line 187) | func (x *Alert) GetAction() string {
    method GetActionLocKey (line 194) | func (x *Alert) GetActionLocKey() string {
    method GetLaunchImage (line 201) | func (x *Alert) GetLaunchImage() string {
    method GetLocKey (line 208) | func (x *Alert) GetLocKey() string {
    method GetTitleLocKey (line 215) | func (x *Alert) GetTitleLocKey() string {
    method GetLocArgs (line 222) | func (x *Alert) GetLocArgs() []string {
    method GetTitleLocArgs (line 229) | func (x *Alert) GetTitleLocArgs() []string {
  type NotificationRequest (line 236) | type NotificationRequest struct
    method Reset (line 263) | func (x *NotificationRequest) Reset() {
    method String (line 270) | func (x *NotificationRequest) String() string {
    method ProtoMessage (line 274) | func (*NotificationRequest) ProtoMessage() {}
    method ProtoReflect (line 276) | func (x *NotificationRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 289) | func (*NotificationRequest) Descriptor() ([]byte, []int) {
    method GetTokens (line 293) | func (x *NotificationRequest) GetTokens() []string {
    method GetPlatform (line 300) | func (x *NotificationRequest) GetPlatform() int32 {
    method GetMessage (line 307) | func (x *NotificationRequest) GetMessage() string {
    method GetTitle (line 314) | func (x *NotificationRequest) GetTitle() string {
    method GetTopic (line 321) | func (x *NotificationRequest) GetTopic() string {
    method GetKey (line 328) | func (x *NotificationRequest) GetKey() string {
    method GetBadge (line 335) | func (x *NotificationRequest) GetBadge() int32 {
    method GetCategory (line 342) | func (x *NotificationRequest) GetCategory() string {
    method GetAlert (line 349) | func (x *NotificationRequest) GetAlert() *Alert {
    method GetSound (line 356) | func (x *NotificationRequest) GetSound() string {
    method GetContentAvailable (line 363) | func (x *NotificationRequest) GetContentAvailable() bool {
    method GetThreadID (line 370) | func (x *NotificationRequest) GetThreadID() string {
    method GetMutableContent (line 377) | func (x *NotificationRequest) GetMutableContent() bool {
    method GetData (line 384) | func (x *NotificationRequest) GetData() *structpb.Struct {
    method GetImage (line 391) | func (x *NotificationRequest) GetImage() string {
    method GetPriority (line 398) | func (x *NotificationRequest) GetPriority() NotificationRequest_Priori...
    method GetID (line 405) | func (x *NotificationRequest) GetID() string {
    method GetPushType (line 412) | func (x *NotificationRequest) GetPushType() string {
    method GetDevelopment (line 419) | func (x *NotificationRequest) GetDevelopment() bool {
    method GetFcmOptions (line 426) | func (x *NotificationRequest) GetFcmOptions() *FCMOptions {
  type FCMOptions (line 433) | type FCMOptions struct
    method Reset (line 440) | func (x *FCMOptions) Reset() {
    method String (line 447) | func (x *FCMOptions) String() string {
    method ProtoMessage (line 451) | func (*FCMOptions) ProtoMessage() {}
    method ProtoReflect (line 453) | func (x *FCMOptions) ProtoReflect() protoreflect.Message {
    method Descriptor (line 466) | func (*FCMOptions) Descriptor() ([]byte, []int) {
    method GetAnalyticsLabel (line 470) | func (x *FCMOptions) GetAnalyticsLabel() string {
  type NotificationReply (line 477) | type NotificationReply struct
    method Reset (line 485) | func (x *NotificationReply) Reset() {
    method String (line 492) | func (x *NotificationReply) String() string {
    method ProtoMessage (line 496) | func (*NotificationReply) ProtoMessage() {}
    method ProtoReflect (line 498) | func (x *NotificationReply) ProtoReflect() protoreflect.Message {
    method Descriptor (line 511) | func (*NotificationReply) Descriptor() ([]byte, []int) {
    method GetSuccess (line 515) | func (x *NotificationReply) GetSuccess() bool {
    method GetCounts (line 522) | func (x *NotificationReply) GetCounts() int32 {
  type HealthCheckRequest (line 529) | type HealthCheckRequest struct
    method Reset (line 536) | func (x *HealthCheckRequest) Reset() {
    method String (line 543) | func (x *HealthCheckRequest) String() string {
    method ProtoMessage (line 547) | func (*HealthCheckRequest) ProtoMessage() {}
    method ProtoReflect (line 549) | func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 562) | func (*HealthCheckRequest) Descriptor() ([]byte, []int) {
    method GetService (line 566) | func (x *HealthCheckRequest) GetService() string {
  type HealthCheckResponse (line 573) | type HealthCheckResponse struct
    method Reset (line 580) | func (x *HealthCheckResponse) Reset() {
    method String (line 587) | func (x *HealthCheckResponse) String() string {
    method ProtoMessage (line 591) | func (*HealthCheckResponse) ProtoMessage() {}
    method ProtoReflect (line 593) | func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 606) | func (*HealthCheckResponse) Descriptor() ([]byte, []int) {
    method GetStatus (line 610) | func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingS...
  constant file_gorush_proto_rawDesc (line 619) | file_gorush_proto_rawDesc = "" +
  function file_gorush_proto_rawDescGZIP (line 687) | func file_gorush_proto_rawDescGZIP() []byte {
  function init (line 724) | func init() { file_gorush_proto_init() }
  function file_gorush_proto_init (line 725) | func file_gorush_proto_init() {

FILE: rpc/proto/gorush_grpc.pb.go
  constant _ (line 19) | _ = grpc.SupportPackageIsVersion9
  constant Gorush_Send_FullMethodName (line 22) | Gorush_Send_FullMethodName = "/proto.Gorush/Send"
  type GorushClient (line 28) | type GorushClient interface
  type gorushClient (line 32) | type gorushClient struct
    method Send (line 40) | func (c *gorushClient) Send(ctx context.Context, in *NotificationReque...
  function NewGorushClient (line 36) | func NewGorushClient(cc grpc.ClientConnInterface) GorushClient {
  type GorushServer (line 53) | type GorushServer interface
  type UnimplementedGorushServer (line 62) | type UnimplementedGorushServer struct
    method Send (line 64) | func (UnimplementedGorushServer) Send(context.Context, *NotificationRe...
    method testEmbeddedByValue (line 67) | func (UnimplementedGorushServer) testEmbeddedByValue() {}
  type UnsafeGorushServer (line 72) | type UnsafeGorushServer interface
  function RegisterGorushServer (line 76) | func RegisterGorushServer(s grpc.ServiceRegistrar, srv GorushServer) {
  function _Gorush_Send_Handler (line 87) | func _Gorush_Send_Handler(srv interface{}, ctx context.Context, dec func...
  constant Health_Check_FullMethodName (line 122) | Health_Check_FullMethodName = "/proto.Health/Check"
  type HealthClient (line 128) | type HealthClient interface
  type healthClient (line 132) | type healthClient struct
    method Check (line 140) | func (c *healthClient) Check(ctx context.Context, in *HealthCheckReque...
  function NewHealthClient (line 136) | func NewHealthClient(cc grpc.ClientConnInterface) HealthClient {
  type HealthServer (line 153) | type HealthServer interface
  type UnimplementedHealthServer (line 162) | type UnimplementedHealthServer struct
    method Check (line 164) | func (UnimplementedHealthServer) Check(context.Context, *HealthCheckRe...
    method testEmbeddedByValue (line 167) | func (UnimplementedHealthServer) testEmbeddedByValue() {}
  type UnsafeHealthServer (line 172) | type UnsafeHealthServer interface
  function RegisterHealthServer (line 176) | func RegisterHealthServer(s grpc.ServiceRegistrar, srv HealthServer) {
  function _Health_Check_Handler (line 187) | func _Health_Check_Handler(srv interface{}, ctx context.Context, dec fun...

FILE: rpc/server.go
  type Server (line 33) | type Server struct
    method Check (line 49) | func (s *Server) Check(
    method Send (line 70) | func (s *Server) Send(
  function NewServer (line 41) | func NewServer(cfg *config.ConfYaml) *Server {
  function safeIntToInt32 (line 146) | func safeIntToInt32(n int) (int32, error) {
  function RunGRPCServer (line 154) | func RunGRPCServer(ctx context.Context, cfg *config.ConfYaml) error {

FILE: rpc/server_test.go
  function TestSafeIntToInt32 (line 8) | func TestSafeIntToInt32(t *testing.T) {

FILE: status/status.go
  type App (line 26) | type App struct
  type AndroidStatus (line 39) | type AndroidStatus struct
  type IosStatus (line 45) | type IosStatus struct
  type HuaweiStatus (line 51) | type HuaweiStatus struct
  function InitAppStatus (line 57) | func InitAppStatus(conf *config.ConfYaml) error {

FILE: status/status_test.go
  constant redisEngine (line 14) | redisEngine = "redis"
  function TestMain (line 16) | func TestMain(m *testing.M) {
  function TestStorageDriverExist (line 20) | func TestStorageDriverExist(t *testing.T) {
  function TestStatForMemoryEngine (line 27) | func TestStatForMemoryEngine(t *testing.T) {
  function TestRedisServerSuccess (line 55) | func TestRedisServerSuccess(t *testing.T) {
  function TestRedisServerError (line 65) | func TestRedisServerError(t *testing.T) {
  function TestStatForRedisEngine (line 75) | func TestStatForRedisEngine(t *testing.T) {
  function TestDefaultEngine (line 104) | func TestDefaultEngine(t *testing.T) {
  function TestStatForBoltDBEngine (line 131) | func TestStatForBoltDBEngine(t *testing.T) {

FILE: status/storage.go
  type StateStorage (line 7) | type StateStorage struct
    method Init (line 17) | func (s *StateStorage) Init() error {
    method Close (line 21) | func (s *StateStorage) Close() error {
    method Reset (line 26) | func (s *StateStorage) Reset() {
    method AddTotalCount (line 37) | func (s *StateStorage) AddTotalCount(count int64) {
    method AddIosSuccess (line 42) | func (s *StateStorage) AddIosSuccess(count int64) {
    method AddIosError (line 47) | func (s *StateStorage) AddIosError(count int64) {
    method AddAndroidSuccess (line 52) | func (s *StateStorage) AddAndroidSuccess(count int64) {
    method AddAndroidError (line 57) | func (s *StateStorage) AddAndroidError(count int64) {
    method AddHuaweiSuccess (line 62) | func (s *StateStorage) AddHuaweiSuccess(count int64) {
    method AddHuaweiError (line 67) | func (s *StateStorage) AddHuaweiError(count int64) {
    method GetTotalCount (line 72) | func (s *StateStorage) GetTotalCount() int64 {
    method GetIosSuccess (line 77) | func (s *StateStorage) GetIosSuccess() int64 {
    method GetIosError (line 82) | func (s *StateStorage) GetIosError() int64 {
    method GetAndroidSuccess (line 87) | func (s *StateStorage) GetAndroidSuccess() int64 {
    method GetAndroidError (line 92) | func (s *StateStorage) GetAndroidError() int64 {
    method GetHuaweiSuccess (line 97) | func (s *StateStorage) GetHuaweiSuccess() int64 {
    method GetHuaweiError (line 102) | func (s *StateStorage) GetHuaweiError() int64 {
  function NewStateStorage (line 11) | func NewStateStorage(store core.Storage) *StateStorage {

FILE: storage/badger/badger.go
  function New (line 17) | func New(dbPath string) *Storage {
  type Storage (line 24) | type Storage struct
    method Add (line 33) | func (s *Storage) Add(key string, count int64) {
    method Set (line 39) | func (s *Storage) Set(key string, count int64) {
    method Get (line 45) | func (s *Storage) Get(key string) int64 {
    method Init (line 52) | func (s *Storage) Init() error {
    method Close (line 66) | func (s *Storage) Close() error {
    method setBadger (line 74) | func (s *Storage) setBadger(key string, count int64) {
    method getBadger (line 84) | func (s *Storage) getBadger(key string) int64 {

FILE: storage/badger/badger_test.go
  function TestBadgerEngine (line 13) | func TestBadgerEngine(t *testing.T) {

FILE: storage/boltdb/boltdb.go
  function New (line 16) | func New(dbPath, bucket string) *Storage {
  type Storage (line 24) | type Storage struct
    method Add (line 31) | func (s *Storage) Add(key string, count int64) {
    method Set (line 37) | func (s *Storage) Set(key string, count int64) {
    method Get (line 43) | func (s *Storage) Get(key string) int64 {
    method Init (line 50) | func (s *Storage) Init() error {
    method Close (line 60) | func (s *Storage) Close() error {
    method setBoltDB (line 68) | func (s *Storage) setBoltDB(key string, count int64) {
    method getBoltDB (line 75) | func (s *Storage) getBoltDB(key string) int64 {

FILE: storage/boltdb/boltdb_test.go
  function TestBoltDBEngine (line 13) | func TestBoltDBEngine(t *testing.T) {

FILE: storage/buntdb/buntdb.go
  function New (line 17) | func New(dbPath string) *Storage {
  type Storage (line 24) | type Storage struct
    method Add (line 30) | func (s *Storage) Add(key string, count int64) {
    method Set (line 36) | func (s *Storage) Set(key string, count int64) {
    method Get (line 42) | func (s *Storage) Get(key string) int64 {
    method Init (line 49) | func (s *Storage) Init() error {
    method Close (line 59) | func (s *Storage) Close() error {
    method setBuntDB (line 67) | func (s *Storage) setBuntDB(key string, count int64) {
    method getBuntDB (line 79) | func (s *Storage) getBuntDB(key string) int64 {

FILE: storage/buntdb/buntdb_test.go
  function TestBuntDBEngine (line 13) | func TestBuntDBEngine(t *testing.T) {

FILE: storage/leveldb/leveldb.go
  function New (line 27) | func New(dbPath string) *Storage {
  type Storage (line 34) | type Storage struct
    method setLevelDB (line 15) | func (s *Storage) setLevelDB(key string, count int64) {
    method getLevelDB (line 20) | func (s *Storage) getLevelDB(key string) int64 {
    method Add (line 40) | func (s *Storage) Add(key string, count int64) {
    method Set (line 46) | func (s *Storage) Set(key string, count int64) {
    method Get (line 52) | func (s *Storage) Get(key string) int64 {
    method Init (line 59) | func (s *Storage) Init() error {
    method Close (line 69) | func (s *Storage) Close() error {

FILE: storage/leveldb/leveldb_test.go
  function TestLevelDBEngine (line 13) | func TestLevelDBEngine(t *testing.T) {

FILE: storage/memory/memory.go
  function New (line 14) | func New() *Storage {
  type Storage (line 19) | type Storage struct
    method getValueBtKey (line 23) | func (s *Storage) getValueBtKey(key string) *atomic.Int64 {
    method Add (line 32) | func (s *Storage) Add(key string, count int64) {
    method Set (line 36) | func (s *Storage) Set(key string, count int64) {
    method Get (line 40) | func (s *Storage) Get(key string) int64 {
    method Init (line 45) | func (*Storage) Init() error {
    method Close (line 50) | func (*Storage) Close() error {

FILE: storage/memory/memory_test.go
  function TestMemoryEngine (line 13) | func TestMemoryEngine(t *testing.T) {

FILE: storage/redis/redis.go
  function New (line 18) | func New(
  type Storage (line 36) | type Storage struct
    method Add (line 46) | func (s *Storage) Add(key string, count int64) {
    method Set (line 50) | func (s *Storage) Set(key string, count int64) {
    method Get (line 54) | func (s *Storage) Get(key string) int64 {
    method Init (line 61) | func (s *Storage) Init() error {
    method Close (line 80) | func (s *Storage) Close() error {

FILE: storage/redis/redis_test.go
  function TestRedisServerError (line 13) | func TestRedisServerError(t *testing.T) {
  function TestRedisEngine (line 26) | func TestRedisEngine(t *testing.T) {
Condensed preview — 126 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (592K chars).
[
  {
    "path": ".dockerignore",
    "chars": 12,
    "preview": "*\n!release/\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 565,
    "preview": "# These are supported funding model platforms\n\ngithub: appleboy\npatreon: # Replace with a single Patreon username\nopen_c"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 195,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: weekly\n  - pack"
  },
  {
    "path": ".github/workflows/codeql.yaml",
    "chars": 1663,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/docker.yaml",
    "chars": 2030,
    "preview": "name: Docker Image\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - \"v*\"\n  pull_request:\n    branches:\n      "
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "chars": 694,
    "preview": "name: Goreleaser\n\non:\n  push:\n    tags:\n      - \"*\"\n\npermissions:\n  contents: write\n\njobs:\n  goreleaser:\n    runs-on: ub"
  },
  {
    "path": ".github/workflows/testing.yml",
    "chars": 1578,
    "preview": "name: Run Lint and Testing\n\non:\n  push:\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repo"
  },
  {
    "path": ".github/workflows/trivy-scan.yml",
    "chars": 2744,
    "preview": "name: Trivy Security Scan\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n  sched"
  },
  {
    "path": ".gitignore",
    "chars": 423,
    "preview": "# 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 spe"
  },
  {
    "path": ".golangci.yml",
    "chars": 2377,
    "preview": "version: \"2\"\noutput:\n  sort-order:\n    - file\nlinters:\n  default: none\n  enable:\n    - bidichk\n    - bodyclose\n    - dep"
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 2332,
    "preview": "version: 2\n\nbefore:\n  hooks:\n    - go mod tidy\n\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    goos:\n      - darwin\n      - "
  },
  {
    "path": ".hadolint.yaml",
    "chars": 20,
    "preview": "ignored:\n  - DL3018\n"
  },
  {
    "path": ".roomodes",
    "chars": 9310,
    "preview": "customModes:\n  - slug: go-code-tester\n    name: 🧪 Go Code Tester\n    description: Go testing and quality expert\n    role"
  },
  {
    "path": "CLAUDE.md",
    "chars": 3693,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "LICENSE",
    "chars": 1075,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Bo-Yi Wu\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "Makefile",
    "chars": 5460,
    "preview": "EXECUTABLE := gorush\nGO ?= go\nGOFILES := $(shell find . -name \"*.go\" -type f)\nTAGS ?= sqlite\nLDFLAGS ?= -X main.version="
  },
  {
    "path": "README.md",
    "chars": 44368,
    "preview": "# gorush\n\nA push notification micro server using [Gin](https://github.com/gin-gonic/gin) framework written in Go (Golang"
  },
  {
    "path": "app/config.go",
    "chars": 2516,
    "preview": "package app\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/appleboy/gorush/config\"\n)\n\n// MergeConfig merges CLI options into the configu"
  },
  {
    "path": "app/config_test.go",
    "chars": 2832,
    "preview": "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.c"
  },
  {
    "path": "app/options.go",
    "chars": 3817,
    "preview": "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 Option"
  },
  {
    "path": "app/options_test.go",
    "chars": 1535,
    "preview": "package app\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewOptions(t *testing.T) {\n\topts := "
  },
  {
    "path": "app/sender.go",
    "chars": 2844,
    "preview": "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/a"
  },
  {
    "path": "app/sender_test.go",
    "chars": 856,
    "preview": "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 Test"
  },
  {
    "path": "app/worker.go",
    "chars": 2306,
    "preview": "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/apple"
  },
  {
    "path": "app/worker_test.go",
    "chars": 835,
    "preview": "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.c"
  },
  {
    "path": "certificate/authkey-invalid.p8",
    "chars": 187,
    "preview": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE\nZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKW"
  },
  {
    "path": "certificate/authkey-valid.p8",
    "chars": 241,
    "preview": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE\nZV05laAoJAl+/6Xt2O4mOB611sO"
  },
  {
    "path": "certificate/certificate-valid.pem",
    "chars": 3684,
    "preview": "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 Pri"
  },
  {
    "path": "certificate/localhost.cert",
    "chars": 1094,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIC+zCCAeOgAwIBAgIJALbZEDvUQrFKMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV\nBAMMCWxvY2FsaG9zdDAeFw0xNjA"
  },
  {
    "path": "certificate/localhost.key",
    "chars": 1675,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAyPX7GDiNUvNWcHmPufW6XfRYQTgELNw0XGDkA4Hkfmr7CN7R\nhiJUWDo9DK8YKKmohFdVF1V"
  },
  {
    "path": "config/config.go",
    "chars": 22351,
    "preview": "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"
  },
  {
    "path": "config/config_test.go",
    "chars": 27217,
    "preview": "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/stre"
  },
  {
    "path": "config/testdata/empty.yml",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "config/testdata/redis_db_config.yml",
    "chars": 310,
    "preview": "queue:\n  engine: \"redis\"\n  redis:\n    addr: 127.0.0.1:6379\n    group: gorush\n    consumer: gorush\n    stream_name: gorus"
  },
  {
    "path": "contrib/init/debian/README.md",
    "chars": 548,
    "preview": "# Run gorush in Debian/Ubuntu\n\n## Installation\n\nPut `gorush` binary into `/usr/bin` folder.\n\n```sh\ncp gorush /usr/bin/\nc"
  },
  {
    "path": "contrib/init/debian/gorush",
    "chars": 1819,
    "preview": "#!/bin/sh\n\n### BEGIN INIT INFO\n# Provides:          gorush\n# Required-Start:    $syslog $network\n# Required-Stop:     $s"
  },
  {
    "path": "core/core.go",
    "chars": 3148,
    "preview": "package core\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strings\"\n)\n\n// Backward-compatible integer constants kept as-is.\ncon"
  },
  {
    "path": "core/core_test.go",
    "chars": 2542,
    "preview": "package core\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n)\n\nfunc TestPlatformStringAndValidity(t *testing.T) {\n\tcases := []str"
  },
  {
    "path": "core/health.go",
    "chars": 189,
    "preview": "package core\n\nimport \"context\"\n\n// Health defines a health-check connection.\ntype Health interface {\n\t// Check returns i"
  },
  {
    "path": "core/queue.go",
    "chars": 422,
    "preview": "package core\n\n// Queue as backend\ntype Queue string\n\nvar (\n\t// LocalQueue for channel in Go\n\tLocalQueue Queue = \"local\"\n"
  },
  {
    "path": "core/storage.go",
    "chars": 974,
    "preview": "package core\n\nconst (\n\t// TotalCountKey is key name for total count of storage\n\tTotalCountKey = \"gorush-total-count\"\n\n\t/"
  },
  {
    "path": "doc.go",
    "chars": 1393,
    "preview": "// A push notification server using Gin framework written in Go (Golang).\n//\n// Details about the gorush project are fou"
  },
  {
    "path": "docker/Dockerfile",
    "chars": 584,
    "preview": "FROM alpine:3.22\n\nARG TARGETOS\nARG TARGETARCH\nARG USER=gorush\nENV HOME /home/$USER\n\nLABEL maintainer=\"Bo-Yi Wu <appleboy"
  },
  {
    "path": "docker-compose.yml",
    "chars": 258,
    "preview": "version: '3'\n\nservices:\n  gorush:\n    image: appleboy/gorush\n    restart: always\n    ports:\n      - \"8088:8088\"\n      - "
  },
  {
    "path": "go.mod",
    "chars": 7655,
    "preview": "module github.com/appleboy/gorush\n\ngo 1.25.0\n\nreplace github.com/msalihkarakasli/go-hms-push => github.com/spawn2kill/go"
  },
  {
    "path": "go.sum",
    "chars": 59402,
    "preview": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ"
  },
  {
    "path": "helm/gorush/.helmignore",
    "chars": 349,
    "preview": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation"
  },
  {
    "path": "helm/gorush/Chart.yaml",
    "chars": 300,
    "preview": "apiVersion: v2\nname: gorush\ndescription: A push notification micro server using Gin framework written in Go (Golang)\ntyp"
  },
  {
    "path": "helm/gorush/templates/NOTES.txt",
    "chars": 1743,
    "preview": "1. Get the application URL by running these commands:\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingre"
  },
  {
    "path": "helm/gorush/templates/_helpers.tpl",
    "chars": 1772,
    "preview": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"gorush.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trun"
  },
  {
    "path": "helm/gorush/templates/configmap.yml",
    "chars": 284,
    "preview": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: {{ .Chart.Name }}\n  namespace: {{ .Chart.Name }}\ndata:\n  # stat\n  stats"
  },
  {
    "path": "helm/gorush/templates/deployment.yaml",
    "chars": 2322,
    "preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ .Chart.Name }}\n  namespace: {{ .Release.Namespace }}\n  labels:"
  },
  {
    "path": "helm/gorush/templates/hpa.yaml",
    "chars": 912,
    "preview": "{{- if .Values.autoscaling.enabled }}\napiVersion: autoscaling/v2beta1\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: {{"
  },
  {
    "path": "helm/gorush/templates/ingress.yaml",
    "chars": 1092,
    "preview": "{{- if .Values.ingress.enabled -}}\n{{- $svcPort := .Values.service.port -}}\n{{- if semverCompare \">=1.14-0\" .Capabilitie"
  },
  {
    "path": "helm/gorush/templates/service.yaml",
    "chars": 373,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ .Chart.Name }}\n  namespace: {{ .Chart.Name }}\n  labels:\n    {{- includ"
  },
  {
    "path": "helm/gorush/templates/serviceaccount.yaml",
    "chars": 349,
    "preview": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"gorush.servic"
  },
  {
    "path": "helm/gorush/values.yaml",
    "chars": 1276,
    "preview": "# Default values for gorush.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nrep"
  },
  {
    "path": "install.sh",
    "chars": 6737,
    "preview": "#!/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;14"
  },
  {
    "path": "k8s/gorush-aws-alb-ingress.yaml",
    "chars": 642,
    "preview": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: gorush\n  namespace: gorush\n  annotations:\n    # Kuberne"
  },
  {
    "path": "k8s/gorush-configmap.yaml",
    "chars": 149,
    "preview": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: gorush-config\n  namespace: gorush\ndata:\n  # stat\n  stat.engine: redis\n "
  },
  {
    "path": "k8s/gorush-deployment.yaml",
    "chars": 1436,
    "preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: gorush\n  namespace: gorush\nspec:\n  replicas: 2\n  selector:\n    ma"
  },
  {
    "path": "k8s/gorush-namespace.yaml",
    "chars": 56,
    "preview": "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: gorush\n"
  },
  {
    "path": "k8s/gorush-redis-deployment.yaml",
    "chars": 829,
    "preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: redis\n  namespace: gorush\nspec:\n  selector:\n    matchLabels:\n    "
  },
  {
    "path": "k8s/gorush-redis-service.yaml",
    "chars": 250,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  name: redis\n  namespace: gorush\n  labels:\n    app: redis\n    role: master\n    t"
  },
  {
    "path": "k8s/gorush-service.yaml",
    "chars": 601,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  name: gorush\n  namespace: gorush\n  labels:\n    app: gorush\n    tier: frontend\ns"
  },
  {
    "path": "logx/log/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "logx/log/access.log",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "logx/log.go",
    "chars": 5377,
    "preview": "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."
  },
  {
    "path": "logx/log_interface.go",
    "chars": 973,
    "preview": "package logx\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// QueueLogger for simple logger.\nfunc QueueLogger() Def"
  },
  {
    "path": "logx/log_test.go",
    "chars": 3795,
    "preview": "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\""
  },
  {
    "path": "main.go",
    "chars": 7259,
    "preview": "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"
  },
  {
    "path": "metric/metrics.go",
    "chars": 3983,
    "preview": "package metric\n\nimport (\n\t\"github.com/appleboy/gorush/status\"\n\n\t\"github.com/golang-queue/queue\"\n\t\"github.com/prometheus/"
  },
  {
    "path": "metric/metrics_test.go",
    "chars": 505,
    "preview": "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/a"
  },
  {
    "path": "netlify.toml",
    "chars": 268,
    "preview": "[build]\ncommand = \"make build_linux_lambda\"\nfunctions = \"release/linux/lambda\"\n\n[build.environment]\nGO111MODULE = \"on\"\nG"
  },
  {
    "path": "notify/feedback.go",
    "chars": 1894,
    "preview": "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"
  },
  {
    "path": "notify/feedback_test.go",
    "chars": 1985,
    "preview": "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/con"
  },
  {
    "path": "notify/global.go",
    "chars": 842,
    "preview": "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/pus"
  },
  {
    "path": "notify/main_test.go",
    "chars": 278,
    "preview": "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/stat"
  },
  {
    "path": "notify/notification.go",
    "chars": 10210,
    "preview": "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\"githu"
  },
  {
    "path": "notify/notification_apns.go",
    "chars": 13200,
    "preview": "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"
  },
  {
    "path": "notify/notification_apns_test.go",
    "chars": 41578,
    "preview": "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/confi"
  },
  {
    "path": "notify/notification_fcm.go",
    "chars": 8416,
    "preview": "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/g"
  },
  {
    "path": "notify/notification_fcm_test.go",
    "chars": 11425,
    "preview": "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/app"
  },
  {
    "path": "notify/notification_hms.go",
    "chars": 5742,
    "preview": "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"
  },
  {
    "path": "notify/notification_hms_test.go",
    "chars": 5902,
    "preview": "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\""
  },
  {
    "path": "notify/notification_test.go",
    "chars": 4059,
    "preview": "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\"githu"
  },
  {
    "path": "router/server.go",
    "chars": 8967,
    "preview": "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/goru"
  },
  {
    "path": "router/server_lambda.go",
    "chars": 639,
    "preview": "//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/ap"
  },
  {
    "path": "router/server_normal.go",
    "chars": 3379,
    "preview": "//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"
  },
  {
    "path": "router/server_test.go",
    "chars": 23514,
    "preview": "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"
  },
  {
    "path": "router/version.go",
    "chars": 927,
    "preview": "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  "
  },
  {
    "path": "rpc/client_grpc_health.go",
    "chars": 1297,
    "preview": "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.g"
  },
  {
    "path": "rpc/client_test.go",
    "chars": 12,
    "preview": "package rpc\n"
  },
  {
    "path": "rpc/example/go/health/main.go",
    "chars": 772,
    "preview": "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."
  },
  {
    "path": "rpc/example/go/send/main.go",
    "chars": 1363,
    "preview": "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.go"
  },
  {
    "path": "rpc/example/node/.gitignore",
    "chars": 42,
    "preview": "*~\nnode_modules\nnpm-debug.log\n.yarn-cache\n"
  },
  {
    "path": "rpc/example/node/README.md",
    "chars": 419,
    "preview": "# 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 inst"
  },
  {
    "path": "rpc/example/node/client.js",
    "chars": 938,
    "preview": "var messages = require('./gorush_pb');\nvar services = require('./gorush_grpc_pb');\n\nvar grpc = require('@grpc/grpc-js');"
  },
  {
    "path": "rpc/example/node/gorush_grpc_pb.js",
    "chars": 2898,
    "preview": "// GENERATED CODE -- DO NOT EDIT!\n\n'use strict';\nvar grpc = require('@grpc/grpc-js');\nvar gorush_pb = require('./gorush_"
  },
  {
    "path": "rpc/example/node/gorush_pb.js",
    "chars": 53456,
    "preview": "// source: gorush.proto\n/**\n * @fileoverview\n * @enhanceable\n * @suppress {missingRequire} reports error on implicit typ"
  },
  {
    "path": "rpc/example/node/package.json",
    "chars": 220,
    "preview": "{\n  \"name\": \"gorush-examples\",\n  \"version\": \"0.1.0\",\n  \"dependencies\": {\n    \"async\": \"^3.2.6\",\n    \"@grpc/grpc-js\": \"^1"
  },
  {
    "path": "rpc/proto/gorush.pb.go",
    "chars": 22446,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.6\n// \tprotoc        v5.29.3\n// sou"
  },
  {
    "path": "rpc/proto/gorush.proto",
    "chars": 1452,
    "preview": "syntax = \"proto3\";\nimport \"google/protobuf/struct.proto\";\n\npackage proto;\noption go_package = \"./;proto\";\n\nmessage Alert"
  },
  {
    "path": "rpc/proto/gorush_grpc.pb.go",
    "chars": 7602,
    "preview": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.5.1\n// - protoc           "
  },
  {
    "path": "rpc/server.go",
    "chars": 5939,
    "preview": "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\"fi"
  },
  {
    "path": "rpc/server_test.go",
    "chars": 1717,
    "preview": "package rpc\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\nfunc TestSafeIntToInt32(t *testing.T) {\n\ttests := []struct {\n\t\tname    strin"
  },
  {
    "path": "status/status.go",
    "chars": 2582,
    "preview": "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"
  },
  {
    "path": "status/status_test.go",
    "chars": 5964,
    "preview": "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/a"
  },
  {
    "path": "status/storage.go",
    "chars": 2861,
    "preview": "package status\n\nimport (\n\t\"github.com/appleboy/gorush/core\"\n)\n\ntype StateStorage struct {\n\tstore core.Storage\n}\n\nfunc Ne"
  },
  {
    "path": "storage/badger/badger.go",
    "chars": 1915,
    "preview": "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/bad"
  },
  {
    "path": "storage/badger/badger_test.go",
    "chars": 1026,
    "preview": "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"
  },
  {
    "path": "storage/boltdb/boltdb.go",
    "chars": 1462,
    "preview": "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 "
  },
  {
    "path": "storage/boltdb/boltdb_test.go",
    "chars": 1034,
    "preview": "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"
  },
  {
    "path": "storage/buntdb/buntdb.go",
    "chars": 1650,
    "preview": "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/buntd"
  },
  {
    "path": "storage/buntdb/buntdb_test.go",
    "chars": 1024,
    "preview": "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"
  },
  {
    "path": "storage/leveldb/leveldb.go",
    "chars": 1400,
    "preview": "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/le"
  },
  {
    "path": "storage/leveldb/leveldb_test.go",
    "chars": 1039,
    "preview": "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"
  },
  {
    "path": "storage/memory/memory.go",
    "chars": 938,
    "preview": "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 = (*St"
  },
  {
    "path": "storage/memory/memory_test.go",
    "chars": 1022,
    "preview": "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"
  },
  {
    "path": "storage/redis/redis.go",
    "chars": 1816,
    "preview": "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"
  },
  {
    "path": "storage/redis/redis_test.go",
    "chars": 1374,
    "preview": "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\""
  },
  {
    "path": "storage/storage.go",
    "chars": 16,
    "preview": "package storage\n"
  },
  {
    "path": "tests/README.md",
    "chars": 779,
    "preview": "# Testing\n\nHow to test gorush with http request?\n\n## download bat tool\n\nDownload [cURL-like tool for humans](https://git"
  },
  {
    "path": "tests/test.json",
    "chars": 471,
    "preview": "{\n  \"notifications\": [\n    {\n      \"tokens\": [\"token_a\", \"token_b\"],\n      \"platform\": 1,\n      \"message\": \"Hello World "
  },
  {
    "path": "trivy.yaml",
    "chars": 215,
    "preview": "# Trivy configuration file for gorush project\n# Docs: https://aquasecurity.github.io/trivy/latest/configuration/config-f"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the appleboy/gorush GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 126 files (534.7 KB), approximately 173.0k tokens, and a symbol index with 602 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!