Full Code of binwiederhier/ntfy for AI

main f9974d8a2f45 cached
377 files
4.7 MB
1.2M tokens
2715 symbols
1 requests
Download .txt
Showing preview only (4,960K chars total). Download the full file or copy to clipboard to get everything.
Repository: binwiederhier/ntfy
Branch: main
Commit: f9974d8a2f45
Files: 377
Total size: 4.7 MB

Directory structure:
gitextract_5801oewz/

├── .dockerignore
├── .git-blame-ignore-revs
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── 1_bug_report.md
│   │   ├── 2_enhancement_request.md
│   │   ├── 3_tech_support.md
│   │   └── 4_question.md
│   └── workflows/
│       ├── build.yaml
│       ├── docs.yaml
│       ├── release.yaml
│       └── test.yaml
├── .gitignore
├── .gitpod.yml
├── .goreleaser.yml
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── Dockerfile-arm
├── Dockerfile-build
├── LICENSE
├── LICENSE.GPLv2
├── Makefile
├── README.md
├── SECURITY.md
├── assets/
│   └── favicon.xcf
├── client/
│   ├── client.go
│   ├── client.yml
│   ├── client_test.go
│   ├── config.go
│   ├── config_darwin.go
│   ├── config_test.go
│   ├── config_unix.go
│   ├── config_windows.go
│   ├── ntfy-client.service
│   ├── options.go
│   └── user/
│       └── ntfy-client.service
├── cmd/
│   ├── access.go
│   ├── access_test.go
│   ├── app.go
│   ├── app_test.go
│   ├── config_loader.go
│   ├── config_loader_test.go
│   ├── publish.go
│   ├── publish_test.go
│   ├── publish_unix.go
│   ├── publish_windows.go
│   ├── serve.go
│   ├── serve_test.go
│   ├── serve_unix.go
│   ├── serve_windows.go
│   ├── subscribe.go
│   ├── subscribe_darwin.go
│   ├── subscribe_test.go
│   ├── subscribe_unix.go
│   ├── subscribe_windows.go
│   ├── tier.go
│   ├── tier_test.go
│   ├── token.go
│   ├── token_test.go
│   ├── user.go
│   ├── user_test.go
│   ├── webpush.go
│   └── webpush_test.go
├── db/
│   ├── db.go
│   ├── pg/
│   │   ├── pg.go
│   │   └── pg_test.go
│   ├── test/
│   │   └── test.go
│   ├── types.go
│   └── util.go
├── docker-compose.yml
├── docs/
│   ├── _overrides/
│   │   └── main.html
│   ├── config.md
│   ├── contact.md
│   ├── contributing.md
│   ├── deprecations.md
│   ├── develop.md
│   ├── emojis.md
│   ├── examples.md
│   ├── faq.md
│   ├── hooks.py
│   ├── index.md
│   ├── install.md
│   ├── integrations.md
│   ├── known-issues.md
│   ├── privacy.md
│   ├── publish/
│   │   └── template-functions.md
│   ├── publish.md
│   ├── releases.md
│   ├── static/
│   │   ├── audio/
│   │   │   └── ntfy-phone-call.ogg
│   │   ├── css/
│   │   │   ├── config-generator.css
│   │   │   └── extra.css
│   │   ├── img/
│   │   │   ├── cli-subscribe-video-2.webm
│   │   │   └── cli-subscribe-video-3.webm
│   │   └── js/
│   │       ├── bcrypt.js
│   │       ├── config-generator.js
│   │       └── extra.js
│   ├── subscribe/
│   │   ├── api.md
│   │   ├── cli.md
│   │   ├── phone.md
│   │   ├── pwa.md
│   │   └── web.md
│   ├── terms.md
│   └── troubleshooting.md
├── examples/
│   ├── grafana-dashboard/
│   │   └── ntfy-grafana.json
│   ├── linux-desktop-notifications/
│   │   └── notify-desktop.sh
│   ├── publish-go/
│   │   └── main.go
│   ├── publish-php/
│   │   └── publish.php
│   ├── publish-python/
│   │   └── publish.py
│   ├── ssh-login-alert/
│   │   ├── ntfy-ssh-login.sh
│   │   └── pam_sshd
│   ├── subscribe-go/
│   │   └── main.go
│   ├── subscribe-php/
│   │   └── subscribe.php
│   ├── subscribe-python/
│   │   └── subscribe.py
│   ├── web-example-eventsource/
│   │   └── example-sse.html
│   └── web-example-websocket/
│       └── example-ws.html
├── go.mod
├── go.sum
├── log/
│   ├── event.go
│   ├── log.go
│   ├── log_test.go
│   └── types.go
├── main.go
├── message/
│   ├── cache.go
│   ├── cache_postgres.go
│   ├── cache_postgres_schema.go
│   ├── cache_sqlite.go
│   ├── cache_sqlite_schema.go
│   ├── cache_sqlite_test.go
│   └── cache_test.go
├── mkdocs.yml
├── model/
│   └── model.go
├── payments/
│   ├── payments.go
│   └── payments_dummy.go
├── requirements.txt
├── scripts/
│   ├── emoji-convert.sh
│   ├── emoji.json
│   ├── postinst.sh
│   ├── postrm.sh
│   ├── preinst.sh
│   └── prerm.sh
├── server/
│   ├── actions.go
│   ├── actions_test.go
│   ├── config.go
│   ├── config_test.go
│   ├── config_unix.go
│   ├── config_windows.go
│   ├── errors.go
│   ├── file_cache.go
│   ├── file_cache_test.go
│   ├── log.go
│   ├── mailer_emoji_map.json
│   ├── ntfy.service
│   ├── server.go
│   ├── server.yml
│   ├── server_account.go
│   ├── server_account_test.go
│   ├── server_admin.go
│   ├── server_admin_test.go
│   ├── server_firebase.go
│   ├── server_firebase_dummy.go
│   ├── server_firebase_test.go
│   ├── server_manager.go
│   ├── server_manager_test.go
│   ├── server_matrix.go
│   ├── server_matrix_test.go
│   ├── server_metrics.go
│   ├── server_middleware.go
│   ├── server_payments.go
│   ├── server_payments_dummy.go
│   ├── server_payments_test.go
│   ├── server_race_off_test.go
│   ├── server_race_on_test.go
│   ├── server_test.go
│   ├── server_twilio.go
│   ├── server_twilio_test.go
│   ├── server_webpush.go
│   ├── server_webpush_dummy.go
│   ├── server_webpush_test.go
│   ├── smtp_sender.go
│   ├── smtp_sender_test.go
│   ├── smtp_server.go
│   ├── smtp_server_test.go
│   ├── templates/
│   │   ├── alertmanager.yml
│   │   ├── github.yml
│   │   └── grafana.yml
│   ├── testdata/
│   │   ├── webhook_alertmanager_firing.json
│   │   ├── webhook_github_comment_created.json
│   │   ├── webhook_github_issue_opened.json
│   │   ├── webhook_github_pr_opened.json
│   │   ├── webhook_github_star_created.json
│   │   ├── webhook_github_watch_created.json
│   │   └── webhook_grafana_resolved.json
│   ├── topic.go
│   ├── topic_test.go
│   ├── types.go
│   ├── util.go
│   ├── util_test.go
│   └── visitor.go
├── test/
│   ├── server.go
│   ├── test.go
│   └── util.go
├── tools/
│   ├── fbsend/
│   │   ├── README.md
│   │   └── main.go
│   ├── loadgen/
│   │   └── main.go
│   ├── loadtest/
│   │   ├── go.mod
│   │   ├── go.sum
│   │   └── main.go
│   ├── pgimport/
│   │   ├── README.md
│   │   └── main.go
│   └── shrink-png.sh
├── user/
│   ├── manager.go
│   ├── manager_postgres.go
│   ├── manager_postgres_schema.go
│   ├── manager_sqlite.go
│   ├── manager_sqlite_schema.go
│   ├── manager_test.go
│   ├── types.go
│   ├── types_test.go
│   ├── util.go
│   └── util_test.go
├── util/
│   ├── batching_queue.go
│   ├── batching_queue_test.go
│   ├── content_type_writer.go
│   ├── content_type_writer_test.go
│   ├── embedfs/
│   │   └── test.txt
│   ├── embedfs.go
│   ├── embedfs_test.go
│   ├── gzip_handler.go
│   ├── gzip_handler_test.go
│   ├── limit.go
│   ├── limit_test.go
│   ├── lookup_cache.go
│   ├── lookup_cache_test.go
│   ├── peek.go
│   ├── peek_test.go
│   ├── sprig/
│   │   ├── LICENSE.txt
│   │   ├── crypto.go
│   │   ├── crypto_test.go
│   │   ├── date.go
│   │   ├── date_test.go
│   │   ├── defaults.go
│   │   ├── defaults_test.go
│   │   ├── dict.go
│   │   ├── dict_test.go
│   │   ├── doc.go
│   │   ├── example_test.go
│   │   ├── flow_control.go
│   │   ├── flow_control_test.go
│   │   ├── functions.go
│   │   ├── functions_linux_test.go
│   │   ├── functions_test.go
│   │   ├── list.go
│   │   ├── list_test.go
│   │   ├── numeric.go
│   │   ├── numeric_test.go
│   │   ├── reflect.go
│   │   ├── reflect_test.go
│   │   ├── regex.go
│   │   ├── regex_test.go
│   │   ├── strings.go
│   │   ├── strings_test.go
│   │   ├── url.go
│   │   └── url_test.go
│   ├── time.go
│   ├── time_test.go
│   ├── timeout_writer.go
│   ├── util.go
│   └── util_test.go
├── web/
│   ├── .eslintignore
│   ├── .eslintrc
│   ├── .prettierignore
│   ├── index.html
│   ├── package.json
│   ├── public/
│   │   ├── config.js
│   │   ├── static/
│   │   │   ├── css/
│   │   │   │   ├── app.css
│   │   │   │   └── fonts.css
│   │   │   └── langs/
│   │   │       ├── ar.json
│   │   │       ├── bg.json
│   │   │       ├── bn.json
│   │   │       ├── ca.json
│   │   │       ├── cs.json
│   │   │       ├── cu.json
│   │   │       ├── cy.json
│   │   │       ├── da.json
│   │   │       ├── de.json
│   │   │       ├── en.json
│   │   │       ├── eo.json
│   │   │       ├── es.json
│   │   │       ├── et.json
│   │   │       ├── fa.json
│   │   │       ├── fi.json
│   │   │       ├── fr.json
│   │   │       ├── gl.json
│   │   │       ├── he.json
│   │   │       ├── hu.json
│   │   │       ├── id.json
│   │   │       ├── it.json
│   │   │       ├── ja.json
│   │   │       ├── ko.json
│   │   │       ├── mk.json
│   │   │       ├── ms.json
│   │   │       ├── nb_NO.json
│   │   │       ├── nl.json
│   │   │       ├── pl.json
│   │   │       ├── pt.json
│   │   │       ├── pt_BR.json
│   │   │       ├── ro.json
│   │   │       ├── ru.json
│   │   │       ├── sk.json
│   │   │       ├── sq.json
│   │   │       ├── sv.json
│   │   │       ├── ta.json
│   │   │       ├── th.json
│   │   │       ├── tr.json
│   │   │       ├── uk.json
│   │   │       ├── uz.json
│   │   │       ├── vi.json
│   │   │       ├── zh_Hans.json
│   │   │       └── zh_Hant.json
│   │   └── sw.js
│   ├── src/
│   │   ├── app/
│   │   │   ├── AccountApi.js
│   │   │   ├── Api.js
│   │   │   ├── Connection.js
│   │   │   ├── ConnectionManager.js
│   │   │   ├── Notifier.js
│   │   │   ├── Poller.js
│   │   │   ├── Prefs.js
│   │   │   ├── Pruner.js
│   │   │   ├── Session.js
│   │   │   ├── SubscriptionManager.js
│   │   │   ├── UserManager.js
│   │   │   ├── VersionChecker.js
│   │   │   ├── actions.js
│   │   │   ├── config.js
│   │   │   ├── db.js
│   │   │   ├── emojis.js
│   │   │   ├── emojisMapped.js
│   │   │   ├── errors.js
│   │   │   ├── events.js
│   │   │   ├── i18n.js
│   │   │   ├── notificationUtils.js
│   │   │   └── utils.js
│   │   ├── components/
│   │   │   ├── Account.jsx
│   │   │   ├── ActionBar.jsx
│   │   │   ├── App.jsx
│   │   │   ├── AttachmentIcon.jsx
│   │   │   ├── AvatarBox.jsx
│   │   │   ├── DialogFooter.jsx
│   │   │   ├── EmojiPicker.jsx
│   │   │   ├── ErrorBoundary.jsx
│   │   │   ├── Login.jsx
│   │   │   ├── Messaging.jsx
│   │   │   ├── Navigation.jsx
│   │   │   ├── Notifications.jsx
│   │   │   ├── PopupMenu.jsx
│   │   │   ├── Pref.jsx
│   │   │   ├── Preferences.jsx
│   │   │   ├── PublishDialog.jsx
│   │   │   ├── RTLCacheProvider.jsx
│   │   │   ├── ReserveDialogs.jsx
│   │   │   ├── ReserveIcons.jsx
│   │   │   ├── ReserveTopicSelect.jsx
│   │   │   ├── Signup.jsx
│   │   │   ├── SubscribeDialog.jsx
│   │   │   ├── SubscriptionPopup.jsx
│   │   │   ├── UpgradeDialog.jsx
│   │   │   ├── hooks.js
│   │   │   ├── routes.js
│   │   │   ├── styles.js
│   │   │   └── theme.js
│   │   ├── index.jsx
│   │   └── registerSW.js
│   └── vite.config.js
└── webpush/
    ├── store.go
    ├── store_postgres.go
    ├── store_sqlite.go
    ├── store_test.go
    └── types.go

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

================================================
FILE: .dockerignore
================================================
dist
*/node_modules
Dockerfile*


================================================
FILE: .git-blame-ignore-revs
================================================
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view

# Run prettier (https://github.com/binwiederhier/ntfy/pull/746)
6f6a2d1f693070bf72e89d86748080e4825c9164
c87549e71a10bc789eac8036078228f06e515a8e
ca5d736a7169eb6b4b0d849e061d5bf9565dcc53
2e27f58963feb9e4d1c573d4745d07770777fa7d

# Run eslint (https://github.com/binwiederhier/ntfy/pull/748)
f558b4dbe9bb5b9e0e87fada1215de2558353173
8319f1cf26113167fb29fe12edaff5db74caf35f


================================================
FILE: .github/FUNDING.yml
================================================
github: [binwiederhier]
liberapay: ntfy


================================================
FILE: .github/ISSUE_TEMPLATE/1_bug_report.md
================================================
---
name: 🐛 Bug Report
about: Report any errors and problems
title: ''
labels: '🪲 bug'
assignees: ''

---

:lady_beetle: **Describe the bug**
<!-- A clear and concise description of the problem. -->

:computer: **Components impacted**
<!-- ntfy server, Android app, iOS app, web app  -->

:bulb: **Screenshots and/or logs**
<!-- 
If applicable, add screenshots or share logs help explain your problem.
To get logs from the ...
- ntfy server: Enable "log-level: trace" in your server.yml file
- Android app: Go to "Settings" -> "Record logs", then eventually "Copy/upload logs"
- web app: Press "F12" and find the "Console" window 
-->

:crystal_ball: **Additional context**
<!-- Add any other context about the problem here. -->


================================================
FILE: .github/ISSUE_TEMPLATE/2_enhancement_request.md
================================================
---
name: 💡 Feature/Enhancement Request
about: Got a great idea? Let us know!
title: ''
labels: 'enhancement'
assignees: ''

---

<!--

Before you submit, consider asking on Discord/Matrix instead. You'll usually get an answer
sooner, and there are more people there to help!

- Discord: https://discord.gg/cT7ECsZj9w
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org

-->

:bulb: **Idea**
<!-- Share your thoughts; try to be detailed if you can -->

:computer: **Target components**
<!-- Where should this feature/enhancement be added? -->
<!-- e.g. ntfy server, Android app, iOS app, web app -->



================================================
FILE: .github/ISSUE_TEMPLATE/3_tech_support.md
================================================
---
name: 🆘 I need help with ...
about: Installing ntfy, configuring the app, etc.
title: ''
labels: 'tech-support'
assignees: ''

---


<!--

STOP! 

This is not the right place to ask for help. Consider asking on Discord/Matrix instead. 
You'll usually get an answer sooner, and there are more people there to help!

- Discord: https://discord.gg/cT7ECsZj9w
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org

-->


================================================
FILE: .github/ISSUE_TEMPLATE/4_question.md
================================================
---
name: ❓ Question
about: Ask a question about ntfy
title: ''
labels: 'question'
assignees: ''

---

<!--

Before you submit, consider asking on Discord/Matrix instead. You'll usually get an answer
sooner, and there are more people there to help!

- Discord: https://discord.gg/cT7ECsZj9w
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org

-->

:question: **Question**
<!-- Go ahead and ask your question here :) -->


================================================
FILE: .github/workflows/build.yaml
================================================
name: build
on: [ push, pull_request ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Install Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.24.x'
      - name: Install node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: './web/package-lock.json'
      - name: Install dependencies
        run: make build-deps-ubuntu
      - name: Build all the things
        run: make build
      - name: Print build results and checksums
        run: make cli-build-results


================================================
FILE: .github/workflows/docs.yaml
================================================
name: docs
on:
  push:
    branches:
      - main
jobs:
  publish-docs:
    runs-on: ubuntu-latest
    steps:
      -
        name: Checkout ntfy code
        uses: actions/checkout@v3
      -
        name: Checkout docs pages code
        uses: actions/checkout@v3
        with:
          repository: binwiederhier/ntfy-docs.github.io
          path: build/ntfy-docs.github.io
          token: ${{secrets.NTFY_DOCS_PUSH_TOKEN}}
          # Expires after 1 year, re-generate via
          # User -> Settings -> Developer options -> Personal Access Tokens -> Fine Grained Token
      -
        name: Build docs
        run: make docs
      -
        name: Copy generated docs
        run: rsync -av --exclude CNAME --delete server/docs/ build/ntfy-docs.github.io/docs/
      -
        name: Publish docs
        run: |
          cd build/ntfy-docs.github.io
          git config user.name "GitHub Actions Bot"
          git config user.email "<actions@github.com>"          
          git add docs/
          git commit -m "Updated docs"
          git push origin main


================================================
FILE: .github/workflows/release.yaml
================================================
name: release
on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
  release:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_USER: ntfy
          POSTGRES_PASSWORD: ntfy
          POSTGRES_DB: ntfy_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U ntfy"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    env:
      NTFY_TEST_DATABASE_URL: "postgres://ntfy:ntfy@localhost:5432/ntfy_test?sslmode=disable"
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Install Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.24.x'
      - name: Install node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: './web/package-lock.json'
      - name: Docker login
        uses: docker/login-action@v2
        with:
          username: ${{ github.repository_owner }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}
      - name: Install dependencies
        run: make build-deps-ubuntu
      - name: Build and publish
        run: make release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: Print build results and checksums
        run: make cli-build-results


================================================
FILE: .github/workflows/test.yaml
================================================
name: test
on: [ push, pull_request ]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_USER: ntfy
          POSTGRES_PASSWORD: ntfy
          POSTGRES_DB: ntfy_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U ntfy"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    env:
      NTFY_TEST_DATABASE_URL: "postgres://ntfy:ntfy@localhost:5432/ntfy_test?sslmode=disable"
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Install Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.24.x'
      - name: Install node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: './web/package-lock.json'
      - name: Install dependencies
        run: make build-deps-ubuntu
      - name: Build docs (required for tests)
        run: make docs
      - name: Build web app (required for tests)
        run: make web
      - name: Run tests, formatting, vetting and linting
        run: make checkv
      - name: Run coverage
        run: make coverage


================================================
FILE: .gitignore
================================================
dist/
dev-dist/
build/
.idea/
.vscode/
*.swp
server/docs/
server/site/
tools/fbsend/fbsend
tools/pgimport/pgimport
tools/loadtest/loadtest
playground/
secrets/
*.iml
node_modules/
.DS_Store
__pycache__
web/dev-dist/
venv/
cmd/key-file.yaml


================================================
FILE: .gitpod.yml
================================================
tasks:
  - name: docs
    before: make docs-deps
    command: mkdocs serve
  - name: binary
    before: |
      npm install --global nodemon
      make cli-deps-static-sites
    command: |
      nodemon --watch './**/*.go' --ext go --signal SIGTERM --exec "CGO_ENABLED=1 go run main.go serve --listen-http :2586 --debug --base-url $(gp url 2586)"
    openMode: split-right
  - name: web
    before: make web-deps
    command: cd web && npm start
    openMode: split-right

vscode:
  extensions:
    - golang.go
    - ms-azuretools.vscode-docker

ports:
  - name: docs
    port: 8000
  - name: binary
    port: 2586
  - name: web
    port: 3000

================================================
FILE: .goreleaser.yml
================================================
version: 2
before:
  hooks:
    - go mod download
    - go mod tidy
builds:
  - id: ntfy_linux_amd64
    binary: ntfy
    env:
      - CGO_ENABLED=1 # required for go-sqlite3
    tags: [ sqlite_omit_load_extension,osusergo,netgo ]
    ldflags:
      - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
    goos: [ linux ]
    goarch: [ amd64 ]
  - id: ntfy_linux_armv6
    binary: ntfy
    env:
      - CGO_ENABLED=1 # required for go-sqlite3
      - CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
    tags: [ sqlite_omit_load_extension,osusergo,netgo ]
    ldflags:
      - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
    goos: [ linux ]
    goarch: [ arm ]
    goarm: [ 6 ]
  - id: ntfy_linux_armv7
    binary: ntfy
    env:
      - CGO_ENABLED=1 # required for go-sqlite3
      - CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
    tags: [ sqlite_omit_load_extension,osusergo,netgo ]
    ldflags:
      - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
    goos: [ linux ]
    goarch: [ arm ]
    goarm: [ 7 ]
  - id: ntfy_linux_arm64
    binary: ntfy
    env:
      - CGO_ENABLED=1 # required for go-sqlite3
      - CC=aarch64-linux-gnu-gcc # apt install gcc-aarch64-linux-gnu
    tags: [ sqlite_omit_load_extension,osusergo,netgo ]
    ldflags:
      - "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
    goos: [ linux ]
    goarch: [ arm64 ]
  - id: ntfy_windows_amd64
    binary: ntfy
    env:
      - CGO_ENABLED=1 # required for go-sqlite3
      - CC=x86_64-w64-mingw32-gcc # apt install gcc-mingw-w64-x86-64
    tags: [ sqlite_omit_load_extension,osusergo,netgo ]
    ldflags:
      - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
    goos: [ windows ]
    goarch: [amd64 ]
  -
    id: ntfy_darwin_all
    binary: ntfy
    env:
      - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
    tags: [ noserver ] # don't include server files
    ldflags:
      - "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
    goos: [ darwin ]
    goarch: [ amd64, arm64 ] # will be combined to "universal binary" (see below)
nfpms:
  - package_name: ntfy
    homepage: https://heckel.io/ntfy
    maintainer: Philipp C. Heckel <philipp.heckel@gmail.com>
    description: Simple pub-sub notification service
    license: Apache 2.0
    formats:
      - deb
      - rpm
    bindir: /usr/bin
    contents:
      - src: server/server.yml
        dst: /etc/ntfy/server.yml
        type: "config|noreplace"
      - src: server/ntfy.service
        dst: /lib/systemd/system/ntfy.service
      - src: client/client.yml
        dst: /etc/ntfy/client.yml
        type: "config|noreplace"
      - src: client/ntfy-client.service
        dst: /lib/systemd/system/ntfy-client.service
      - src: client/user/ntfy-client.service
        dst: /lib/systemd/user/ntfy-client.service
      - dst: /var/cache/ntfy
        type: dir
      - dst: /var/cache/ntfy/attachments
        type: dir
      - dst: /var/lib/ntfy
        type: dir
      - dst: /usr/share/ntfy/logo.png
        src: web/public/static/images/ntfy.png
    scripts:
      preinstall: "scripts/preinst.sh"
      postinstall: "scripts/postinst.sh"
      preremove: "scripts/prerm.sh"
      postremove: "scripts/postrm.sh"
archives:
  - id: ntfy_linux
    ids:
      - ntfy_linux_amd64
      - ntfy_linux_armv6
      - ntfy_linux_armv7
      - ntfy_linux_arm64
    wrap_in_directory: true
    files:
      - LICENSE
      - README.md
      - server/server.yml
      - server/ntfy.service
      - client/client.yml
      - client/ntfy-client.service
      - client/user/ntfy-client.service
  - id: ntfy_windows
    ids:
      - ntfy_windows_amd64
    formats: [ zip ]
    wrap_in_directory: true
    files:
      - LICENSE
      - README.md
      - client/client.yml
  - id: ntfy_darwin
    ids:
      - ntfy_darwin_all
    wrap_in_directory: true
    files:
      - LICENSE
      - README.md
      - client/client.yml
universal_binaries:
  - id: ntfy_darwin_all
    replace: true
    name_template: ntfy
checksum:
  name_template: 'checksums.txt'
snapshot:
  version_template: "{{ .Tag }}-next"
changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'
dockers:
  - image_templates:
      - &amd64_image "binwiederhier/ntfy:{{ .Tag }}-amd64"
    use: buildx
    dockerfile: Dockerfile
    goarch: amd64
    build_flag_templates:
      - "--platform=linux/amd64"
  - image_templates:
      - &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
    use: buildx
    dockerfile: Dockerfile-arm
    goarch: arm64
    build_flag_templates:
      - "--platform=linux/arm64/v8"
  - image_templates:
      - &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
    use: buildx
    dockerfile: Dockerfile-arm
    goarch: arm
    goarm: 7
    build_flag_templates:
      - "--platform=linux/arm/v7"
  - image_templates:
      - &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
    use: buildx
    dockerfile: Dockerfile-arm
    goarch: arm
    goarm: 6
    build_flag_templates:
      - "--platform=linux/arm/v6"
docker_manifests:
  - name_template: "binwiederhier/ntfy:latest"
    image_templates:
      - *amd64_image
      - *arm64v8_image
      - *armv7_image
      - *armv6_image
  - name_template: "binwiederhier/ntfy:{{ .Tag }}"
    image_templates:
      - *amd64_image
      - *arm64v8_image
      - *armv7_image
      - *armv6_image
  - name_template: "binwiederhier/ntfy:v{{ .Major }}"
    image_templates:
      - *amd64_image
      - *arm64v8_image
      - *armv7_image
      - *armv6_image
  - name_template: "binwiederhier/ntfy:v{{ .Major }}.{{ .Minor }}"
    image_templates:
      - *amd64_image
      - *arm64v8_image
      - *armv7_image
      - *armv6_image


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
  community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or advances of
  any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
  without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement via Discord/Matrix (binwiederhier),
or email (ntfy@heckel.io). All complaints will be reviewed and investigated promptly 
and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series of
actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within the
community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].

Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].

For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].

[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations



================================================
FILE: Dockerfile
================================================
FROM alpine

LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"

RUN apk add --no-cache tzdata
COPY ntfy /usr/bin

EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]


================================================
FILE: Dockerfile-arm
================================================
FROM alpine

LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"

# Alpine does not support adding "tzdata" on ARM anymore, see
# https://github.com/binwiederhier/ntfy/issues/894

COPY ntfy /usr/bin

EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]


================================================
FILE: Dockerfile-build
================================================
FROM golang:1.24-bullseye as builder

ARG VERSION=dev
ARG COMMIT=unknown
ARG NODE_MAJOR=18

RUN apt-get update && apt-get install -y \
       build-essential ca-certificates curl gnupg \
    && mkdir -p /etc/apt/keyrings \
    && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
    && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \
    && apt-get update \
    && apt-get install -y \
      python3-pip \
      python3-venv \
      nodejs \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
ADD Makefile .

# docs
ADD ./requirements.txt .
RUN make docs-deps
ADD ./mkdocs.yml .
ADD ./docs ./docs
RUN make docs-build

# web
ADD ./web/package.json ./web/package-lock.json ./web/
RUN make web-deps
ADD ./web ./web
RUN make web-build

# cli & server
ADD go.mod go.sum main.go ./
ADD ./client ./client
ADD ./cmd ./cmd
ADD ./log ./log
ADD ./server ./server
ADD ./user ./user
ADD ./util ./util
ADD ./payments ./payments
RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server

FROM alpine

ARG VERSION=dev

LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
LABEL org.opencontainers.image.version="$VERSION"

COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy

EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2021 Philipp C. Heckel

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: LICENSE.GPLv2
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    ntfy
    Copyright (C) 2021 Philipp C. Heckel

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.



================================================
FILE: Makefile
================================================
MAKEFLAGS := --jobs=1
NPM := npm
PYTHON := python3
PIP := pip3
VERSION := $(shell git describe --tag)
COMMIT := $(shell git rev-parse --short HEAD)

.PHONY:

help:
	@echo "Typical commands (more see below):"
	@echo "  make build                      - Build web app, documentation and server/client (sloowwww)"
	@echo "  make cli-linux-amd64            - Build server/client binary (amd64, no web app or docs)"
	@echo "  make install-linux-amd64        - Install ntfy binary to /usr/bin/ntfy (amd64)"
	@echo "  make web                        - Build the web app"
	@echo "  make docs                       - Build the documentation"
	@echo "  make check                      - Run all tests, vetting/formatting checks and linters"
	@echo
	@echo "Build everything:"
	@echo "  make build                      - Build web app, documentation and server/client"
	@echo "  make clean                      - Clean build/dist folders"
	@echo
	@echo "Build server & client (using GoReleaser, not release version):"
	@echo "  make cli                        - Build server & client (all architectures)"
	@echo "  make cli-linux-amd64            - Build server & client (Linux, amd64 only)"
	@echo "  make cli-linux-armv6            - Build server & client (Linux, armv6 only)"
	@echo "  make cli-linux-armv7            - Build server & client (Linux, armv7 only)"
	@echo "  make cli-linux-arm64            - Build server & client (Linux, arm64 only)"
	@echo "  make cli-windows-amd64          - Build client (Windows, amd64 only)"
	@echo "  make cli-darwin-all             - Build client (macOS, arm64+amd64 universal binary)"
	@echo
	@echo "Build server & client (without GoReleaser):"
	@echo "  make cli-linux-server           - Build client & server (no GoReleaser, current arch, Linux)"
	@echo "  make cli-darwin-server          - Build client & server (no GoReleaser, current arch, macOS)"
	@echo "  make cli-windows-server         - Build client & server (no GoReleaser, amd64 only, Windows)"
	@echo "  make cli-client                 - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
	@echo
	@echo "Build dev Docker:"
	@echo "  make docker-dev                 - Build client & server for current architecture using Docker only"
	@echo
	@echo "Build web app:"
	@echo "  make web                        - Build the web app"
	@echo "  make web-deps                   - Install web app dependencies (npm install the universe)"
	@echo "  make web-build                  - Actually build the web app"
	@echo "  make web-lint                   - Run eslint on the web app"
	@echo "  make web-fmt                    - Run prettier on the web app"
	@echo "  make web-fmt-check              - Run prettier on the web app, but don't change anything"
	@echo
	@echo "Build documentation:"
	@echo "  make docs                       - Build the documentation"
	@echo "  make docs-deps                  - Install Python dependencies (pip3 install)"
	@echo "  make docs-build                 - Actually build the documentation"
	@echo
	@echo "Test/check:"
	@echo "  make test                       - Run tests"
	@echo "  make race                       - Run tests with -race flag"
	@echo "  make coverage                   - Run tests and show coverage"
	@echo "  make coverage-html              - Run tests and show coverage (as HTML)"
	@echo "  make coverage-upload            - Upload coverage results to codecov.io"
	@echo
	@echo "Lint/format:"
	@echo "  make fmt                        - Run 'go fmt'"
	@echo "  make fmt-check                  - Run 'go fmt', but don't change anything"
	@echo "  make vet                        - Run 'go vet'"
	@echo "  make lint                       - Run 'golint'"
	@echo "  make staticcheck                - Run 'staticcheck'"
	@echo
	@echo "Releasing:"
	@echo "  make release                    - Create a release"
	@echo "  make release-snapshot           - Create a test release"
	@echo
	@echo "Install locally (requires sudo):"
	@echo "  make install-linux-amd64        - Copy amd64 binary from dist/ to /usr/bin/ntfy"
	@echo "  make install-linux-armv6        - Copy armv6 binary from dist/ to /usr/bin/ntfy"
	@echo "  make install-linux-armv7        - Copy armv7 binary from dist/ to /usr/bin/ntfy"
	@echo "  make install-linux-arm64        - Copy arm64 binary from dist/ to /usr/bin/ntfy"
	@echo "  make install-linux-deb-amd64    - Install .deb from dist/ (amd64 only)"
	@echo "  make install-linux-deb-armv6    - Install .deb from dist/ (armv6 only)"
	@echo "  make install-linux-deb-armv7    - Install .deb from dist/ (armv7 only)"
	@echo "  make install-linux-deb-arm64    - Install .deb from dist/ (arm64 only)"


# Building everything

clean: .PHONY
	rm -rf dist build server/docs server/site

build: web docs cli

update: web-deps-update cli-deps-update docs-deps-update
	docker pull alpine

docker-dev:
	docker build \
		--file ./Dockerfile-build \
		--tag binwiederhier/ntfy:$(VERSION) \
		--tag binwiederhier/ntfy:dev \
		--build-arg VERSION=$(VERSION) \
		--build-arg COMMIT=$(COMMIT) \
		./


# Ubuntu-specific

build-deps-ubuntu:
	sudo apt-get update
	sudo apt-get install -y \
		curl \
		gcc-aarch64-linux-gnu \
		gcc-arm-linux-gnueabi \
		gcc-mingw-w64-x86-64 \
		python3 \
		python3-venv \
		jq
	which pip3 || sudo apt-get install -y python3-pip


# Documentation

docs: docs-deps docs-build

docs-venv: .PHONY
	$(PYTHON) -m venv ./venv

docs-build: docs-venv
	(. venv/bin/activate && $(PYTHON) -m mkdocs build)

docs-deps: docs-venv
	(. venv/bin/activate && $(PIP) install -r requirements.txt)

docs-deps-update: .PHONY
	(. venv/bin/activate && $(PIP) install -r requirements.txt --upgrade)


# Web app

web: web-deps web-build

web-build:
	cd web \
		&& $(NPM) run build \
		&& mv build/index.html build/app.html \
		&& rm -rf ../server/site \
		&& mv build ../server/site \
		&& rm \
			../server/site/config.js

web-deps:
	cd web && $(NPM) install
	# If this fails for .svg files, optimize them with svgo

web-deps-update:
	cd web && $(NPM) update

web-fmt:
	cd web && $(NPM) run format

web-fmt-check:
	cd web && $(NPM) run format:check

web-lint:
	cd web && $(NPM) run lint

# Main server/client build

cli: cli-deps
	goreleaser build --snapshot --clean

cli-linux-amd64: cli-deps-static-sites
	goreleaser build --snapshot --clean --id ntfy_linux_amd64

cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
	goreleaser build --snapshot --clean --id ntfy_linux_armv6

cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
	goreleaser build --snapshot --clean --id ntfy_linux_armv7

cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
	goreleaser build --snapshot --clean --id ntfy_linux_arm64

cli-windows-amd64: cli-deps-static-sites
	goreleaser build --snapshot --clean --id ntfy_windows_amd64

cli-darwin-all: cli-deps-static-sites
	goreleaser build --snapshot --clean --id ntfy_darwin_all

cli-linux-server: cli-deps-static-sites
	# This is a target to build the CLI (including the server) manually.
	# Use this for development, if you really don't want to install GoReleaser ...
	mkdir -p dist/ntfy_linux_server server/docs
	CGO_ENABLED=1 go build \
		-o dist/ntfy_linux_server/ntfy \
		-tags sqlite_omit_load_extension,osusergo,netgo \
		-ldflags \
		"-linkmode=external -extldflags=-static -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"

cli-darwin-server: cli-deps-static-sites
	# This is a target to build the CLI (including the server) manually.
	# Use this for macOS/iOS development, so you have a local server to test with.
	mkdir -p dist/ntfy_darwin_server server/docs
	CGO_ENABLED=1 go build \
		-o dist/ntfy_darwin_server/ntfy \
		-tags sqlite_omit_load_extension,osusergo,netgo \
		-ldflags \
		"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"

cli-windows-server: cli-deps-static-sites
	# This is a target to build the CLI (including the server) for Windows.
	# Use this for Windows development, if you really don't want to install GoReleaser ...
	mkdir -p dist/ntfy_windows_server server/docs
	CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build \
		-o dist/ntfy_windows_server/ntfy.exe \
		-tags sqlite_omit_load_extension,osusergo,netgo \
		-ldflags \
		"-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"

cli-client: cli-deps-static-sites
	# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
	# Use this for development, if you really don't want to install GoReleaser ...
	mkdir -p dist/ntfy_client server/docs
	CGO_ENABLED=0 go build \
		-o dist/ntfy_client/ntfy \
		-tags noserver \
		-ldflags \
		"-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"

cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc

cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64 cli-deps-gcc-windows

cli-deps-static-sites:
	mkdir -p server/docs server/site
	touch server/docs/index.html server/site/app.html

cli-deps-all:
	go install github.com/goreleaser/goreleaser/v2@latest

cli-deps-gcc-armv6-armv7:
	which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }

cli-deps-gcc-arm64:
	which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }

cli-deps-gcc-windows:
	which x86_64-w64-mingw32-gcc || { echo "ERROR: Windows cross compiler not installed. On Ubuntu, run: apt install gcc-mingw-w64-x86-64"; exit 1; }

cli-deps-update:
	go get -u
	go mod tidy
	go install honnef.co/go/tools/cmd/staticcheck@latest
	go install golang.org/x/lint/golint@latest
	go install github.com/goreleaser/goreleaser/v2@latest

cli-build-results:
	cat dist/config.yaml
	[ -f dist/artifacts.json ] && cat dist/artifacts.json | jq . || true
	[ -f dist/metadata.json ] && cat dist/metadata.json | jq . || true
	[ -f dist/checksums.txt ] && cat dist/checksums.txt || true
	find dist -maxdepth 2 -type f \
		\( -name '*.deb' -or -name '*.rpm' -or -name '*.zip' -or -name '*.tar.gz' -or -name 'ntfy' \) \
		-and -not -path 'dist/goreleaserdocker*' \
		-exec sha256sum {} \;

# Test/check targets

check: test web-fmt-check fmt-check vet web-lint lint staticcheck

checkv: testv web-fmt-check fmt-check vet web-lint lint staticcheck

test: .PHONY
	go test $(shell go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./... | grep -vE 'ntfy/v2/(test|examples|tools)')

testv: .PHONY
	go test -v $(shell go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./... | grep -vE 'ntfy/v2/(test|examples|tools)')

race: .PHONY
	go test -v -race $(shell go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./... | grep -vE 'ntfy/v2/(test|examples|tools)')

coverage:
	mkdir -p build/coverage
	go test -v -race -coverprofile=build/coverage/coverage.txt -covermode=atomic $(shell go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./... | grep -vE 'ntfy/v2/(test|examples|tools|web)')
	go tool cover -func build/coverage/coverage.txt

coverage-html:
	mkdir -p build/coverage
	go test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic $(shell go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./... | grep -vE 'ntfy/v2/(test|examples|tools)')
	go tool cover -html build/coverage/coverage.txt

coverage-upload:
	cd build/coverage && (curl -s https://codecov.io/bash | bash)


# Lint/formatting targets

fmt: web-fmt
	gofmt -s -w .

fmt-check:
	test -z $(shell gofmt -l .)

vet:
	go vet ./...

lint:
	which golint || go install golang.org/x/lint/golint@latest
	go list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status

staticcheck: .PHONY
	rm -rf build/staticcheck
	which staticcheck || go install honnef.co/go/tools/cmd/staticcheck@latest
	mkdir -p build/staticcheck
	ln -s "go" build/staticcheck/go
	PATH="$(PWD)/build/staticcheck:$(PATH)" staticcheck ./...
	rm -rf build/staticcheck


# Releasing targets

release: clean cli-deps release-checks docs web check
	goreleaser release --clean

release-snapshot: clean cli-deps docs web check
	goreleaser release --snapshot --clean

release-checks:
	$(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))
	if ! grep -q $(LATEST_TAG) docs/install.md; then\
	 	echo "ERROR: Must update docs/install.md with latest tag first.";\
	 	exit 1;\
	fi
	if ! grep -q $(LATEST_TAG) docs/releases.md; then\
		echo "ERROR: Must update docs/releases.md with latest tag first.";\
		exit 1;\
	fi
	if [ -n "$(shell git status -s)" ]; then\
	  echo "ERROR: Git repository is in an unclean state.";\
	  exit 1;\
	fi


# Installing targets

install-linux-amd64: remove-binary
	sudo cp -a dist/ntfy_linux_amd64_linux_amd64_v1/ntfy /usr/bin/ntfy

install-linux-armv6: remove-binary
	sudo cp -a dist/ntfy_linux_armv6_linux_arm_6/ntfy /usr/bin/ntfy

install-linux-armv7: remove-binary
	sudo cp -a dist/ntfy_linux_armv7_linux_arm_7/ntfy /usr/bin/ntfy

install-linux-arm64: remove-binary
	sudo cp -a dist/ntfy_linux_arm64_linux_arm64/ntfy /usr/bin/ntfy

remove-binary:
	sudo rm -f /usr/bin/ntfy

install-linux-amd64-deb: purge-package
	sudo dpkg -i dist/ntfy_*_linux_amd64.deb

install-linux-armv6-deb: purge-package
	sudo dpkg -i dist/ntfy_*_linux_armv6.deb

install-linux-armv7-deb: purge-package
	sudo dpkg -i dist/ntfy_*_linux_armv7.deb

install-linux-arm64-deb: purge-package
	sudo dpkg -i dist/ntfy_*_linux_arm64.deb

purge-package:
	sudo systemctl stop ntfy || true
	sudo apt-get purge ntfy || true


================================================
FILE: README.md
================================================
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://go.warp.dev/ntfy">
  <img alt="Warp sponsorship" width="400" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
</a>

### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/ntfy)
[Available for MacOS, Linux, & Windows](https://go.warp.dev/ntfy)<br>
</div>
<hr>

![ntfy](web/public/static/images/ntfy.png)

# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest)
[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy/v2)
[![Tests](https://github.com/binwiederhier/ntfy/workflows/test/badge.svg)](https://github.com/binwiederhier/ntfy/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/binwiederhier/ntfy)](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
[![codecov](https://codecov.io/gh/binwiederhier/ntfy/branch/main/graph/badge.svg?token=A597KQ463G)](https://codecov.io/gh/binwiederhier/ntfy)
[![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w)
[![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org)
[![Matrix space](https://img.shields.io/matrix/ntfy-space:matrix.org?label=Matrix+space)](https://matrix.to/#/#ntfy-space:matrix.org)
[![Healthcheck](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)](https://ntfy.statuspage.io/)
[![Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/binwiederhier/ntfy)

**ntfy** (pronounced "*notify*") is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) 
notification service. With ntfy, you can **send notifications to your phone or desktop via scripts** from any computer, 
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do 
so since ntfy is open source.

You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open-source Android app](https://github.com/binwiederhier/ntfy-android)
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).

<p>
  <a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img height="50" src="docs/static/img/badge-googleplay.png"></a>
  <a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="docs/static/img/badge-fdroid.svg"></a>
  <a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img height="50" src="docs/static/img/badge-appstore.png"></a>
</p>

<p>
  <img src=".github/images/screenshot-curl.png" height="180">
  <img src=".github/images/screenshot-web-detail.png" height="180">
  <img src=".github/images/screenshot-phone-main.jpg" height="180">
  <img src=".github/images/screenshot-phone-detail.jpg" height="180">
  <img src=".github/images/screenshot-phone-notification.jpg" height="180">
</p>

## [ntfy Pro](https://ntfy.sh/app) 💸 🎉
I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of 
ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $5/month**.
You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy).
I would be very humbled by your sponsorship. ❤️ 

## **[Documentation](https://ntfy.sh/docs/)**

[Getting started](https://ntfy.sh/docs/) |
[Android/iOS](https://ntfy.sh/docs/subscribe/phone/) |
[API](https://ntfy.sh/docs/publish/) |
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
[Building](https://ntfy.sh/docs/develop/)

## Chat/forum
There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
works best for you:

* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs

## Announcements/beta testers
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements) 
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas,
join Discord/Matrix (I'll eventually make a testing channel in Google Play).

## Sponsors
If you'd like to support the ntfy maintainers, please consider donating to [GitHub Sponsors](https://github.com/sponsors/binwiederhier) or
and [Liberapay](https://liberapay.com/ntfy). We would be humbled if you helped carry the server and developer 
account costs. Even small donations are very much appreciated. 

Thank you to our commercial sponsors, who help keep the service running and the development going:

<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>

<a href="https://www.magicbell.com/?utm_source=ntfy"><img src="assets/sponsors/magicbell.png" width="180px"></a>

<a href="https://go.warp.dev/ntfy"><img src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Logos/Warp-Wordmark-Black.png" width="160px"></a>

And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy:

<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
<a href="https://github.com/nickexyz"><img src="https://github.com/nickexyz.png" width="40px" /></a>
<a href="https://github.com/qcasey"><img src="https://github.com/qcasey.png" width="40px" /></a>
<a href="https://github.com/mckay115"><img src="https://github.com/mckay115.png" width="40px" /></a>
<a href="https://github.com/Salamafet"><img src="https://github.com/Salamafet.png" width="40px" /></a>
<a href="https://github.com/codinghipster"><img src="https://github.com/codinghipster.png" width="40px" /></a>
<a href="https://github.com/HinFort"><img src="https://github.com/HinFort.png" width="40px" /></a>
<a href="https://github.com/Lexevolution"><img src="https://github.com/Lexevolution.png" width="40px" /></a>
<a href="https://github.com/johnnyip"><img src="https://github.com/johnnyip.png" width="40px" /></a>
<a href="https://github.com/JonDerThan"><img src="https://github.com/JonDerThan.png" width="40px" /></a>
<a href="https://github.com/12nick12"><img src="https://github.com/12nick12.png" width="40px" /></a>
<a href="https://github.com/eanplatter"><img src="https://github.com/eanplatter.png" width="40px" /></a>
<a href="https://github.com/fnoelscher"><img src="https://github.com/fnoelscher.png" width="40px" /></a>
<a href="https://github.com/bnorick"><img src="https://github.com/bnorick.png" width="40px" /></a>
<a href="https://github.com/snh"><img src="https://github.com/snh.png" width="40px" /></a>
<a href="https://github.com/hen-x"><img src="https://github.com/hen-x.png" width="40px" /></a>
<a href="https://github.com/JamieGoodson"><img src="https://github.com/JamieGoodson.png" width="40px" /></a>
<a href="https://github.com/cremesk"><img src="https://github.com/cremesk.png" width="40px" /></a>
<a href="https://github.com/dangowans"><img src="https://github.com/dangowans.png" width="40px" /></a>
<a href="https://github.com/mnault"><img src="https://github.com/mnault.png" width="40px" /></a>
<a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a>
<a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a>
<a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
<a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a>
<a href="https://github.com/skrollme"><img src="https://github.com/skrollme.png" width="40px" /></a>
<a href="https://github.com/gergepalfi"><img src="https://github.com/gergepalfi.png" width="40px" /></a>
<a href="https://github.com/tonyakwei"><img src="https://github.com/tonyakwei.png" width="40px" /></a>
<a href="https://github.com/crosbyh"><img src="https://github.com/crosbyh.png" width="40px" /></a>
<a href="https://github.com/mdlnr"><img src="https://github.com/mdlnr.png" width="40px" /></a>
<a href="https://github.com/p-samuel"><img src="https://github.com/p-samuel.png" width="40px" /></a>
<a href="https://github.com/zugaldia"><img src="https://github.com/zugaldia.png" width="40px" /></a>
<a href="https://github.com/NathanSweet"><img src="https://github.com/NathanSweet.png" width="40px" /></a>
<a href="https://github.com/msdeibel"><img src="https://github.com/msdeibel.png" width="40px" /></a>
<a href="https://github.com/ksurl"><img src="https://github.com/ksurl.png" width="40px" /></a>
<a href="https://github.com/CodingTimeDEV"><img src="https://github.com/CodingTimeDEV.png" width="40px" /></a>
<a href="https://github.com/Terrormixer3000"><img src="https://github.com/Terrormixer3000.png" width="40px" /></a>
<a href="https://github.com/voroskoi"><img src="https://github.com/voroskoi.png" width="40px" /></a>
<a href="https://github.com/Nickwasused"><img src="https://github.com/Nickwasused.png" width="40px" /></a>
<a href="https://github.com/bahur142"><img src="https://github.com/bahur142.png" width="40px" /></a>
<a href="https://github.com/vinhdizzo"><img src="https://github.com/vinhdizzo.png" width="40px" /></a>
<a href="https://github.com/Ge0rg3"><img src="https://github.com/Ge0rg3.png" width="40px" /></a>
<a href="https://github.com/biopsin"><img src="https://github.com/biopsin.png" width="40px" /></a>
<a href="https://github.com/thebino"><img src="https://github.com/thebino.png" width="40px" /></a>
<a href="https://github.com/sky4055"><img src="https://github.com/sky4055.png" width="40px" /></a>
<a href="https://github.com/julianlam"><img src="https://github.com/julianlam.png" width="40px" /></a>
<a href="https://github.com/andreapx"><img src="https://github.com/andreapx.png" width="40px" /></a>
<a href="https://github.com/billycao"><img src="https://github.com/billycao.png" width="40px" /></a>
<a href="https://github.com/zoic21"><img src="https://github.com/zoic21.png" width="40px" /></a>
<a href="https://github.com/IanKulin"><img src="https://github.com/IanKulin.png" width="40px" /></a>
<a href="https://github.com/Joachim256"><img src="https://github.com/Joachim256.png" width="40px" /></a>
<a href="https://github.com/overtone1000"><img src="https://github.com/overtone1000.png" width="40px" /></a>
<a href="https://github.com/oakd"><img src="https://github.com/oakd.png" width="40px" /></a>
<a href="https://github.com/KucharczykL"><img src="https://github.com/KucharczykL.png" width="40px" /></a>
<a href="https://github.com/hansbickhofe"><img src="https://github.com/hansbickhofe.png" width="40px" /></a>
<a href="https://github.com/caseodilla"><img src="https://github.com/caseodilla.png" width="40px" /></a>
<a href="https://github.com/0xAF"><img src="https://github.com/0xAF.png" width="40px" /></a>
<a href="https://github.com/soonoo"><img src="https://github.com/soonoo.png" width="40px" /></a>
<a href="https://github.com/nichu42"><img src="https://github.com/nichu42.png" width="40px" /></a>
<a href="https://github.com/samliebow"><img src="https://github.com/samliebow.png" width="40px" /></a>
<a href="https://github.com/johman10"><img src="https://github.com/johman10.png" width="40px" /></a>
<a href="https://github.com/R-Gld"><img src="https://github.com/R-Gld.png" width="40px" /></a>
<a href="https://github.com/FingerlessGlov3s"><img src="https://github.com/FingerlessGlov3s.png" width="40px" /></a>
<a href="https://github.com/Twisterado"><img src="https://github.com/Twisterado.png" width="40px" /></a>
<a href="https://github.com/ScrumpyJack"><img src="https://github.com/ScrumpyJack.png" width="40px" /></a>
<a href="https://github.com/andrejarrell"><img src="https://github.com/andrejarrell.png" width="40px" /></a>
<a href="https://github.com/oaustegard"><img src="https://github.com/oaustegard.png" width="40px" /></a>
<a href="https://github.com/CreativeWarlock"><img src="https://github.com/CreativeWarlock.png" width="40px" /></a>
<a href="https://github.com/darkdragon-001"><img src="https://github.com/darkdragon-001.png" width="40px" /></a>
<a href="https://github.com/jonathan-kosgei"><img src="https://github.com/jonathan-kosgei.png" width="40px" /></a>
<a href="https://github.com/KevinWang15"><img src="https://github.com/KevinWang15.png" width="40px" /></a>
<a href="https://github.com/darkmattercoder"><img src="https://github.com/darkmattercoder.png" width="40px" /></a>
<a href="https://github.com/bmcgonag"><img src="https://github.com/bmcgonag.png" width="40px" /></a>
<a href="https://github.com/skorokithakis"><img src="https://github.com/skorokithakis.png" width="40px" /></a>
<a href="https://github.com/eenturk"><img src="https://github.com/eenturk.png" width="40px" /></a>
<a href="https://github.com/spirossi"><img src="https://github.com/spirossi.png" width="40px" /></a>
<a href="https://github.com/teomarcdhio"><img src="https://github.com/teomarcdhio.png" width="40px" /></a>
<a href="https://github.com/MarcMichalsky"><img src="https://github.com/MarcMichalsky.png" width="40px" /></a>
<a href="https://github.com/LuckVintage"><img src="https://github.com/LuckVintage.png" width="40px" /></a>
<a href="https://github.com/spartan"><img src="https://github.com/spartan.png" width="40px" /></a>
<a href="https://github.com/alexandzors"><img src="https://github.com/alexandzors.png" width="40px" /></a>
<a href="https://github.com/dkramer95"><img src="https://github.com/dkramer95.png" width="40px" /></a>
<a href="https://github.com/YezGotIt"><img src="https://github.com/YezGotIt.png" width="40px" /></a>
<a href="https://github.com/thomasskou"><img src="https://github.com/thomasskou.png" width="40px" /></a>
<a href="https://github.com/surfernv"><img src="https://github.com/surfernv.png" width="40px" /></a>
<a href="https://github.com/richardleach"><img src="https://github.com/richardleach.png" width="40px" /></a>
<a href="https://github.com/bear"><img src="https://github.com/bear.png" width="40px" /></a>
<a href="https://github.com/cminter"><img src="https://github.com/cminter.png" width="40px" /></a>
<a href="https://github.com/bahur142"><img src="https://github.com/bahur142.png" width="40px" /></a>
<a href="https://github.com/pgwiebes"><img src="https://github.com/pgwiebes.png" width="40px" /></a>
<a href="https://github.com/ralhei"><img src="https://github.com/ralhei.png" width="40px" /></a>
<a href="https://github.com/TechMDW"><img src="https://github.com/TechMDW.png" width="40px" /></a>
<a href="https://github.com/ubipo"><img src="https://github.com/ubipo.png" width="40px" /></a>
<a href="https://github.com/tka85"><img src="https://github.com/tka85.png" width="40px" /></a>
<a href="https://github.com/beekeeb"><img src="https://github.com/beekeeb.png" width="40px" /></a>
<a href="https://github.com/Emiliaaah"><img src="https://github.com/Emiliaaah.png" width="40px" /></a>
<a href="https://github.com/zark0s"><img src="https://github.com/zark0s.png" width="40px" /></a>
<a href="https://github.com/tomershvueli"><img src="https://github.com/tomershvueli.png" width="40px" /></a>
<a href="https://github.com/CataIana"><img src="https://github.com/CataIana.png" width="40px" /></a>
<a href="https://github.com/ajay-actuary"><img src="https://github.com/ajay-actuary.png" width="40px" /></a>
<a href="https://github.com/mursec"><img src="https://github.com/mursec.png" width="40px" /></a>
<a href="https://github.com/FrameXX"><img src="https://github.com/FrameXX.png" width="40px" /></a>
<a href="https://github.com/vovayartsev"><img src="https://github.com/vovayartsev.png" width="40px" /></a>
<a href="https://github.com/dwain-lab"><img src="https://github.com/dwain-lab.png" width="40px" /></a>
<a href="https://github.com/brookmg"><img src="https://github.com/brookmg.png" width="40px" /></a>
<a href="https://github.com/siebej"><img src="https://github.com/siebej.png" width="40px" /></a>
<a href="https://github.com/rxsantos"><img src="https://github.com/rxsantos.png" width="40px" /></a>
<a href="https://github.com/hermannx5"><img src="https://github.com/hermannx5.png" width="40px" /></a>
<a href="https://github.com/rwxd"><img src="https://github.com/rwxd.png" width="40px" /></a>
<a href="https://github.com/Integral-Tech"><img src="https://github.com/Integral-Tech.png" width="40px" /></a>
<a href="https://github.com/TheTomik1"><img src="https://github.com/TheTomik1.png" width="40px" /></a>
<a href="https://github.com/dav23r"><img src="https://github.com/dav23r.png" width="40px" /></a>
<a href="https://github.com/stannynuytkens"><img src="https://github.com/stannynuytkens.png" width="40px" /></a>
<a href="https://github.com/danbartram"><img src="https://github.com/danbartram.png" width="40px" /></a>
<a href="https://github.com/arthurgleckler"><img src="https://github.com/arthurgleckler.png" width="40px" /></a>
<a href="https://github.com/tomroth04"><img src="https://github.com/tomroth04.png" width="40px" /></a>
<a href="https://github.com/Circenn5130"><img src="https://github.com/Circenn5130.png" width="40px" /></a>
<a href="https://github.com/jceloria"><img src="https://github.com/jceloria.png" width="40px" /></a>
<a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="40px" /></a>
<a href="https://github.com/PTR-inc"><img src="https://github.com/PTR-inc.png" width="40px" /></a>
<a href="https://github.com/spudooli"><img src="https://github.com/spudooli.png" width="40px" /></a>
<a href="https://github.com/IMarkoMC"><img src="https://github.com/IMarkoMC.png" width="40px" /></a>
<a href="https://github.com/rubund"><img src="https://github.com/rubund.png" width="40px" /></a>
<a href="https://github.com/Riolku"><img src="https://github.com/Riolku.png" width="40px" /></a>
<a href="https://github.com/arnbrhm"><img src="https://github.com/arnbrhm.png" width="40px" /></a>
<a href="https://github.com/herzkerl"><img src="https://github.com/herzkerl.png" width="40px" /></a>
<a href="https://github.com/0x45796164"><img src="https://github.com/0x45796164.png" width="40px" /></a>
<a href="https://github.com/madchr1st"><img src="https://github.com/madchr1st.png" width="40px" /></a>
<a href="https://github.com/avalentic"><img src="https://github.com/avalentic.png" width="40px" /></a>
<a href="https://github.com/TheCraiggers"><img src="https://github.com/TheCraiggers.png" width="40px" /></a>
<a href="https://github.com/sheetd"><img src="https://github.com/sheetd.png" width="40px" /></a>
<a href="https://github.com/dlt-green"><img src="https://github.com/dlt-green.png" width="40px" /></a>
<a href="https://github.com/suhlig"><img src="https://github.com/suhlig.png" width="40px" /></a>
<a href="https://github.com/Proximus888"><img src="https://github.com/Proximus888.png" width="40px" /></a>
<a href="https://github.com/wielandp"><img src="https://github.com/wielandp.png" width="40px" /></a>
<a href="https://github.com/chxseh"><img src="https://github.com/chxseh.png" width="40px" /></a>
<a href="https://github.com/user8446"><img src="https://github.com/user8446.png" width="40px" /></a>
<a href="https://github.com/cdf-eagles"><img src="https://github.com/cdf-eagles.png" width="40px" /></a>

## Contributing
I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).

<a href="https://hosted.weblate.org/engage/ntfy/">
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
</a>

## Code of Conduct
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for
everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity
and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste,
color, religion, or sexual identity and orientation.

**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.**

_Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._    

## License
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).   
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).

Third-party libraries and resources:
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
* [Roboto Font](https://fonts.google.com/specimen/Roboto) (Apache 2.0) is used as a font in everything web
* [React](https://reactjs.org/) (MIT) is used for the web app
* [Material UI components](https://mui.com/) (MIT) are used in the web app
* [MUI dashboard template](https://github.com/mui/material-ui/tree/master/docs/data/material/getting-started/templates/dashboard) (MIT) was used as a basis for the web app
* [Dexie.js](https://github.com/dexie/Dexie.js) (Apache 2.0) is used for web app persistence in IndexedDB
* [GoReleaser](https://goreleaser.com/) (MIT) is used to create releases
* [go-smtp](https://github.com/emersion/go-smtp) (MIT) is used to receive e-mails
* [stretchr/testify](https://github.com/stretchr/testify) (MIT) is used for unit and integration tests
* [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) (MIT) is used to provide the persistent message cache
* [Firebase Admin SDK](https://github.com/firebase/firebase-admin-go) (Apache 2.0) is used to send FCM messages
* [github/gemoji](https://github.com/github/gemoji) (MIT) is used for emoji support (specifically the [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file)
* [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox) as a lightbox on the landing page 
* [HTTP middleware for gzip compression](https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7) (MIT) is used for serving static files
* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used)
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)
* [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications
* [Sprig](https://github.com/Masterminds/sprig) (MIT) is used to add template parsing functions


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

As of today, I only support the latest version of ntfy. Please make sure you stay up-to-date.

## Reporting a Vulnerability

Please report security vulnerabilities privately via email to [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh).

You can also reach me on [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org) 
(my username is `binwiederhier`).


================================================
FILE: client/client.go
================================================
// Package client provides a ntfy client to publish and subscribe to topics
package client

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"heckel.io/ntfy/v2/log"
	"heckel.io/ntfy/v2/util"
	"io"
	"net/http"
	"regexp"
	"strings"
	"sync"
	"time"
)

const (
	// MessageEvent identifies a message event
	MessageEvent = "message"
)

const (
	maxResponseBytes = 4096
)

var (
	topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go
)

// Client is the ntfy client that can be used to publish and subscribe to ntfy topics
type Client struct {
	Messages      chan *Message
	config        *Config
	subscriptions map[string]*subscription
	mu            sync.Mutex
}

// Message is a struct that represents a ntfy message
type Message struct { // TODO combine with server.message
	ID         string
	Event      string
	Time       int64
	Topic      string
	Message    string
	Title      string
	Priority   int
	Tags       []string
	Click      string
	Icon       string
	Attachment *Attachment

	// Additional fields
	TopicURL       string
	SubscriptionID string
	Raw            string
}

// Attachment represents a message attachment
type Attachment struct {
	Name    string `json:"name"`
	Type    string `json:"type,omitempty"`
	Size    int64  `json:"size,omitempty"`
	Expires int64  `json:"expires,omitempty"`
	URL     string `json:"url"`
	Owner   string `json:"-"` // IP address of uploader, used for rate limiting
}

type subscription struct {
	ID       string
	topicURL string
	cancel   context.CancelFunc
}

// New creates a new Client using a given Config
func New(config *Config) *Client {
	return &Client{
		Messages:      make(chan *Message, 50), // Allow reading a few messages
		config:        config,
		subscriptions: make(map[string]*subscription),
	}
}

// Publish sends a message to a specific topic, optionally using options.
// See PublishReader for details.
func (c *Client) Publish(topic, message string, options ...PublishOption) (*Message, error) {
	return c.PublishReader(topic, strings.NewReader(message), options...)
}

// PublishReader sends a message to a specific topic, optionally using options.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
//
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
// WithNoFirebase, and the generic WithHeader.
func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) {
	topicURL, err := c.expandTopicURL(topic)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequest("POST", topicURL, body)
	if err != nil {
		return nil, err
	}
	for _, option := range options {
		if err := option(req); err != nil {
			return nil, err
		}
	}
	log.Debug("%s Publishing message with headers %s", util.ShortTopicURL(topicURL), req.Header)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(strings.TrimSpace(string(b)))
	}
	m, err := toMessage(string(b), topicURL, "")
	if err != nil {
		return nil, err
	}
	return m, nil
}

// Poll queries a topic for all (or a limited set) of messages. Unlike Subscribe, this method only polls for
// messages and does not subscribe to messages that arrive after this call.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
//
// By default, all messages will be returned, but you can change this behavior using a SubscribeOption.
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
	topicURL, err := c.expandTopicURL(topic)
	if err != nil {
		return nil, err
	}
	ctx := context.Background()
	messages := make([]*Message, 0)
	msgChan := make(chan *Message)
	errChan := make(chan error)
	log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL))
	options = append(options, WithPoll())
	go func() {
		err := performSubscribeRequest(ctx, msgChan, topicURL, "", options...)
		close(msgChan)
		errChan <- err
	}()
	for m := range msgChan {
		messages = append(messages, m)
	}
	return messages, <-errChan
}

// Subscribe subscribes to a topic to listen for newly incoming messages. The method starts a connection in the
// background and returns new messages via the Messages channel.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
//
// By default, only new messages will be returned, but you can change this behavior using a SubscribeOption.
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
//
// The method returns a unique subscriptionID that can be used in Unsubscribe.
//
// Example:
//
//	c := client.New(client.NewConfig())
//	subscriptionID, _ := c.Subscribe("mytopic")
//	for m := range c.Messages {
//	  fmt.Printf("New message: %s", m.Message)
//	}
func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, error) {
	topicURL, err := c.expandTopicURL(topic)
	if err != nil {
		return "", err
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	subscriptionID := util.RandomString(10)
	log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL))
	ctx, cancel := context.WithCancel(context.Background())
	c.subscriptions[subscriptionID] = &subscription{
		ID:       subscriptionID,
		topicURL: topicURL,
		cancel:   cancel,
	}
	go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...)
	return subscriptionID, nil
}

// Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique
// subscriptionID returned in Subscribe.
func (c *Client) Unsubscribe(subscriptionID string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	sub, ok := c.subscriptions[subscriptionID]
	if !ok {
		return
	}
	delete(c.subscriptions, subscriptionID)
	sub.cancel()
}

func (c *Client) expandTopicURL(topic string) (string, error) {
	if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
		return topic, nil
	} else if strings.Contains(topic, "/") {
		return fmt.Sprintf("https://%s", topic), nil
	}
	if !topicRegex.MatchString(topic) {
		return "", fmt.Errorf("invalid topic name: %s", topic)
	}
	return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil
}

func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) {
	for {
		// TODO The retry logic is crude and may lose messages. It should record the last message like the
		//      Android client, use since=, and do incremental backoff too
		if err := performSubscribeRequest(ctx, msgChan, topicURL, subcriptionID, options...); err != nil {
			log.Warn("%s Connection failed: %s", util.ShortTopicURL(topicURL), err.Error())
		}
		select {
		case <-ctx.Done():
			log.Info("%s Connection exited", util.ShortTopicURL(topicURL))
			return
		case <-time.After(10 * time.Second): // TODO Add incremental backoff
		}
	}
}

func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicURL string, subscriptionID string, options ...SubscribeOption) error {
	streamURL := fmt.Sprintf("%s/json", topicURL)
	log.Debug("%s Listening to %s", util.ShortTopicURL(topicURL), streamURL)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, streamURL, nil)
	if err != nil {
		return err
	}
	for _, option := range options {
		if err := option(req); err != nil {
			return err
		}
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		b, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
		if err != nil {
			return err
		}
		return errors.New(strings.TrimSpace(string(b)))
	}
	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		messageJSON := scanner.Text()
		m, err := toMessage(messageJSON, topicURL, subscriptionID)
		if err != nil {
			return err
		}
		log.Trace("%s Message received: %s", util.ShortTopicURL(topicURL), messageJSON)
		if m.Event == MessageEvent {
			msgChan <- m
		}
	}
	return nil
}

func toMessage(s, topicURL, subscriptionID string) (*Message, error) {
	var m *Message
	if err := json.NewDecoder(strings.NewReader(s)).Decode(&m); err != nil {
		return nil, err
	}
	m.TopicURL = topicURL
	m.SubscriptionID = subscriptionID
	m.Raw = s
	return m, nil
}


================================================
FILE: client/client.yml
================================================
# ntfy client config file

# Base URL used to expand short topic names in the "ntfy publish" and "ntfy subscribe" commands.
# If you self-host a ntfy server, you'll likely want to change this.
#
# default-host: https://ntfy.sh

# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
# use empty double-quotes ("").
#
# To override the default user:password combination or default token for a particular subscription (e.g., to send
# no Authorization header), set the user:pass/token for the subscription to empty double-quotes ("").

# default-token:

# default-user:
# default-password:

# Default command will execute after "ntfy subscribe" receives a message if no command is provided in subscription below
# default-command:

# Subscriptions to topics and their actions. This option is primarily used by the systemd service,
# or if you can "ntfy subscribe --from-config" directly.
#
# Example:
#     subscribe:
#       - topic: mytopic
#         command: /usr/local/bin/mytopic-triggered.sh
#       - topic: myserver.com/anothertopic
#         command: 'echo "$message"'
#         if:
#             priority: high,urgent
#       - topic: secret
#         command: 'notify-send "$m"'
#         user: phill
#         password: mypass
#       - topic: token_topic
#         token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
#
# Variables:
#     Variable        Aliases               Description
#     --------------- --------------------- -----------------------------------
#     $NTFY_ID        $id                   Unique message ID
#     $NTFY_TIME      $time                 Unix timestamp of the message delivery
#     $NTFY_TOPIC     $topic                Topic name
#     $NTFY_MESSAGE   $message, $m          Message body
#     $NTFY_TITLE     $title, $t            Message title
#     $NTFY_PRIORITY  $priority, $prio, $p  Message priority (1=min, 5=max)
#     $NTFY_TAGS      $tags, $tag, $ta      Message tags (comma separated list)
#     $NTFY_RAW       $raw                  Raw JSON message
#
# Filters ('if:'):
#     You can filter 'message', 'title', 'priority' (comma-separated list, logical OR)
#     and 'tags' (comma-separated list, logical AND). See https://ntfy.sh/docs/subscribe/api/#filter-messages.
#
# subscribe:


================================================
FILE: client/client_test.go
================================================
package client_test

import (
	"fmt"
	"github.com/stretchr/testify/require"
	"heckel.io/ntfy/v2/client"
	"heckel.io/ntfy/v2/log"
	"heckel.io/ntfy/v2/test"
	"os"
	"testing"
	"time"
)

func TestMain(m *testing.M) {
	log.SetLevel(log.ErrorLevel)
	os.Exit(m.Run())
}

func TestClient_Publish_Subscribe(t *testing.T) {
	s, port := test.StartServer(t)
	defer test.StopServer(t, s, port)
	c := client.New(newTestConfig(port))

	subscriptionID, _ := c.Subscribe("mytopic")
	time.Sleep(time.Second)

	msg, err := c.Publish("mytopic", "some message")
	require.Nil(t, err)
	require.Equal(t, "some message", msg.Message)

	msg, err = c.Publish("mytopic", "some other message",
		client.WithTitle("some title"),
		client.WithPriority("high"),
		client.WithTags([]string{"tag1", "tag 2"}))
	require.Nil(t, err)
	require.Equal(t, "some other message", msg.Message)
	require.Equal(t, "some title", msg.Title)
	require.Equal(t, []string{"tag1", "tag 2"}, msg.Tags)
	require.Equal(t, 4, msg.Priority)

	msg, err = c.Publish("mytopic", "some delayed message",
		client.WithDelay("25 hours"))
	require.Nil(t, err)
	require.Equal(t, "some delayed message", msg.Message)
	require.True(t, time.Now().Add(24*time.Hour).Unix() < msg.Time)

	time.Sleep(200 * time.Millisecond)

	msg = nextMessage(c)
	require.NotNil(t, msg)
	require.Equal(t, "some message", msg.Message)

	msg = nextMessage(c)
	require.NotNil(t, msg)
	require.Equal(t, "some other message", msg.Message)
	require.Equal(t, "some title", msg.Title)
	require.Equal(t, []string{"tag1", "tag 2"}, msg.Tags)
	require.Equal(t, 4, msg.Priority)

	msg = nextMessage(c)
	require.Nil(t, msg)

	c.Unsubscribe(subscriptionID)
	time.Sleep(200 * time.Millisecond)

	msg, err = c.Publish("mytopic", "a message that won't be received")
	require.Nil(t, err)
	require.Equal(t, "a message that won't be received", msg.Message)

	msg = nextMessage(c)
	require.Nil(t, msg)
}

func TestClient_Publish_Poll(t *testing.T) {
	s, port := test.StartServer(t)
	defer test.StopServer(t, s, port)
	c := client.New(newTestConfig(port))

	msg, err := c.Publish("mytopic", "some message", client.WithNoFirebase(), client.WithTagsList("tag1,tag2"))
	require.Nil(t, err)
	require.Equal(t, "some message", msg.Message)
	require.Equal(t, []string{"tag1", "tag2"}, msg.Tags)

	msg, err = c.Publish("mytopic", "this won't be cached", client.WithNoCache())
	require.Nil(t, err)
	require.Equal(t, "this won't be cached", msg.Message)

	msg, err = c.Publish("mytopic", "some delayed message", client.WithDelay("20 min"))
	require.Nil(t, err)
	require.Equal(t, "some delayed message", msg.Message)

	messages, err := c.Poll("mytopic")
	require.Nil(t, err)
	require.Equal(t, 1, len(messages))
	require.Equal(t, "some message", messages[0].Message)

	messages, err = c.Poll("mytopic", client.WithScheduled())
	require.Nil(t, err)
	require.Equal(t, 2, len(messages))
	require.Equal(t, "some message", messages[0].Message)
	require.Equal(t, "some delayed message", messages[1].Message)
}

func newTestConfig(port int) *client.Config {
	c := client.NewConfig()
	c.DefaultHost = fmt.Sprintf("http://127.0.0.1:%d", port)
	return c
}

func nextMessage(c *client.Client) *client.Message {
	select {
	case m := <-c.Messages:
		return m
	default:
		return nil
	}
}


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

import (
	"gopkg.in/yaml.v2"
	"heckel.io/ntfy/v2/log"
	"os"
)

const (
	// DefaultBaseURL is the base URL used to expand short topic names
	DefaultBaseURL = "https://ntfy.sh"
)

// DefaultConfigFile is the default path to the client config file (set in config_*.go)
var DefaultConfigFile string

// Config is the config struct for a Client
type Config struct {
	DefaultHost     string      `yaml:"default-host"`
	DefaultUser     string      `yaml:"default-user"`
	DefaultPassword *string     `yaml:"default-password"`
	DefaultToken    string      `yaml:"default-token"`
	DefaultCommand  string      `yaml:"default-command"`
	Subscribe       []Subscribe `yaml:"subscribe"`
}

// Subscribe is the struct for a Subscription within Config
type Subscribe struct {
	Topic    string            `yaml:"topic"`
	User     *string           `yaml:"user"`
	Password *string           `yaml:"password"`
	Token    *string           `yaml:"token"`
	Command  string            `yaml:"command"`
	If       map[string]string `yaml:"if"`
}

// NewConfig creates a new Config struct for a Client
func NewConfig() *Config {
	return &Config{
		DefaultHost:     DefaultBaseURL,
		DefaultUser:     "",
		DefaultPassword: nil,
		DefaultToken:    "",
		DefaultCommand:  "",
		Subscribe:       nil,
	}
}

// LoadConfig loads the Client config from a yaml file
func LoadConfig(filename string) (*Config, error) {
	log.Debug("Loading client config from %s", filename)
	b, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	c := NewConfig()
	if err := yaml.Unmarshal(b, c); err != nil {
		return nil, err
	}
	return c, nil
}


================================================
FILE: client/config_darwin.go
================================================
//go:build darwin

package client

import (
	"os"
	"os/user"
	"path/filepath"
)

func init() {
	u, err := user.Current()
	if err == nil && u.Uid == "0" {
		DefaultConfigFile = "/etc/ntfy/client.yml"
	} else if configDir, err := os.UserConfigDir(); err == nil {
		DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
	}
}


================================================
FILE: client/config_test.go
================================================
package client_test

import (
	"github.com/stretchr/testify/require"
	"heckel.io/ntfy/v2/client"
	"os"
	"path/filepath"
	"testing"
)

func TestConfig_Load(t *testing.T) {
	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(`
default-host: http://localhost
default-user: philipp
default-password: mypass
default-command: 'echo "Got the message: $message"'
subscribe:
  - topic: no-command-with-auth
    user: phil
    password: mypass
  - topic: echo-this
    command: 'echo "Message received: $message"'
  - topic: alerts
    command: notify-send -i /usr/share/ntfy/logo.png "Important" "$m"
    if:
            priority: high,urgent
  - topic: defaults
`), 0600))

	conf, err := client.LoadConfig(filename)
	require.Nil(t, err)
	require.Equal(t, "http://localhost", conf.DefaultHost)
	require.Equal(t, "philipp", conf.DefaultUser)
	require.Equal(t, "mypass", *conf.DefaultPassword)
	require.Equal(t, `echo "Got the message: $message"`, conf.DefaultCommand)
	require.Equal(t, 4, len(conf.Subscribe))
	require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
	require.Equal(t, "", conf.Subscribe[0].Command)
	require.Equal(t, "phil", *conf.Subscribe[0].User)
	require.Equal(t, "mypass", *conf.Subscribe[0].Password)
	require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
	require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
	require.Equal(t, "alerts", conf.Subscribe[2].Topic)
	require.Equal(t, `notify-send -i /usr/share/ntfy/logo.png "Important" "$m"`, conf.Subscribe[2].Command)
	require.Equal(t, "high,urgent", conf.Subscribe[2].If["priority"])
	require.Equal(t, "defaults", conf.Subscribe[3].Topic)
}

func TestConfig_EmptyPassword(t *testing.T) {
	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(`
default-host: http://localhost
default-user: philipp
default-password: ""
subscribe:
  - topic: no-command-with-auth
    user: phil
    password: ""
`), 0600))

	conf, err := client.LoadConfig(filename)
	require.Nil(t, err)
	require.Equal(t, "http://localhost", conf.DefaultHost)
	require.Equal(t, "philipp", conf.DefaultUser)
	require.Equal(t, "", *conf.DefaultPassword)
	require.Equal(t, 1, len(conf.Subscribe))
	require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
	require.Equal(t, "", conf.Subscribe[0].Command)
	require.Equal(t, "phil", *conf.Subscribe[0].User)
	require.Equal(t, "", *conf.Subscribe[0].Password)
}

func TestConfig_NullPassword(t *testing.T) {
	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(`
default-host: http://localhost
default-user: philipp
default-password: ~
subscribe:
  - topic: no-command-with-auth
    user: phil
    password: ~
`), 0600))

	conf, err := client.LoadConfig(filename)
	require.Nil(t, err)
	require.Equal(t, "http://localhost", conf.DefaultHost)
	require.Equal(t, "philipp", conf.DefaultUser)
	require.Nil(t, conf.DefaultPassword)
	require.Equal(t, 1, len(conf.Subscribe))
	require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
	require.Equal(t, "", conf.Subscribe[0].Command)
	require.Equal(t, "phil", *conf.Subscribe[0].User)
	require.Nil(t, conf.Subscribe[0].Password)
}

func TestConfig_NoPassword(t *testing.T) {
	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(`
default-host: http://localhost
default-user: philipp
subscribe:
  - topic: no-command-with-auth
    user: phil
`), 0600))

	conf, err := client.LoadConfig(filename)
	require.Nil(t, err)
	require.Equal(t, "http://localhost", conf.DefaultHost)
	require.Equal(t, "philipp", conf.DefaultUser)
	require.Nil(t, conf.DefaultPassword)
	require.Equal(t, 1, len(conf.Subscribe))
	require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
	require.Equal(t, "", conf.Subscribe[0].Command)
	require.Equal(t, "phil", *conf.Subscribe[0].User)
	require.Nil(t, conf.Subscribe[0].Password)
}

func TestConfig_DefaultToken(t *testing.T) {
	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(`
default-host: http://localhost
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
subscribe:
  - topic: mytopic
`), 0600))

	conf, err := client.LoadConfig(filename)
	require.Nil(t, err)
	require.Equal(t, "http://localhost", conf.DefaultHost)
	require.Equal(t, "", conf.DefaultUser)
	require.Nil(t, conf.DefaultPassword)
	require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
	require.Equal(t, 1, len(conf.Subscribe))
	require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
	require.Nil(t, conf.Subscribe[0].User)
	require.Nil(t, conf.Subscribe[0].Password)
	require.Nil(t, conf.Subscribe[0].Token)
}


================================================
FILE: client/config_unix.go
================================================
//go:build linux || dragonfly || freebsd || netbsd || openbsd

package client

import (
	"os"
	"os/user"
	"path/filepath"
)

func init() {
	u, err := user.Current()
	if err == nil && u.Uid == "0" {
		DefaultConfigFile = "/etc/ntfy/client.yml"
	} else if configDir, err := os.UserConfigDir(); err == nil {
		DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
	}
}


================================================
FILE: client/config_windows.go
================================================
//go:build windows

package client

import (
	"os"
	"path/filepath"
)

func init() {
	if configDir, err := os.UserConfigDir(); err == nil {
		DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
	}
}


================================================
FILE: client/ntfy-client.service
================================================
[Unit]
Description=ntfy client
After=network.target

[Service]
User=ntfy
Group=ntfy
ExecStart=/usr/bin/ntfy subscribe --config /etc/ntfy/client.yml --from-config
Restart=on-failure

[Install]
WantedBy=multi-user.target


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

import (
	"fmt"
	"heckel.io/ntfy/v2/util"
	"net/http"
	"strings"
	"time"
)

// RequestOption is a generic request option that can be added to Client calls
type RequestOption = func(r *http.Request) error

// PublishOption is an option that can be passed to the Client.Publish call
type PublishOption = RequestOption

// SubscribeOption is an option that can be passed to a Client.Subscribe or Client.Poll call
type SubscribeOption = RequestOption

// WithMessage sets the notification message. This is an alternative way to passing the message body.
func WithMessage(message string) PublishOption {
	return WithHeader("X-Message", message)
}

// WithTitle adds a title to a message
func WithTitle(title string) PublishOption {
	return WithHeader("X-Title", title)
}

// WithPriority adds a priority to a message. The priority can be either a number (1=min, 5=max),
// or the corresponding names (see util.ParsePriority).
func WithPriority(priority string) PublishOption {
	return WithHeader("X-Priority", priority)
}

// WithTagsList adds a list of tags to a message. The tags parameter must be a comma-separated list
// of tags. To use a slice, use WithTags instead
func WithTagsList(tags string) PublishOption {
	return WithHeader("X-Tags", tags)
}

// WithTags adds a list of a tags to a message
func WithTags(tags []string) PublishOption {
	return WithTagsList(strings.Join(tags, ","))
}

// WithDelay instructs the server to send the message at a later date. The delay parameter can be a
// Unix timestamp, a duration string or a natural langage string. See https://ntfy.sh/docs/publish/#scheduled-delivery
// for details.
func WithDelay(delay string) PublishOption {
	return WithHeader("X-Delay", delay)
}

// WithClick makes the notification action open the given URL as opposed to entering the detail view
func WithClick(url string) PublishOption {
	return WithHeader("X-Click", url)
}

// WithIcon makes the notification use the given URL as its icon
func WithIcon(icon string) PublishOption {
	return WithHeader("X-Icon", icon)
}

// WithActions adds custom user actions to the notification. The value can be either a JSON array or the
// simple format definition. See https://ntfy.sh/docs/publish/#action-buttons for details.
func WithActions(value string) PublishOption {
	return WithHeader("X-Actions", value)
}

// WithAttach sets a URL that will be used by the client to download an attachment
func WithAttach(attach string) PublishOption {
	return WithHeader("X-Attach", attach)
}

// WithMarkdown instructs the server to interpret the message body as Markdown
func WithMarkdown() PublishOption {
	return WithHeader("X-Markdown", "yes")
}

// WithTemplate instructs the server to use a specific template for the message. If templateName is is "yes" or "1",
// the server will interpret the message and title as a template.
func WithTemplate(templateName string) PublishOption {
	return WithHeader("X-Template", templateName)
}

// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
func WithFilename(filename string) PublishOption {
	return WithHeader("X-Filename", filename)
}

// WithSequenceID sets a sequence ID for the message, allowing updates to existing notifications
func WithSequenceID(sequenceID string) PublishOption {
	return WithHeader("X-Sequence-ID", sequenceID)
}

// WithEmail instructs the server to also send the message to the given e-mail address
func WithEmail(email string) PublishOption {
	return WithHeader("X-Email", email)
}

// WithBasicAuth adds the Authorization header for basic auth to the request
func WithBasicAuth(user, pass string) PublishOption {
	return WithHeader("Authorization", util.BasicAuth(user, pass))
}

// WithBearerAuth adds the Authorization header for Bearer auth to the request
func WithBearerAuth(token string) PublishOption {
	return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
}

// WithEmptyAuth clears the Authorization header
func WithEmptyAuth() PublishOption {
	return RemoveHeader("Authorization")
}

// WithNoCache instructs the server not to cache the message server-side
func WithNoCache() PublishOption {
	return WithHeader("X-Cache", "no")
}

// WithNoFirebase instructs the server not to forward the message to Firebase
func WithNoFirebase() PublishOption {
	return WithHeader("X-Firebase", "no")
}

// WithSince limits the number of messages returned from the server. The parameter since can be a Unix
// timestamp (see WithSinceUnixTime), a duration (WithSinceDuration) the word "all" (see WithSinceAll).
func WithSince(since string) SubscribeOption {
	return WithQueryParam("since", since)
}

// WithSinceAll instructs the server to return all messages for the given topic from the server
func WithSinceAll() SubscribeOption {
	return WithSince("all")
}

// WithSinceDuration instructs the server to return all messages since the given duration ago
func WithSinceDuration(since time.Duration) SubscribeOption {
	return WithSinceUnixTime(time.Now().Add(-1 * since).Unix())
}

// WithSinceUnixTime instructs the server to return only messages newer or equal to the given timestamp
func WithSinceUnixTime(since int64) SubscribeOption {
	return WithSince(fmt.Sprintf("%d", since))
}

// WithPoll instructs the server to close the connection after messages have been returned. Don't use this option
// directly. Use Client.Poll instead.
func WithPoll() SubscribeOption {
	return WithQueryParam("poll", "1")
}

// WithScheduled instructs the server to also return messages that have not been sent yet, i.e. delayed/scheduled
// messages (see WithDelay). The messages will have a future date.
func WithScheduled() SubscribeOption {
	return WithQueryParam("scheduled", "1")
}

// WithFilter is a generic subscribe option meant to be used to filter for certain messages only
func WithFilter(param, value string) SubscribeOption {
	return WithQueryParam(param, value)
}

// WithMessageFilter instructs the server to only return messages that match the exact message
func WithMessageFilter(message string) SubscribeOption {
	return WithQueryParam("message", message)
}

// WithTitleFilter instructs the server to only return messages with a title that match the exact string
func WithTitleFilter(title string) SubscribeOption {
	return WithQueryParam("title", title)
}

// WithPriorityFilter instructs the server to only return messages with the matching priority. Not that messages
// without priority also implicitly match priority 3.
func WithPriorityFilter(priority int) SubscribeOption {
	return WithQueryParam("priority", fmt.Sprintf("%d", priority))
}

// WithTagsFilter instructs the server to only return messages that contain all of the given tags
func WithTagsFilter(tags []string) SubscribeOption {
	return WithQueryParam("tags", strings.Join(tags, ","))
}

// WithHeader is a generic option to add headers to a request
func WithHeader(header, value string) RequestOption {
	return func(r *http.Request) error {
		if value != "" {
			r.Header.Set(header, value)
		}
		return nil
	}
}

// WithQueryParam is a generic option to add query parameters to a request
func WithQueryParam(param, value string) RequestOption {
	return func(r *http.Request) error {
		if value != "" {
			q := r.URL.Query()
			q.Add(param, value)
			r.URL.RawQuery = q.Encode()
		}
		return nil
	}
}

// RemoveHeader is a generic option to remove a header from a request
func RemoveHeader(header string) RequestOption {
	return func(r *http.Request) error {
		if header != "" {
			delete(r.Header, header)
		}
		return nil
	}
}


================================================
FILE: client/user/ntfy-client.service
================================================
[Unit]
Description=ntfy client
After=network.target

[Service]
ExecStart=/usr/bin/ntfy subscribe --config "%h/.config/ntfy/client.yml" --from-config
Restart=on-failure

[Install]
WantedBy=default.target


================================================
FILE: cmd/access.go
================================================
//go:build !noserver

package cmd

import (
	"errors"
	"fmt"
	"github.com/urfave/cli/v2"
	"heckel.io/ntfy/v2/user"
	"heckel.io/ntfy/v2/util"
)

func init() {
	commands = append(commands, cmdAccess)
}

const (
	userEveryone = "everyone"
)

var flagsAccess = append(
	append([]cli.Flag{}, flagsUser...),
	&cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"},
)

var cmdAccess = &cli.Command{
	Name:      "access",
	Usage:     "Grant/revoke access to a topic, or show access",
	UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]",
	Flags:     flagsAccess,
	Before:    initConfigFileInputSourceFunc("config", flagsAccess, initLogFunc),
	Action:    execUserAccess,
	Category:  categoryServer,
	Description: `Manage the access control list for the ntfy server.

This is a server-only command. It directly manages the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
to the related command 'ntfy user'.

The command allows you to show the access control list, as well as change it, depending on how
it is called.

Usage:
  ntfy access                            # Shows access control list (alias: 'ntfy user list')
  ntfy access USERNAME                   # Shows access control entries for USERNAME
  ntfy access USERNAME TOPIC PERMISSION  # Allow/deny access for USERNAME to TOPIC

Arguments:
  USERNAME     an existing user, as created with 'ntfy user add', or "everyone"/"*"
               to define access rules for anonymous/unauthenticated clients
  TOPIC        name of a topic with optional wildcards, e.g. "mytopic*"
  PERMISSION   one of the following:
               - read-write (alias: rw) 
               - read-only (aliases: read, ro)
               - write-only (aliases: write, wo)
               - deny (alias: none)

Examples:
  ntfy access                        # Shows access control list (alias: 'ntfy user list')
  ntfy access phil                   # Shows access for user phil
  ntfy access phil mytopic rw        # Allow read-write access to mytopic for user phil
  ntfy access everyone mytopic rw    # Allow anonymous read-write access to mytopic
  ntfy access everyone "up*" write   # Allow anonymous write-only access to topics "up..." 
  ntfy access --reset                # Reset entire access control list
  ntfy access --reset phil           # Reset all access for user phil
  ntfy access --reset phil mytopic   # Reset access for user phil and topic mytopic
`,
}

func execUserAccess(c *cli.Context) error {
	if c.NArg() > 3 {
		return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
	}
	manager, err := createUserManager(c)
	if err != nil {
		return err
	}
	username := c.Args().Get(0)
	if username == userEveryone {
		username = user.Everyone
	}
	topic := c.Args().Get(1)
	perms := c.Args().Get(2)
	reset := c.Bool("reset")
	if reset {
		if perms != "" {
			return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
		}
		return resetAccess(c, manager, username, topic)
	} else if perms == "" {
		if topic != "" {
			return errors.New("invalid syntax, please check 'ntfy access --help' for usage details")
		}
		return showAccess(c, manager, username)
	}
	return changeAccess(c, manager, username, topic, perms)
}

func changeAccess(c *cli.Context, manager *user.Manager, username string, topic string, perms string) error {
	if !util.Contains([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) {
		return errors.New("permission must be one of: read-write, read-only, write-only, or deny (or the aliases: read, ro, write, wo, none)")
	}
	permission, err := user.ParsePermission(perms)
	if err != nil {
		return err
	}
	u, err := manager.User(username)
	if errors.Is(err, user.ErrUserNotFound) {
		return fmt.Errorf("user %s does not exist", username)
	} else if err != nil {
		return err
	} else if u.Role == user.RoleAdmin {
		return fmt.Errorf("user %s is an admin user, access control entries have no effect", username)
	}
	if err := manager.AllowAccess(username, topic, permission); err != nil {
		return err
	}
	if permission.IsReadWrite() {
		fmt.Fprintf(c.App.Writer, "granted read-write access to topic %s\n\n", topic)
	} else if permission.IsRead() {
		fmt.Fprintf(c.App.Writer, "granted read-only access to topic %s\n\n", topic)
	} else if permission.IsWrite() {
		fmt.Fprintf(c.App.Writer, "granted write-only access to topic %s\n\n", topic)
	} else {
		fmt.Fprintf(c.App.Writer, "revoked all access to topic %s\n\n", topic)
	}
	return showUserAccess(c, manager, username)
}

func resetAccess(c *cli.Context, manager *user.Manager, username, topic string) error {
	if username == "" {
		return resetAllAccess(c, manager)
	} else if topic == "" {
		return resetUserAccess(c, manager, username)
	}
	return resetUserTopicAccess(c, manager, username, topic)
}

func resetAllAccess(c *cli.Context, manager *user.Manager) error {
	if err := manager.ResetAccess("", ""); err != nil {
		return err
	}
	fmt.Fprintln(c.App.Writer, "reset access for all users")
	return nil
}

func resetUserAccess(c *cli.Context, manager *user.Manager, username string) error {
	if err := manager.ResetAccess(username, ""); err != nil {
		return err
	}
	fmt.Fprintf(c.App.Writer, "reset access for user %s\n\n", username)
	return showUserAccess(c, manager, username)
}

func resetUserTopicAccess(c *cli.Context, manager *user.Manager, username string, topic string) error {
	if err := manager.ResetAccess(username, topic); err != nil {
		return err
	}
	fmt.Fprintf(c.App.Writer, "reset access for user %s and topic %s\n\n", username, topic)
	return showUserAccess(c, manager, username)
}

func showAccess(c *cli.Context, manager *user.Manager, username string) error {
	if username == "" {
		return showAllAccess(c, manager)
	}
	return showUserAccess(c, manager, username)
}

func showAllAccess(c *cli.Context, manager *user.Manager) error {
	users, err := manager.Users()
	if err != nil {
		return err
	}
	return showUsers(c, manager, users)
}

func showUserAccess(c *cli.Context, manager *user.Manager, username string) error {
	users, err := manager.User(username)
	if errors.Is(err, user.ErrUserNotFound) {
		return fmt.Errorf("user %s does not exist", username)
	} else if err != nil {
		return err
	}
	return showUsers(c, manager, []*user.User{users})
}

func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error {
	for _, u := range users {
		grants, err := manager.Grants(u.Name)
		if err != nil {
			return err
		}
		tier := "none"
		if u.Tier != nil {
			tier = u.Tier.Name
		}
		provisioned := ""
		if u.Provisioned {
			provisioned = ", server config"
		}
		fmt.Fprintf(c.App.Writer, "user %s (role: %s, tier: %s%s)\n", u.Name, u.Role, tier, provisioned)
		if u.Role == user.RoleAdmin {
			fmt.Fprintf(c.App.Writer, "- read-write access to all topics (admin role)\n")
		} else if len(grants) > 0 {
			for _, grant := range grants {
				grantProvisioned := ""
				if grant.Provisioned {
					grantProvisioned = " (server config)"
				}
				if grant.Permission.IsReadWrite() {
					fmt.Fprintf(c.App.Writer, "- read-write access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
				} else if grant.Permission.IsRead() {
					fmt.Fprintf(c.App.Writer, "- read-only access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
				} else if grant.Permission.IsWrite() {
					fmt.Fprintf(c.App.Writer, "- write-only access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
				} else {
					fmt.Fprintf(c.App.Writer, "- no access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
				}
			}
		} else {
			fmt.Fprintf(c.App.Writer, "- no topic-specific permissions\n")
		}
		if u.Name == user.Everyone {
			access := manager.DefaultAccess()
			if access.IsReadWrite() {
				fmt.Fprintln(c.App.Writer, "- read-write access to all (other) topics (server config)")
			} else if access.IsRead() {
				fmt.Fprintln(c.App.Writer, "- read-only access to all (other) topics (server config)")
			} else if access.IsWrite() {
				fmt.Fprintln(c.App.Writer, "- write-only access to all (other) topics (server config)")
			} else {
				fmt.Fprintln(c.App.Writer, "- no access to any (other) topics (server config)")
			}
		}
	}
	return nil
}


================================================
FILE: cmd/access_test.go
================================================
package cmd

import (
	"fmt"
	"github.com/stretchr/testify/require"
	"github.com/urfave/cli/v2"
	"heckel.io/ntfy/v2/server"
	"heckel.io/ntfy/v2/test"
	"testing"
)

func TestCLI_Access_Show(t *testing.T) {
	s, conf, port := newTestServerWithAuth(t)
	defer test.StopServer(t, s, port)

	app, _, stdout, _ := newTestApp()
	require.Nil(t, runAccessCommand(app, conf))
	require.Contains(t, stdout.String(), "user * (role: anonymous, tier: none)\n- no topic-specific permissions\n- no access to any (other) topics (server config)")
}

func TestCLI_Access_Grant_And_Publish(t *testing.T) {
	s, conf, port := newTestServerWithAuth(t)
	defer test.StopServer(t, s, port)

	app, stdin, _, _ := newTestApp()
	stdin.WriteString("philpass\nphilpass\nbenpass\nbenpass")
	require.Nil(t, runUserCommand(app, conf, "add", "--role=admin", "phil"))
	require.Nil(t, runUserCommand(app, conf, "add", "ben"))
	require.Nil(t, runAccessCommand(app, conf, "ben", "announcements", "rw"))
	require.Nil(t, runAccessCommand(app, conf, "ben", "sometopic", "read"))
	require.Nil(t, runAccessCommand(app, conf, "everyone", "announcements", "read"))

	app, _, stdout, _ := newTestApp()
	require.Nil(t, runAccessCommand(app, conf))
	expected := `user phil (role: admin, tier: none)
- read-write access to all topics (admin role)
user ben (role: user, tier: none)
- read-write access to topic announcements
- read-only access to topic sometopic
user * (role: anonymous, tier: none)
- read-only access to topic announcements
- no access to any (other) topics (server config)
`
	require.Equal(t, expected, stdout.String())

	// See if access permissions match
	app, _, _, _ = newTestApp()
	require.Error(t, app.Run([]string{
		"ntfy",
		"publish",
		fmt.Sprintf("http://127.0.0.1:%d/announcements", port),
	}))
	require.Nil(t, app.Run([]string{
		"ntfy",
		"publish",
		"-u", "ben:benpass",
		fmt.Sprintf("http://127.0.0.1:%d/announcements", port),
	}))
	require.Nil(t, app.Run([]string{
		"ntfy",
		"publish",
		"-u", "phil:philpass",
		fmt.Sprintf("http://127.0.0.1:%d/announcements", port),
	}))
	require.Nil(t, app.Run([]string{
		"ntfy",
		"subscribe",
		"--poll",
		fmt.Sprintf("http://127.0.0.1:%d/announcements", port),
	}))
	require.Error(t, app.Run([]string{
		"ntfy",
		"subscribe",
		"--poll",
		fmt.Sprintf("http://127.0.0.1:%d/something-else", port),
	}))
}

func runAccessCommand(app *cli.App, conf *server.Config, args ...string) error {
	userArgs := []string{
		"ntfy",
		"--log-level=ERROR",
		"access",
		"--config=" + conf.File, // Dummy config file to avoid lookups of real file
		"--auth-file=" + conf.AuthFile,
		"--auth-default-access=" + conf.AuthDefault.String(),
	}
	return app.Run(append(userArgs, args...))
}


================================================
FILE: cmd/app.go
================================================
// Package cmd provides the ntfy CLI application
package cmd

import (
	"fmt"
	"os"
	"regexp"

	"github.com/urfave/cli/v2"
	"github.com/urfave/cli/v2/altsrc"
	"heckel.io/ntfy/v2/log"
)

const (
	categoryClient = "Client commands"
	categoryServer = "Server commands"
)

// Build metadata keys for app.Metadata
const (
	MetadataKeyCommit = "commit"
	MetadataKeyDate   = "date"
)

var commands = make([]*cli.Command, 0)

var flagsDefault = []cli.Flag{
	&cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"NTFY_DEBUG"}, Usage: "enable debug logging"},
	&cli.BoolFlag{Name: "trace", EnvVars: []string{"NTFY_TRACE"}, Usage: "enable tracing (very verbose, be careful)"},
	&cli.BoolFlag{Name: "no-log-dates", Aliases: []string{"no_log_dates"}, EnvVars: []string{"NTFY_NO_LOG_DATES"}, Usage: "disable the date/time prefix"},
	altsrc.NewStringFlag(&cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}),
	altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "log-level-overrides", Aliases: []string{"log_level_overrides"}, EnvVars: []string{"NTFY_LOG_LEVEL_OVERRIDES"}, Usage: "set log level overrides"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "log-format", Aliases: []string{"log_format"}, Value: log.TextFormat.String(), EnvVars: []string{"NTFY_LOG_FORMAT"}, Usage: "set log format"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "log-file", Aliases: []string{"log_file"}, EnvVars: []string{"NTFY_LOG_FILE"}, Usage: "set log file, default is STDOUT"}),
}

var (
	logLevelOverrideRegex = regexp.MustCompile(`(?i)^([^=\s]+)(?:\s*=\s*(\S+))?\s*->\s*(TRACE|DEBUG|INFO|WARN|ERROR)$`)
)

// New creates a new CLI application
func New() *cli.App {
	return &cli.App{
		Name:                   "ntfy",
		Usage:                  "Simple pub-sub notification service",
		UsageText:              "ntfy [OPTION..]",
		HideVersion:            true,
		UseShortOptionHandling: true,
		Reader:                 os.Stdin,
		Writer:                 os.Stdout,
		ErrWriter:              os.Stderr,
		Commands:               commands,
		Flags:                  flagsDefault,
		Before:                 initLogFunc,
	}
}

func initLogFunc(c *cli.Context) error {
	log.SetLevel(log.ToLevel(c.String("log-level")))
	log.SetFormat(log.ToFormat(c.String("log-format")))
	if c.Bool("trace") {
		log.SetLevel(log.TraceLevel)
	} else if c.Bool("debug") {
		log.SetLevel(log.DebugLevel)
	}
	if c.Bool("no-log-dates") {
		log.DisableDates()
	}
	if err := applyLogLevelOverrides(c.StringSlice("log-level-overrides")); err != nil {
		return err
	}
	logFile := c.String("log-file")
	if logFile != "" {
		w, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
		if err != nil {
			return err
		}
		log.SetOutput(w)
	}
	return nil
}

func applyLogLevelOverrides(rawOverrides []string) error {
	for _, override := range rawOverrides {
		m := logLevelOverrideRegex.FindStringSubmatch(override)
		if len(m) == 4 {
			field, value, level := m[1], m[2], m[3]
			log.SetLevelOverride(field, value, log.ToLevel(level))
		} else if len(m) == 3 {
			field, level := m[1], m[2]
			log.SetLevelOverride(field, "", log.ToLevel(level)) // Matches any value
		} else {
			return fmt.Errorf(`invalid log level override "%s", must be "field=value -> loglevel", e.g. "user_id=u_123 -> DEBUG"`, override)
		}
	}
	return nil
}


================================================
FILE: cmd/app_test.go
================================================
package cmd

import (
	"bytes"
	"encoding/json"
	"github.com/urfave/cli/v2"
	"heckel.io/ntfy/v2/client"
	"heckel.io/ntfy/v2/log"
	"os"
	"strings"
	"testing"
)

// This only contains helpers so far

func TestMain(m *testing.M) {
	log.SetLevel(log.ErrorLevel)
	os.Exit(m.Run())
}

func newTestApp() (*cli.App, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
	var stdin, stdout, stderr bytes.Buffer
	app := New()
	app.Reader = &stdin
	app.Writer = &stdout
	app.ErrWriter = &stderr
	return app, &stdin, &stdout, &stderr
}

func toMessage(t *testing.T, s string) *client.Message {
	var m *client.Message
	if err := json.NewDecoder(strings.NewReader(s)).Decode(&m); err != nil {
		t.Fatal(err)
	}
	return m
}


================================================
FILE: cmd/config_loader.go
================================================
package cmd

import (
	"fmt"
	"github.com/urfave/cli/v2"
	"github.com/urfave/cli/v2/altsrc"
	"gopkg.in/yaml.v2"
	"heckel.io/ntfy/v2/util"
	"os"
)

// initConfigFileInputSourceFunc is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks
// if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails.
func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag, next cli.BeforeFunc) cli.BeforeFunc {
	return func(context *cli.Context) error {
		configFile := context.String(configFlag)
		if context.IsSet(configFlag) && !util.FileExists(configFile) {
			return fmt.Errorf("config file %s does not exist", configFile)
		} else if !context.IsSet(configFlag) && !util.FileExists(configFile) {
			return nil
		}
		inputSource, err := newYamlSourceFromFile(configFile, flags)
		if err != nil {
			return err
		}
		if err := altsrc.ApplyInputSourceValues(context, inputSource, flags); err != nil {
			return err
		}
		if next != nil {
			if err := next(context); err != nil {
				return err
			}
		}
		return nil
	}
}

// newYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath.
//
// This function also maps aliases, so a .yml file can contain short options, or options with underscores
// instead of dashes. See https://github.com/binwiederhier/ntfy/issues/255.
func newYamlSourceFromFile(file string, flags []cli.Flag) (altsrc.InputSourceContext, error) {
	var rawConfig map[any]any
	b, err := os.ReadFile(file)
	if err != nil {
		return nil, err
	}
	if err := yaml.Unmarshal(b, &rawConfig); err != nil {
		return nil, err
	}
	for _, f := range flags {
		flagName := f.Names()[0]
		for _, flagAlias := range f.Names()[1:] {
			if _, ok := rawConfig[flagAlias]; ok {
				rawConfig[flagName] = rawConfig[flagAlias]
			}
		}
	}
	return altsrc.NewMapInputSource(file, rawConfig), nil
}


================================================
FILE: cmd/config_loader_test.go
================================================
package cmd

import (
	"github.com/stretchr/testify/require"
	"os"
	"path/filepath"
	"testing"
)

func TestNewYamlSourceFromFile(t *testing.T) {
	filename := filepath.Join(t.TempDir(), "server.yml")
	contents := `
# Normal options
listen-https: ":10443"

# Note the underscore!
listen_http: ":1080"

# OMG this is allowed now ...
K: /some/file.pem
`
	require.Nil(t, os.WriteFile(filename, []byte(contents), 0600))

	ctx, err := newYamlSourceFromFile(filename, flagsServe)
	require.Nil(t, err)

	listenHTTPS, err := ctx.String("listen-https")
	require.Nil(t, err)
	require.Equal(t, ":10443", listenHTTPS)

	listenHTTP, err := ctx.String("listen-http") // No underscore!
	require.Nil(t, err)
	require.Equal(t, ":1080", listenHTTP)

	keyFile, err := ctx.String("key-file") // Long option!
	require.Nil(t, err)
	require.Equal(t, "/some/file.pem", keyFile)
}


================================================
FILE: cmd/publish.go
================================================
package cmd

import (
	"errors"
	"fmt"
	"github.com/urfave/cli/v2"
	"heckel.io/ntfy/v2/client"
	"heckel.io/ntfy/v2/log"
	"heckel.io/ntfy/v2/util"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

func init() {
	commands = append(commands, cmdPublish)
}

var flagsPublish = append(
	append([]cli.Flag{}, flagsDefault...),
	&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"},
	&cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"},
	&cli.StringFlag{Name: "message", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MESSAGE"}, Usage: "message body"},
	&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
	&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
	&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
	&cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"},
	&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
	&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
	&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
	&cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"},
	&cli.StringFlag{Name: "template", Aliases: []string{"tpl"}, EnvVars: []string{"NTFY_TEMPLATE"}, Usage: "use templates to transform JSON message body"},
	&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
	&cli.StringFlag{Name: "sequence-id", Aliases: []string{"sequence_id", "sid", "S"}, EnvVars: []string{"NTFY_SEQUENCE_ID"}, Usage: "sequence ID for updating notifications"},
	&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
	&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
	&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
	&cli.StringFlag{Name: "token", Aliases: []string{"k"}, EnvVars: []string{"NTFY_TOKEN"}, Usage: "access token used to auth against the server"},
	&cli.IntFlag{Name: "wait-pid", Aliases: []string{"wait_pid", "pid"}, EnvVars: []string{"NTFY_WAIT_PID"}, Usage: "wait until PID exits before publishing"},
	&cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"wait_cmd", "cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run command and wait until it finishes before publishing"},
	&cli.BoolFlag{Name: "no-cache", Aliases: []string{"no_cache", "C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
	&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"no_firebase", "F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
	&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do not print message"},
)

var cmdPublish = &cli.Command{
	Name:    "publish",
	Aliases: []string{"pub", "send", "trigger"},
	Usage:   "Send message via a ntfy server",
	UsageText: `ntfy publish [OPTIONS..] TOPIC [MESSAGE...]
ntfy publish [OPTIONS..] --wait-cmd COMMAND...
NTFY_TOPIC=.. ntfy publish [OPTIONS..] [MESSAGE...]`,
	Action:   execPublish,
	Category: categoryClient,
	Flags:    flagsPublish,
	Before:   initLogFunc,
	Description: `Publish a message to a ntfy server.

Examples:
  ntfy publish mytopic This is my message                 # Send simple message
  ntfy send myserver.com/mytopic "This is my message"     # Send message to different default host
  ntfy pub -p high backups "Backups failed"               # Send high priority message
  ntfy pub --tags=warning,skull backups "Backups failed"  # Add tags/emojis to message
  ntfy pub --delay=10s delayed_topic Laterzz              # Delay message by 10s
  ntfy pub --at=8:30am delayed_topic Laterzz              # Send message at 8:30am
  ntfy pub -e phil@example.com alerts 'App is down!'      # Also send email to phil@example.com
  ntfy pub --click="https://reddit.com" redd 'New msg'    # Opens Reddit when notification is clicked
  ntfy pub --icon="http://some.tld/icon.png" 'Icon!'      # Send notification with custom icon
  ntfy pub --attach="http://some.tld/file.zip" files      # Send ZIP archive from URL as attachment
  ntfy pub --file=flower.jpg flowers 'Nice!'              # Send image.jpg as attachment
  ntfy pub -S my-id mytopic 'Update me'                   # Send with sequence ID for updates
  echo 'message' | ntfy publish mytopic                   # Send message from stdin
  ntfy pub -u phil:mypass secret Psst                     # Publish with username/password
  ntfy pub --wait-pid 1234 mytopic                        # Wait for process 1234 to exit before publishing
  ntfy pub --wait-cmd mytopic rsync -av ./ /tmp/a         # Run command and publish after it completes
  NTFY_USER=phil:mypass ntfy pub secret Psst              # Use env variables to set username/password
  NTFY_TOPIC=mytopic ntfy pub "some message"              # Use NTFY_TOPIC variable as topic 
  cat flower.jpg | ntfy pub --file=- flowers 'Nice!'      # Same as above, send image.jpg as attachment
  ntfy trigger mywebhook                                  # Sending without message, useful for webhooks
 
Please also check out the docs on publishing messages. Especially for the --tags and --delay options, 
it has incredibly useful information: https://ntfy.sh/docs/publish/.

` + clientCommandDescriptionSuffix,
}

func execPublish(c *cli.Context) error {
	conf, err := loadConfig(c)
	if err != nil {
		return err
	}
	title := c.String("title")
	priority := c.String("priority")
	tags := c.String("tags")
	delay := c.String("delay")
	click := c.String("click")
	icon := c.String("icon")
	actions := c.String("actions")
	attach := c.String("attach")
	markdown := c.Bool("markdown")
	template := c.String("template")
	filename := c.String("filename")
	sequenceID := c.String("sequence-id")
	file := c.String("file")
	email := c.String("email")
	user := c.String("user")
	token := c.String("token")
	noCache := c.Bool("no-cache")
	noFirebase := c.Bool("no-firebase")
	quiet := c.Bool("quiet")
	pid := c.Int("wait-pid")

	// Checks
	if user != "" && token != "" {
		return errors.New("cannot set both --user and --token")
	}

	// Do the things
	topic, message, command, err := parseTopicMessageCommand(c)
	if err != nil {
		return err
	}
	var options []client.PublishOption
	if title != "" {
		options = append(options, client.WithTitle(title))
	}
	if priority != "" {
		options = append(options, client.WithPriority(priority))
	}
	if tags != "" {
		options = append(options, client.WithTagsList(tags))
	}
	if delay != "" {
		options = append(options, client.WithDelay(delay))
	}
	if click != "" {
		options = append(options, client.WithClick(click))
	}
	if icon != "" {
		options = append(options, client.WithIcon(icon))
	}
	if actions != "" {
		options = append(options, client.WithActions(strings.ReplaceAll(actions, "\n", " ")))
	}
	if attach != "" {
		options = append(options, client.WithAttach(attach))
	}
	if markdown {
		options = append(options, client.WithMarkdown())
	}
	if template != "" {
		options = append(options, client.WithTemplate(template))
	}
	if filename != "" {
		options = append(options, client.WithFilename(filename))
	}
	if sequenceID != "" {
		options = append(options, client.WithSequenceID(sequenceID))
	}
	if email != "" {
		options = append(options, client.WithEmail(email))
	}
	if noCache {
		options = append(options, client.WithNoCache())
	}
	if noFirebase {
		options = append(options, client.WithNoFirebase())
	}
	if token != "" {
		options = append(options, client.WithBearerAuth(token))
	} else if user != "" {
		var pass string
		parts := strings.SplitN(user, ":", 2)
		if len(parts) == 2 {
			user = parts[0]
			pass = parts[1]
		} else {
			fmt.Fprint(c.App.ErrWriter, "Enter Password: ")
			p, err := util.ReadPassword(c.App.Reader)
			if err != nil {
				return err
			}
			pass = string(p)
			fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
		}
		options = append(options, client.WithBasicAuth(user, pass))
	} else if conf.DefaultToken != "" {
		options = append(options, client.WithBearerAuth(conf.DefaultToken))
	} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
		options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
	}
	if pid > 0 {
		newMessage, err := waitForProcess(pid)
		if err != nil {
			return err
		} else if message == "" {
			message = newMessage
		}
	} else if len(command) > 0 {
		newMessage, err := runAndWaitForCommand(command)
		if err != nil {
			return err
		} else if message == "" {
			message = newMessage
		}
	}
	var body io.Reader
	if file == "" {
		body = strings.NewReader(message)
	} else {
		if message != "" {
			options = append(options, client.WithMessage(message))
		}
		if file == "-" {
			if filename == "" {
				options = append(options, client.WithFilename("stdin"))
			}
			body = c.App.Reader
		} else {
			if filename == "" {
				options = append(options, client.WithFilename(filepath.Base(file)))
			}
			body, err = os.Open(file)
			if err != nil {
				return err
			}
		}
	}
	cl := client.New(conf)
	m, err := cl.PublishReader(topic, body, options...)
	if err != nil {
		return err
	}
	if !quiet {
		fmt.Fprintln(c.App.Writer, strings.TrimSpace(m.Raw))
	}
	return nil
}

// parseTopicMessageCommand reads the topic and the remaining arguments from the context.

// There are a few cases to consider:
//
//	ntfy publish <topic> [<message>]
//	ntfy publish --wait-cmd <topic> <command>
//	NTFY_TOPIC=.. ntfy publish [<message>]
//	NTFY_TOPIC=.. ntfy publish --wait-cmd <command>
func parseTopicMessageCommand(c *cli.Context) (topic string, message string, command []string, err error) {
	var args []string
	topic, args, err = parseTopicAndArgs(c)
	if err != nil {
		return
	}
	if c.Bool("wait-cmd") {
		if len(args) == 0 {
			err = errors.New("must specify command when --wait-cmd is passed, type 'ntfy publish --help' for help")
			return
		}
		command = args
	} else {
		message = strings.Join(args, " ")
	}
	if c.String("message") != "" {
		message = c.String("message")
	}
	if message == "" && isStdinRedirected() {
		var data []byte
		data, err = io.ReadAll(io.LimitReader(c.App.Reader, 1024*1024))
		if err != nil {
			log.Debug("Failed to read from stdin: %s", err.Error())
			return
		}
		message = strings.TrimSpace(string(data))
	}
	return
}

func parseTopicAndArgs(c *cli.Context) (topic string, args []string, err error) {
	envTopic := os.Getenv("NTFY_TOPIC")
	if envTopic != "" {
		topic = envTopic
		return topic, remainingArgs(c, 0), nil
	}
	if c.NArg() < 1 {
		return "", nil, errors.New("must specify topic, type 'ntfy publish --help' for help")
	}
	return c.Args().Get(0), remainingArgs(c, 1), nil
}

func remainingArgs(c *cli.Context, fromIndex int) []string {
	if c.NArg() > fromIndex {
		return c.Args().Slice()[fromIndex:]
	}
	return []string{}
}

func waitForProcess(pid int) (message string, err error) {
	if !processExists(pid) {
		return "", fmt.Errorf("process with PID %d not running", pid)
	}
	start := time.Now()
	log.Debug("Waiting for process with PID %d to exit", pid)
	for processExists(pid) {
		time.Sleep(500 * time.Millisecond)
	}
	runtime := time.Since(start).Round(time.Millisecond)
	log.Debug("Process with PID %d exited after %s", pid, runtime)
	return fmt.Sprintf("Process with PID %d exited after %s", pid, runtime), nil
}

func runAndWaitForCommand(command []string) (message string, err error) {
	prettyCmd := util.QuoteCommand(command)
	log.Debug("Running command: %s", prettyCmd)
	start := time.Now()
	cmd := exec.Command(command[0], command[1:]...)
	if log.IsTrace() {
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	}
	err = cmd.Run()
	runtime := time.Since(start).Round(time.Millisecond)
	if err != nil {
		if exitError, ok := err.(*exec.ExitError); ok {
			log.Debug("Command failed after %s (exit code %d): %s", runtime, exitError.ExitCode(), prettyCmd)
			return fmt.Sprintf("Command failed after %s (exit code %d): %s", runtime, exitError.ExitCode(), prettyCmd), nil
		}
		// Hard fail when command does not exist or could not be properly launched
		return "", fmt.Errorf("command failed: %s, error: %s", prettyCmd, err.Error())
	}
	log.Debug("Command succeeded after %s: %s", runtime, prettyCmd)
	return fmt.Sprintf("Command succeeded after %s: %s", runtime, prettyCmd), nil
}

func isStdinRedirected() bool {
	stat, err := os.Stdin.Stat()
	if err != nil {
		log.Debug("Failed to stat stdin: %s", err.Error())
		return false
	}
	return (stat.Mode() & os.ModeCharDevice) == 0
}


================================================
FILE: cmd/publish_test.go
================================================
package cmd

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"heckel.io/ntfy/v2/test"
	"heckel.io/ntfy/v2/util"
)

func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
	t.Skip("temporarily disabled") // FIXME
	testMessage := util.RandomString(10)
	app, _, _, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))

	_, err := util.Retry(func() (*int, error) {
		app2, _, stdout, _ := newTestApp()
		if err := app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}); err != nil {
			return nil, err
		}
		if !strings.Contains(stdout.String(), testMessage) {
			return nil, fmt.Errorf("test message %s not found in topic", testMessage)
		}
		return util.Int(1), nil
	}, time.Second, 2*time.Second, 5*time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
	require.Nil(t, err)
}

func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
	s, port := test.StartServer(t)
	defer test.StopServer(t, s, port)
	topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)

	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", topic, "some message"}))
	m := toMessage(t, stdout.String())
	require.Equal(t, "some message", m.Message)

	app2, _, stdout, _ := newTestApp()
	require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", topic}))
	m = toMessage(t, stdout.String())
	require.Equal(t, "some message", m.Message)
}

func TestCLI_Publish_All_The_Things(t *testing.T) {
	s, port := test.StartServer(t)
	defer test.StopServer(t, s, port)
	topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)

	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{
		"ntfy", "publish",
		"--title", "this is a title",
		"--priority", "high",
		"--tags", "tag1,tag2",
		// No --delay, --email
		"--click", "https://ntfy.sh",
		"--icon", "https://ntfy.sh/static/img/ntfy.png",
		"--attach", "https://f-droid.org/F-Droid.apk",
		"--filename", "fdroid.apk",
		"--no-cache",
		"--no-firebase",
		topic,
		"some message",
	}))
	m := toMessage(t, stdout.String())
	require.Equal(t, "message", m.Event)
	require.Equal(t, "mytopic", m.Topic)
	require.Equal(t, "some message", m.Message)
	require.Equal(t, "this is a title", m.Title)
	require.Equal(t, 4, m.Priority)
	require.Equal(t, []string{"tag1", "tag2"}, m.Tags)
	require.Equal(t, "https://ntfy.sh", m.Click)
	require.Equal(t, "https://f-droid.org/F-Droid.apk", m.Attachment.URL)
	require.Equal(t, "fdroid.apk", m.Attachment.Name)
	require.Equal(t, int64(0), m.Attachment.Size)
	require.Equal(t, "", m.Attachment.Owner)
	require.Equal(t, int64(0), m.Attachment.Expires)
	require.Equal(t, "", m.Attachment.Type)
	require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
}

func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
	s, port := test.StartServer(t)
	defer test.StopServer(t, s, port)
	topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)

	// Test: sleep 0.5
	sleep := exec.Command("sleep", "0.5")
	require.Nil(t, sleep.Start())
	go sleep.Wait() // Must be called to release resources
	start := time.Now()
	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-pid", strconv.Itoa(sleep.Process.Pid), topic}))
	m := toMessage(t, stdout.String())
	require.True(t, time.Since(start) >= 500*time.Millisecond)
	require.Regexp(t, `Process with PID \d+ exited after `, m.Message)

	// Test: PID does not exist
	app, _, _, _ = newTestApp()
	err := app.Run([]string{"ntfy", "publish", "--wait-pid", "1234567", topic})
	require.Error(t, err)
	require.Equal(t, "process with PID 1234567 not running", err.Error())

	// Test: Successful command (exit 0)
	start = time.Now()
	app, _, stdout, _ = newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "sleep", "0.5"}))
	m = toMessage(t, stdout.String())
	require.True(t, time.Since(start) >= 500*time.Millisecond)
	require.Contains(t, m.Message, `Command succeeded after `)
	require.Contains(t, m.Message, `: sleep 0.5`)

	// Test: Failing command (exit 1)
	app, _, stdout, _ = newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "/bin/false", "false doesn't care about its args"}))
	m = toMessage(t, stdout.String())
	require.Contains(t, m.Message, `Command failed after `)
	require.Contains(t, m.Message, `(exit code 1): /bin/false "false doesn't care about its args"`, m.Message)

	// Test: Non-existing command (hard fail!)
	app, _, _, _ = newTestApp()
	err = app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "does-not-exist-no-really", "really though"})
	require.Error(t, err)
	require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())

	// Tests with NTFY_TOPIC set ////
	t.Setenv("NTFY_TOPIC", topic)

	// Test: Successful command with NTFY_TOPIC
	app, _, stdout, _ = newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--cmd", "echo", "hi there"}))
	m = toMessage(t, stdout.String())
	require.Equal(t, "mytopic", m.Topic)

	// Test: Successful --wait-pid with NTFY_TOPIC
	sleep = exec.Command("sleep", "0.2")
	require.Nil(t, sleep.Start())
	go sleep.Wait() // Must be called to release resources
	app, _, stdout, _ = newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-pid", strconv.Itoa(sleep.Process.Pid)}))
	m = toMessage(t, stdout.String())
	require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
}

func TestCLI_Publish_Default_UserPass(t *testing.T) {
	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/mytopic", r.URL.Path)
		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(message))
	}))
	defer server.Close()

	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
`, server.URL)), 0600))

	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
	m := toMessage(t, stdout.String())
	require.Equal(t, "triggered", m.Message)
}

func TestCLI_Publish_Default_Token(t *testing.T) {
	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/mytopic", r.URL.Path)
		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(message))
	}))
	defer server.Close()

	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))

	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
	m := toMessage(t, stdout.String())
	require.Equal(t, "triggered", m.Message)
}

func TestCLI_Publish_Default_UserPass_CLI_Token(t *testing.T) {
	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/mytopic", r.URL.Path)
		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(message))
	}))
	defer server.Close()

	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
`, server.URL)), 0600))

	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
	m := toMessage(t, stdout.String())
	require.Equal(t, "triggered", m.Message)
}

func TestCLI_Publish_Default_Token_CLI_UserPass(t *testing.T) {
	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/mytopic", r.URL.Path)
		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(message))
	}))
	defer server.Close()

	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))

	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
	m := toMessage(t, stdout.String())
	require.Equal(t, "triggered", m.Message)
}

func TestCLI_Publish_Default_Token_CLI_Token(t *testing.T) {
	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/mytopic", r.URL.Path)
		require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(message))
	}))
	defer server.Close()

	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_FAKETOKEN01234567890FAKETOKEN
`, server.URL)), 0600))

	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
	m := toMessage(t, stdout.String())
	require.Equal(t, "triggered", m.Message)
}

func TestCLI_Publish_Default_UserPass_CLI_UserPass(t *testing.T) {
	message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/mytopic", r.URL.Path)
		require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(message))
	}))
	defer server.Close()

	filename := filepath.Join(t.TempDir(), "client.yml")
	require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: fakepass
`, server.URL)), 0600))

	app, _, stdout, _ := newTestApp()
	require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
	m := toMessage(t, stdout.String())
	require.Equal(t, "triggered", m.Message)
}

func TestCLI_Publish_Token_And_UserPass(t *testing.T) {
	app, _, _, _ := newTestApp()
	err := app.Run([]string{"ntfy", "publish", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
	require.Error(t, err)
	require.Equal(t, "cannot set both --user and --token", err.Error())
}


================================================
FILE: cmd/publish_unix.go
================================================
//go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd

package cmd

import "syscall"

func processExists(pid int) bool {
	err := syscall.Kill(pid, syscall.Signal(0))
	return err == nil
}


================================================
FILE: cmd/publish_windows.go
================================================
package cmd

import (
	"os"
)

func processExists(pid int) bool {
	_, err := os.FindProcess(pid)
	return err == nil
}


================================================
FILE: cmd/serve.go
================================================
//go:build !noserver

package cmd

import (
	"errors"
	"fmt"
	"io/fs"
	"math"
	"net"
	"net/netip"
	"net/url"
	"runtime"
	"strings"
	"text/template"
	"time"

	"github.com/urfave/cli/v2"
	"github.com/urfave/cli/v2/altsrc"
	"heckel.io/ntfy/v2/log"
	"heckel.io/ntfy/v2/payments"
	"heckel.io/ntfy/v2/server"
	"heckel.io/ntfy/v2/user"
	"heckel.io/ntfy/v2/util"
)

func init() {
	commands = append(commands, cmdServe)
}

var flagsServe = append(
	append([]cli.Flag{}, flagsDefault...),
	&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: server.DefaultConfigFile, Usage: "config file"},
	altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used as HTTP listen address"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used as HTTPS listen address"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"listen_unix", "U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "listen-unix-mode", Aliases: []string{"listen_unix_mode"}, EnvVars: []string{"NTFY_LISTEN_UNIX_MODE"}, DefaultText: "system default", Usage: "file permissions of unix socket, e.g. 0700"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"key_file", "K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "database-url", Aliases: []string{"database_url"}, EnvVars: []string{"NTFY_DATABASE_URL"}, Usage: "PostgreSQL connection string for database-backed stores (e.g. postgres://user:pass@host:5432/ntfy)"}),
	altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "database-replica-urls", Aliases: []string{"database_replica_urls"}, EnvVars: []string{"NTFY_DATABASE_REPLICA_URLS"}, Usage: "PostgreSQL read replica connection strings for offloading read queries"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: util.FormatDuration(server.DefaultCacheDuration), Usage: "buffer messages for this time to allow `since` requests"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "cache-batch-size", Aliases: []string{"cache_batch_size"}, EnvVars: []string{"NTFY_BATCH_SIZE"}, Usage: "max size of messages to batch together when writing to message cache (if zero, writes are synchronous)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Value: util.FormatDuration(server.DefaultCacheBatchTimeout), Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
	altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}),
	altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-access", Aliases: []string{"auth_access"}, EnvVars: []string{"NTFY_AUTH_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}),
	altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-tokens", Aliases: []string{"auth_tokens"}, EnvVars: []string{"NTFY_AUTH_TOKENS"}, Usage: "pre-provisioned declarative access tokens"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultAttachmentExpiryDuration), Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "template-dir", Aliases: []string{"template_dir"}, EnvVars: []string{"NTFY_TEMPLATE_DIR"}, Value: server.DefaultTemplateDir, Usage: "directory to load named message templates from"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: util.FormatDuration(server.DefaultKeepaliveInterval), Usage: "interval of keepalive messages"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: util.FormatDuration(server.DefaultManagerInterval), Usage: "interval of for message pruning and stats printing"}),
	altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}),
	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "require-login", Aliases: []string{"require_login"}, EnvVars: []string{"NTFY_REQUIRE_LOGIN"}, Value: false, Usage: "all actions via the web app requires a login"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-from", Aliases: []string{"smtp_sender_from"}, EnvVars: []string{"NTFY_SMTP_SENDER_FROM"}, Usage: "SMTP sender address (if e-mail sending is enabled)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", Aliases: []string{"smtp_server_listen"}, EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", Aliases: []string{"smtp_server_addr_prefix"}, EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-account", Aliases: []string{"twilio_account"}, EnvVars: []string{"NTFY_TWILIO_ACCOUNT"}, Usage: "Twilio account SID, used for phone calls, e.g. AC123..."}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-phone-number", Aliases: []string{"twilio_phone_number"}, EnvVars: []string{"NTFY_TWILIO_PHONE_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-call-format", Aliases: []string{"twilio_call_format"}, EnvVars: []string{"NTFY_TWILIO_CALL_FORMAT"}, Usage: "Twilio/TwiML format string for phone calls"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "message-size-limit", Aliases: []string{"message_size_limit"}, EnvVars: []string{"NTFY_MESSAGE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultMessageSizeLimit), Usage: "size limit for the message (see docs for limitations)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "message-delay-limit", Aliases: []string{"message_delay_limit"}, EnvVars: []string{"NTFY_MESSAGE_DELAY_LIMIT"}, Value: util.FormatDuration(server.DefaultMessageDelayMax), Usage: "max duration a message can be scheduled into the future"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultVisitorAttachmentTotalSizeLimit), Usage: "total storage limit used for attachments per visitor"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-bandwidth-limit", Aliases: []string{"visitor_attachment_daily_bandwidth_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload bandwidth limit per visitor"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", Aliases: []string{"visitor_request_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorRequestLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-exempt-hosts", Aliases: []string{"visitor_request_limit_exempt_hosts"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS"}, Value: "", Usage: "hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-message-daily-limit", Aliases: []string{"visitor_message_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_MESSAGE_DAILY_LIMIT"}, Value: server.DefaultVisitorMessageDailyLimit, Usage: "max messages per visitor per day, derived from request limit if unset"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv4", Aliases: []string{"visitor_prefix_bits_ipv4"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV4"}, Value: server.DefaultVisitorPrefixBitsIPv4, Usage: "number of bits of the IPv4 address to use for rate limiting (default: 32, full address)"}),
	altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv6", Aliases: []string{"visitor_prefix_bits_ipv6"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV6"}, Value: server.DefaultVisitorPrefixBitsIPv6, Usage: "number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)"}),
	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "use specified header to determine visitor IP address (for rate limiting)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-hosts", Aliases: []string{"proxy_trusted_hosts"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_HOSTS"}, Value: "", Usage: "comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}),
	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "automatically expire unused subscriptions after this time"}),
	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}),
)

var cmdServe = &cli.Command{
	Name:      "serve",
	Usage:     "Run the ntfy server",
	UsageText: "ntfy serve [OPTIONS..]",
	Action:    execServe,
	Category:  categoryServer,
	Flags:     flagsServe,
	Before:    initConfigFileInputSourceFunc("config", flagsServe, initLogFunc),
	Description: `Run the ntfy server and listen for incoming requests

The command will load the configuration from /etc/ntfy/server.yml. Config options can 
be overridden using the command line options.

Examples:
  ntfy serve                      # Starts server in the foreground (on port 80)
  ntfy serve --listen-http :8080  # Starts server with alternate port`,
}

func execServe(c *cli.Context) error {
	if c.NArg() > 0 {
		return errors.New("no arguments expected, see 'ntfy serve --help' for help")
	}

	// Read all the options
	config := c.String("config")
	baseURL := strings.TrimSuffix(c.String("base-url"), "/")
	listenHTTP := c.String("listen-http")
	listenHTTPS := c.String("listen-https")
	listenUnix := c.String("listen-unix")
	listenUnixMode := c.Int("listen-unix-mode")
	keyFile := c.String("key-file")
	certFile := c.String("cert-file")
	firebaseKeyFile := c.String("firebase-key-file")
	databaseURL := c.String("database-url")
	databaseReplicaURLs := c.StringSlice("database-replica-urls")
	webPushPrivateKey := c.String("web-push-private-key")
	webPushPublicKey := c.String("web-push-public-key")
	webPushFile := c.String("web-push-file")
	webPushEmailAddress := c.String("web-push-email-address")
	webPushStartupQueries := c.String("web-push-startup-queries")
	webPushExpiryDurationStr := c.String("web-push-expiry-duration")
	webPushExpiryWarningDurationStr := c.String("web-push-expiry-warning-duration")
	cacheFile := c.String("cache-file")
	cacheDurationStr := c.String("cache-duration")
	cacheStartupQueries := c.String("cache-startup-queries")
	cacheBatchSize := c.Int("cache-batch-size")
	cacheBatchTimeoutStr := c.String("cache-batch-timeout")
	authFile := c.String("auth-file")
	authStartupQueries := c.String("auth-startup-queries")
	authDefaultAccess := c.String("auth-default-access")
	authUsersRaw := c.StringSlice("auth-users")
	authAccessRaw := c.StringSlice("auth-access")
	authTokensRaw := c.StringSlice("auth-tokens")
	attachmentCacheDir := c.String("attachment-cache-dir")
	attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
	attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
	attachmentExpiryDurationStr := c.String("attachment-expiry-duration")
	templateDir := c.String("template-dir")
	keepaliveIntervalStr := c.String("keepalive-interval")
	managerIntervalStr := c.String("manager-interval")
	disallowedTopics := c.StringSlice("disallowed-topics")
	webRoot := c.String("web-root")
	enableSignup := c.Bool("enable-signup")
	enableLogin := c.Bool("enable-login")
	requireLogin := c.Bool("require-login")
	enableReservations := c.Bool("enable-reservations")
	upstreamBaseURL := c.String("upstream-base-url")
	upstreamAccessToken := c.String("upstream-access-token")
	smtpSenderAddr := c.String("smtp-sender-addr")
	smtpSenderUser := c.String("smtp-sender-user")
	smtpSenderPass := c.String("smtp-sender-pass")
	smtpSenderFrom := c.String("smtp-sender-from")
	smtpServerListen := c.String("smtp-server-listen")
	smtpServerDomain := c.String("smtp-server-domain")
	smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
	twilioAccount := c.String("twilio-account")
	twilioAuthToken := c.String("twilio-auth-token")
	twilioPhoneNumber := c.String("twilio-phone-number")
	twilioVerifyService := c.String("twilio-verify-service")
	twilioCallFormat := c.String("twilio-call-format")
	messageSizeLimitStr := c.String("message-size-limit")
	messageDelayLimitStr := c.String("message-delay-limit")
	totalTopicLimit := c.Int("global-topic-limit")
	visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
	visitorSubscriberRateLimiting := c.Bool("visitor-subscriber-rate-limiting")
	visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
	visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
	visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
	visitorRequestLimitReplenishStr := c.String("visitor-request-limit-replenish")
	visitorRequestLimitExemptHosts := util.SplitNoEmpty(c.String("visitor-request-limit-exempt-hosts"), ",")
	visitorMessageDailyLimit := c.Int("visitor-message-daily-limit")
	visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
	visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish")
	visitorPrefixBitsIPv4 := c.Int("visitor-prefix-bits-ipv4")
	visitorPrefixBitsIPv6 := c.Int("visitor-prefix-bits-ipv6")
	behindProxy := c.Bool("behind-proxy")
	proxyForwardedHeader := c.String("proxy-forwarded-header")
	proxyTrustedHosts := util.SplitNoEmpty(c.String("proxy-trusted-hosts"), ",")
	stripeSecretKey := c.String("stripe-secret-key")
	stripeWebhookKey := c.String("stripe-webhook-key")
	billingContact := c.String("billing-contact")
	metricsListenHTTP := c.String("metrics-listen-http")
	enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
	profileListenHTTP := c.String("profile-listen-http")

	// Convert durations
	cacheDuration, err := util.ParseDuration(cacheDurationStr)
	if err != nil {
		return fmt.Errorf("invalid cache duration: %s", cacheDurationStr)
	}
	cacheBatchTimeout, err := util.ParseDuration(cacheBatchTimeoutStr)
	if err != nil {
		return fmt.Errorf("invalid cache batch timeout: %s", cacheBatchTimeoutStr)
	}
	attachmentExpiryDuration, err := util.ParseDuration(attachmentExpiryDurationStr)
	if err != nil {
		return fmt.Errorf("invalid attachment expiry duration: %s", attachmentExpiryDurationStr)
	}
	keepaliveInterval, err := util.ParseDuration(keepaliveIntervalStr)
	if err != nil {
		return fmt.Errorf("invalid keepalive interval: %s", keepaliveIntervalStr)
	}
	managerInterval, err := util.ParseDuration(managerIntervalStr)
	if err != nil {
		return fmt.Errorf("invalid manager interval: %s", managerIntervalStr)
	}
	messageDelayLimit, err := util.ParseDuration(messageDelayLimitStr)
	if err != nil {
		return fmt.Errorf("invalid message delay limit: %s", messageDelayLimitStr)
	}
	visitorRequestLimitReplenish, err := util.ParseDuration(visitorRequestLimitReplenishStr)
	if err != nil {
		return fmt.Errorf("invalid visitor request limit replenish: %s", visitorRequestLimitReplenishStr)
	}
	visitorEmailLimitReplenish, err := util.ParseDuration(visitorEmailLimitReplenishStr)
	if err != nil {
		return fmt.Errorf("invalid visitor email limit replenish: %s", visitorEmailLimitReplenishStr)
	}
	webPushExpiryDuration, err := util.ParseDuration(webPushExpiryDurationStr)
	if err != nil {
		return fmt.Errorf("invalid web push expiry duration: %s", webPushExpiryDurationStr)
	}
	webPushExpiryWarningDuration, err := util.ParseDuration(webPushExpiryWarningDurationStr)
	if err != nil {
		return fmt.Errorf("invalid web push expiry warning duration: %s", webPushExpiryWarningDurationStr)
	}

	// Convert sizes to bytes
	messageSizeLimit, err := util.ParseSize(messageSizeLimitStr)
	if err != nil {
		return fmt.Errorf("invalid message size limit: %s", messageSizeLimitStr)
	}
	attachmentTotalSizeLimit, err := util.ParseSize(attachmentTotalSizeLimitStr)
	if err != nil {
		return fmt.Errorf("invalid attachment total size limit: %s", attachmentTotalSizeLimitStr)
	}
	attachmentFileSizeLimit, err := util.ParseSize(attachmentFileSizeLimitStr)
	if err != nil {
		return fmt.Errorf("invalid attachment file size limit: %s", attachmentFileSizeLimitStr)
	}
	visitorAttachmentTotalSizeLimit, err := util.ParseSize(visitorAttachmentTotalSizeLimitStr)
	if err != nil {
		return fmt.Errorf("invalid visitor attachment total size limit: %s", visitorAttachmentTotalSizeLimitStr)
	}
	visitorAttachmentDailyBandwidthLimit, err := util.ParseSize(visitorAttachmentDailyBandwidthLimitStr)
	if err != nil {
		return fmt.Errorf("invalid visitor attachment daily bandwidth limit: %s", visitorAttachmentDailyBandwidthLimitStr)
	} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
		return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
	}

	// Check values
	if databaseURL != "" && !strings.HasPrefix(databaseURL, "postgres://") && !strings.HasPrefix(databaseURL, "postgresql://") {
		return errors.New("if database-url is set, it must start with postgres:// or postgresql://")
	} else if databaseURL != "" && (authFile != "" || cacheFile != "" || webPushFile != "") {
		return errors.New("if database-url is set, auth-file, cache-file, and web-push-file must not be set")
	} else if len(databaseReplicaURLs) > 0 && databaseURL == "" {
		return errors.New("database-replica-urls can only be used if database-url is also set")
	} else if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
		return errors.New("if set, FCM key file must exist")
	} else if firebaseKeyFile != "" && !server.FirebaseAvailable {
		return errors.New("cannot set firebase-key-file, support for Firebase is not available (nofirebase)")
	} else if webPushPublicKey != "" && (webPushPrivateKey == "" || (webPushFile == "" && databaseURL == "") || webPushEmailAddress == "" || baseURL == "") {
		return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file (or database-url), web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
	} else if keepaliveInterval < 5*time.Second {
		return errors.New("keepalive interval cannot be lower than five seconds")
	} else if managerInterval < 5*time.Second {
		return errors.New("manager interval cannot be lower than five seconds")
	} else if cacheDuration > 0 && cacheDuration < managerInterval {
		return errors.New("cache duration cannot be lower than manager interval")
	} else if keyFile != "" && !util.FileExists(keyFile) {
		return errors.New("if set, key file must exist")
	} else if certFile != "" && !util.FileExists(certFile) {
		return errors.New("if set, certificate file must exist")
	} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
		return errors.New("if listen-https is set, both key-file and cert-file must be set")
	} else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderFrom == "") {
		return errors.New("if smtp-sender-addr is set, base-url, and smtp-sender-from must also be set")
	} else if smtpServerListen != "" && smtpServerDomain == "" {
		return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
	} else if attachmentCacheDir !=
Download .txt
gitextract_5801oewz/

├── .dockerignore
├── .git-blame-ignore-revs
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── 1_bug_report.md
│   │   ├── 2_enhancement_request.md
│   │   ├── 3_tech_support.md
│   │   └── 4_question.md
│   └── workflows/
│       ├── build.yaml
│       ├── docs.yaml
│       ├── release.yaml
│       └── test.yaml
├── .gitignore
├── .gitpod.yml
├── .goreleaser.yml
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── Dockerfile-arm
├── Dockerfile-build
├── LICENSE
├── LICENSE.GPLv2
├── Makefile
├── README.md
├── SECURITY.md
├── assets/
│   └── favicon.xcf
├── client/
│   ├── client.go
│   ├── client.yml
│   ├── client_test.go
│   ├── config.go
│   ├── config_darwin.go
│   ├── config_test.go
│   ├── config_unix.go
│   ├── config_windows.go
│   ├── ntfy-client.service
│   ├── options.go
│   └── user/
│       └── ntfy-client.service
├── cmd/
│   ├── access.go
│   ├── access_test.go
│   ├── app.go
│   ├── app_test.go
│   ├── config_loader.go
│   ├── config_loader_test.go
│   ├── publish.go
│   ├── publish_test.go
│   ├── publish_unix.go
│   ├── publish_windows.go
│   ├── serve.go
│   ├── serve_test.go
│   ├── serve_unix.go
│   ├── serve_windows.go
│   ├── subscribe.go
│   ├── subscribe_darwin.go
│   ├── subscribe_test.go
│   ├── subscribe_unix.go
│   ├── subscribe_windows.go
│   ├── tier.go
│   ├── tier_test.go
│   ├── token.go
│   ├── token_test.go
│   ├── user.go
│   ├── user_test.go
│   ├── webpush.go
│   └── webpush_test.go
├── db/
│   ├── db.go
│   ├── pg/
│   │   ├── pg.go
│   │   └── pg_test.go
│   ├── test/
│   │   └── test.go
│   ├── types.go
│   └── util.go
├── docker-compose.yml
├── docs/
│   ├── _overrides/
│   │   └── main.html
│   ├── config.md
│   ├── contact.md
│   ├── contributing.md
│   ├── deprecations.md
│   ├── develop.md
│   ├── emojis.md
│   ├── examples.md
│   ├── faq.md
│   ├── hooks.py
│   ├── index.md
│   ├── install.md
│   ├── integrations.md
│   ├── known-issues.md
│   ├── privacy.md
│   ├── publish/
│   │   └── template-functions.md
│   ├── publish.md
│   ├── releases.md
│   ├── static/
│   │   ├── audio/
│   │   │   └── ntfy-phone-call.ogg
│   │   ├── css/
│   │   │   ├── config-generator.css
│   │   │   └── extra.css
│   │   ├── img/
│   │   │   ├── cli-subscribe-video-2.webm
│   │   │   └── cli-subscribe-video-3.webm
│   │   └── js/
│   │       ├── bcrypt.js
│   │       ├── config-generator.js
│   │       └── extra.js
│   ├── subscribe/
│   │   ├── api.md
│   │   ├── cli.md
│   │   ├── phone.md
│   │   ├── pwa.md
│   │   └── web.md
│   ├── terms.md
│   └── troubleshooting.md
├── examples/
│   ├── grafana-dashboard/
│   │   └── ntfy-grafana.json
│   ├── linux-desktop-notifications/
│   │   └── notify-desktop.sh
│   ├── publish-go/
│   │   └── main.go
│   ├── publish-php/
│   │   └── publish.php
│   ├── publish-python/
│   │   └── publish.py
│   ├── ssh-login-alert/
│   │   ├── ntfy-ssh-login.sh
│   │   └── pam_sshd
│   ├── subscribe-go/
│   │   └── main.go
│   ├── subscribe-php/
│   │   └── subscribe.php
│   ├── subscribe-python/
│   │   └── subscribe.py
│   ├── web-example-eventsource/
│   │   └── example-sse.html
│   └── web-example-websocket/
│       └── example-ws.html
├── go.mod
├── go.sum
├── log/
│   ├── event.go
│   ├── log.go
│   ├── log_test.go
│   └── types.go
├── main.go
├── message/
│   ├── cache.go
│   ├── cache_postgres.go
│   ├── cache_postgres_schema.go
│   ├── cache_sqlite.go
│   ├── cache_sqlite_schema.go
│   ├── cache_sqlite_test.go
│   └── cache_test.go
├── mkdocs.yml
├── model/
│   └── model.go
├── payments/
│   ├── payments.go
│   └── payments_dummy.go
├── requirements.txt
├── scripts/
│   ├── emoji-convert.sh
│   ├── emoji.json
│   ├── postinst.sh
│   ├── postrm.sh
│   ├── preinst.sh
│   └── prerm.sh
├── server/
│   ├── actions.go
│   ├── actions_test.go
│   ├── config.go
│   ├── config_test.go
│   ├── config_unix.go
│   ├── config_windows.go
│   ├── errors.go
│   ├── file_cache.go
│   ├── file_cache_test.go
│   ├── log.go
│   ├── mailer_emoji_map.json
│   ├── ntfy.service
│   ├── server.go
│   ├── server.yml
│   ├── server_account.go
│   ├── server_account_test.go
│   ├── server_admin.go
│   ├── server_admin_test.go
│   ├── server_firebase.go
│   ├── server_firebase_dummy.go
│   ├── server_firebase_test.go
│   ├── server_manager.go
│   ├── server_manager_test.go
│   ├── server_matrix.go
│   ├── server_matrix_test.go
│   ├── server_metrics.go
│   ├── server_middleware.go
│   ├── server_payments.go
│   ├── server_payments_dummy.go
│   ├── server_payments_test.go
│   ├── server_race_off_test.go
│   ├── server_race_on_test.go
│   ├── server_test.go
│   ├── server_twilio.go
│   ├── server_twilio_test.go
│   ├── server_webpush.go
│   ├── server_webpush_dummy.go
│   ├── server_webpush_test.go
│   ├── smtp_sender.go
│   ├── smtp_sender_test.go
│   ├── smtp_server.go
│   ├── smtp_server_test.go
│   ├── templates/
│   │   ├── alertmanager.yml
│   │   ├── github.yml
│   │   └── grafana.yml
│   ├── testdata/
│   │   ├── webhook_alertmanager_firing.json
│   │   ├── webhook_github_comment_created.json
│   │   ├── webhook_github_issue_opened.json
│   │   ├── webhook_github_pr_opened.json
│   │   ├── webhook_github_star_created.json
│   │   ├── webhook_github_watch_created.json
│   │   └── webhook_grafana_resolved.json
│   ├── topic.go
│   ├── topic_test.go
│   ├── types.go
│   ├── util.go
│   ├── util_test.go
│   └── visitor.go
├── test/
│   ├── server.go
│   ├── test.go
│   └── util.go
├── tools/
│   ├── fbsend/
│   │   ├── README.md
│   │   └── main.go
│   ├── loadgen/
│   │   └── main.go
│   ├── loadtest/
│   │   ├── go.mod
│   │   ├── go.sum
│   │   └── main.go
│   ├── pgimport/
│   │   ├── README.md
│   │   └── main.go
│   └── shrink-png.sh
├── user/
│   ├── manager.go
│   ├── manager_postgres.go
│   ├── manager_postgres_schema.go
│   ├── manager_sqlite.go
│   ├── manager_sqlite_schema.go
│   ├── manager_test.go
│   ├── types.go
│   ├── types_test.go
│   ├── util.go
│   └── util_test.go
├── util/
│   ├── batching_queue.go
│   ├── batching_queue_test.go
│   ├── content_type_writer.go
│   ├── content_type_writer_test.go
│   ├── embedfs/
│   │   └── test.txt
│   ├── embedfs.go
│   ├── embedfs_test.go
│   ├── gzip_handler.go
│   ├── gzip_handler_test.go
│   ├── limit.go
│   ├── limit_test.go
│   ├── lookup_cache.go
│   ├── lookup_cache_test.go
│   ├── peek.go
│   ├── peek_test.go
│   ├── sprig/
│   │   ├── LICENSE.txt
│   │   ├── crypto.go
│   │   ├── crypto_test.go
│   │   ├── date.go
│   │   ├── date_test.go
│   │   ├── defaults.go
│   │   ├── defaults_test.go
│   │   ├── dict.go
│   │   ├── dict_test.go
│   │   ├── doc.go
│   │   ├── example_test.go
│   │   ├── flow_control.go
│   │   ├── flow_control_test.go
│   │   ├── functions.go
│   │   ├── functions_linux_test.go
│   │   ├── functions_test.go
│   │   ├── list.go
│   │   ├── list_test.go
│   │   ├── numeric.go
│   │   ├── numeric_test.go
│   │   ├── reflect.go
│   │   ├── reflect_test.go
│   │   ├── regex.go
│   │   ├── regex_test.go
│   │   ├── strings.go
│   │   ├── strings_test.go
│   │   ├── url.go
│   │   └── url_test.go
│   ├── time.go
│   ├── time_test.go
│   ├── timeout_writer.go
│   ├── util.go
│   └── util_test.go
├── web/
│   ├── .eslintignore
│   ├── .eslintrc
│   ├── .prettierignore
│   ├── index.html
│   ├── package.json
│   ├── public/
│   │   ├── config.js
│   │   ├── static/
│   │   │   ├── css/
│   │   │   │   ├── app.css
│   │   │   │   └── fonts.css
│   │   │   └── langs/
│   │   │       ├── ar.json
│   │   │       ├── bg.json
│   │   │       ├── bn.json
│   │   │       ├── ca.json
│   │   │       ├── cs.json
│   │   │       ├── cu.json
│   │   │       ├── cy.json
│   │   │       ├── da.json
│   │   │       ├── de.json
│   │   │       ├── en.json
│   │   │       ├── eo.json
│   │   │       ├── es.json
│   │   │       ├── et.json
│   │   │       ├── fa.json
│   │   │       ├── fi.json
│   │   │       ├── fr.json
│   │   │       ├── gl.json
│   │   │       ├── he.json
│   │   │       ├── hu.json
│   │   │       ├── id.json
│   │   │       ├── it.json
│   │   │       ├── ja.json
│   │   │       ├── ko.json
│   │   │       ├── mk.json
│   │   │       ├── ms.json
│   │   │       ├── nb_NO.json
│   │   │       ├── nl.json
│   │   │       ├── pl.json
│   │   │       ├── pt.json
│   │   │       ├── pt_BR.json
│   │   │       ├── ro.json
│   │   │       ├── ru.json
│   │   │       ├── sk.json
│   │   │       ├── sq.json
│   │   │       ├── sv.json
│   │   │       ├── ta.json
│   │   │       ├── th.json
│   │   │       ├── tr.json
│   │   │       ├── uk.json
│   │   │       ├── uz.json
│   │   │       ├── vi.json
│   │   │       ├── zh_Hans.json
│   │   │       └── zh_Hant.json
│   │   └── sw.js
│   ├── src/
│   │   ├── app/
│   │   │   ├── AccountApi.js
│   │   │   ├── Api.js
│   │   │   ├── Connection.js
│   │   │   ├── ConnectionManager.js
│   │   │   ├── Notifier.js
│   │   │   ├── Poller.js
│   │   │   ├── Prefs.js
│   │   │   ├── Pruner.js
│   │   │   ├── Session.js
│   │   │   ├── SubscriptionManager.js
│   │   │   ├── UserManager.js
│   │   │   ├── VersionChecker.js
│   │   │   ├── actions.js
│   │   │   ├── config.js
│   │   │   ├── db.js
│   │   │   ├── emojis.js
│   │   │   ├── emojisMapped.js
│   │   │   ├── errors.js
│   │   │   ├── events.js
│   │   │   ├── i18n.js
│   │   │   ├── notificationUtils.js
│   │   │   └── utils.js
│   │   ├── components/
│   │   │   ├── Account.jsx
│   │   │   ├── ActionBar.jsx
│   │   │   ├── App.jsx
│   │   │   ├── AttachmentIcon.jsx
│   │   │   ├── AvatarBox.jsx
│   │   │   ├── DialogFooter.jsx
│   │   │   ├── EmojiPicker.jsx
│   │   │   ├── ErrorBoundary.jsx
│   │   │   ├── Login.jsx
│   │   │   ├── Messaging.jsx
│   │   │   ├── Navigation.jsx
│   │   │   ├── Notifications.jsx
│   │   │   ├── PopupMenu.jsx
│   │   │   ├── Pref.jsx
│   │   │   ├── Preferences.jsx
│   │   │   ├── PublishDialog.jsx
│   │   │   ├── RTLCacheProvider.jsx
│   │   │   ├── ReserveDialogs.jsx
│   │   │   ├── ReserveIcons.jsx
│   │   │   ├── ReserveTopicSelect.jsx
│   │   │   ├── Signup.jsx
│   │   │   ├── SubscribeDialog.jsx
│   │   │   ├── SubscriptionPopup.jsx
│   │   │   ├── UpgradeDialog.jsx
│   │   │   ├── hooks.js
│   │   │   ├── routes.js
│   │   │   ├── styles.js
│   │   │   └── theme.js
│   │   ├── index.jsx
│   │   └── registerSW.js
│   └── vite.config.js
└── webpush/
    ├── store.go
    ├── store_postgres.go
    ├── store_sqlite.go
    ├── store_test.go
    └── types.go
Download .txt
Showing preview only (257K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2715 symbols across 190 files)

FILE: client/client.go
  constant MessageEvent (line 22) | MessageEvent = "message"
  constant maxResponseBytes (line 26) | maxResponseBytes = 4096
  type Client (line 34) | type Client struct
    method Publish (line 88) | func (c *Client) Publish(topic, message string, options ...PublishOpti...
    method PublishReader (line 100) | func (c *Client) PublishReader(topic string, body io.Reader, options ....
    method Poll (line 143) | func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Me...
    method Subscribe (line 184) | func (c *Client) Subscribe(topic string, options ...SubscribeOption) (...
    method Unsubscribe (line 205) | func (c *Client) Unsubscribe(subscriptionID string) {
    method expandTopicURL (line 216) | func (c *Client) expandTopicURL(topic string) (string, error) {
  type Message (line 42) | type Message struct
  type Attachment (line 62) | type Attachment struct
  type subscription (line 71) | type subscription struct
  function New (line 78) | func New(config *Config) *Client {
  function handleSubscribeConnLoop (line 228) | func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message,...
  function performSubscribeRequest (line 244) | func performSubscribeRequest(ctx context.Context, msgChan chan *Message,...
  function toMessage (line 283) | func toMessage(s, topicURL, subscriptionID string) (*Message, error) {

FILE: client/client_test.go
  function TestMain (line 14) | func TestMain(m *testing.M) {
  function TestClient_Publish_Subscribe (line 19) | func TestClient_Publish_Subscribe(t *testing.T) {
  function TestClient_Publish_Poll (line 74) | func TestClient_Publish_Poll(t *testing.T) {
  function newTestConfig (line 104) | func newTestConfig(port int) *client.Config {
  function nextMessage (line 110) | func nextMessage(c *client.Client) *client.Message {

FILE: client/config.go
  constant DefaultBaseURL (line 11) | DefaultBaseURL = "https://ntfy.sh"
  type Config (line 18) | type Config struct
  type Subscribe (line 28) | type Subscribe struct
  function NewConfig (line 38) | func NewConfig() *Config {
  function LoadConfig (line 50) | func LoadConfig(filename string) (*Config, error) {

FILE: client/config_darwin.go
  function init (line 11) | func init() {

FILE: client/config_test.go
  function TestConfig_Load (line 11) | func TestConfig_Load(t *testing.T) {
  function TestConfig_EmptyPassword (line 50) | func TestConfig_EmptyPassword(t *testing.T) {
  function TestConfig_NullPassword (line 74) | func TestConfig_NullPassword(t *testing.T) {
  function TestConfig_NoPassword (line 98) | func TestConfig_NoPassword(t *testing.T) {
  function TestConfig_DefaultToken (line 120) | func TestConfig_DefaultToken(t *testing.T) {

FILE: client/config_unix.go
  function init (line 11) | func init() {

FILE: client/config_windows.go
  function init (line 10) | func init() {

FILE: client/options.go
  function WithMessage (line 21) | func WithMessage(message string) PublishOption {
  function WithTitle (line 26) | func WithTitle(title string) PublishOption {
  function WithPriority (line 32) | func WithPriority(priority string) PublishOption {
  function WithTagsList (line 38) | func WithTagsList(tags string) PublishOption {
  function WithTags (line 43) | func WithTags(tags []string) PublishOption {
  function WithDelay (line 50) | func WithDelay(delay string) PublishOption {
  function WithClick (line 55) | func WithClick(url string) PublishOption {
  function WithIcon (line 60) | func WithIcon(icon string) PublishOption {
  function WithActions (line 66) | func WithActions(value string) PublishOption {
  function WithAttach (line 71) | func WithAttach(attach string) PublishOption {
  function WithMarkdown (line 76) | func WithMarkdown() PublishOption {
  function WithTemplate (line 82) | func WithTemplate(templateName string) PublishOption {
  function WithFilename (line 87) | func WithFilename(filename string) PublishOption {
  function WithSequenceID (line 92) | func WithSequenceID(sequenceID string) PublishOption {
  function WithEmail (line 97) | func WithEmail(email string) PublishOption {
  function WithBasicAuth (line 102) | func WithBasicAuth(user, pass string) PublishOption {
  function WithBearerAuth (line 107) | func WithBearerAuth(token string) PublishOption {
  function WithEmptyAuth (line 112) | func WithEmptyAuth() PublishOption {
  function WithNoCache (line 117) | func WithNoCache() PublishOption {
  function WithNoFirebase (line 122) | func WithNoFirebase() PublishOption {
  function WithSince (line 128) | func WithSince(since string) SubscribeOption {
  function WithSinceAll (line 133) | func WithSinceAll() SubscribeOption {
  function WithSinceDuration (line 138) | func WithSinceDuration(since time.Duration) SubscribeOption {
  function WithSinceUnixTime (line 143) | func WithSinceUnixTime(since int64) SubscribeOption {
  function WithPoll (line 149) | func WithPoll() SubscribeOption {
  function WithScheduled (line 155) | func WithScheduled() SubscribeOption {
  function WithFilter (line 160) | func WithFilter(param, value string) SubscribeOption {
  function WithMessageFilter (line 165) | func WithMessageFilter(message string) SubscribeOption {
  function WithTitleFilter (line 170) | func WithTitleFilter(title string) SubscribeOption {
  function WithPriorityFilter (line 176) | func WithPriorityFilter(priority int) SubscribeOption {
  function WithTagsFilter (line 181) | func WithTagsFilter(tags []string) SubscribeOption {
  function WithHeader (line 186) | func WithHeader(header, value string) RequestOption {
  function WithQueryParam (line 196) | func WithQueryParam(param, value string) RequestOption {
  function RemoveHeader (line 208) | func RemoveHeader(header string) RequestOption {

FILE: cmd/access.go
  function init (line 13) | func init() {
  constant userEveryone (line 18) | userEveryone = "everyone"
  function execUserAccess (line 70) | func execUserAccess(c *cli.Context) error {
  function changeAccess (line 99) | func changeAccess(c *cli.Context, manager *user.Manager, username string...
  function resetAccess (line 130) | func resetAccess(c *cli.Context, manager *user.Manager, username, topic ...
  function resetAllAccess (line 139) | func resetAllAccess(c *cli.Context, manager *user.Manager) error {
  function resetUserAccess (line 147) | func resetUserAccess(c *cli.Context, manager *user.Manager, username str...
  function resetUserTopicAccess (line 155) | func resetUserTopicAccess(c *cli.Context, manager *user.Manager, usernam...
  function showAccess (line 163) | func showAccess(c *cli.Context, manager *user.Manager, username string) ...
  function showAllAccess (line 170) | func showAllAccess(c *cli.Context, manager *user.Manager) error {
  function showUserAccess (line 178) | func showUserAccess(c *cli.Context, manager *user.Manager, username stri...
  function showUsers (line 188) | func showUsers(c *cli.Context, manager *user.Manager, users []*user.User...

FILE: cmd/access_test.go
  function TestCLI_Access_Show (line 12) | func TestCLI_Access_Show(t *testing.T) {
  function TestCLI_Access_Grant_And_Publish (line 21) | func TestCLI_Access_Grant_And_Publish(t *testing.T) {
  function runAccessCommand (line 79) | func runAccessCommand(app *cli.App, conf *server.Config, args ...string)...

FILE: cmd/app.go
  constant categoryClient (line 15) | categoryClient = "Client commands"
  constant categoryServer (line 16) | categoryServer = "Server commands"
  constant MetadataKeyCommit (line 21) | MetadataKeyCommit = "commit"
  constant MetadataKeyDate (line 22) | MetadataKeyDate   = "date"
  function New (line 42) | func New() *cli.App {
  function initLogFunc (line 58) | func initLogFunc(c *cli.Context) error {
  function applyLogLevelOverrides (line 83) | func applyLogLevelOverrides(rawOverrides []string) error {

FILE: cmd/app_test.go
  function TestMain (line 16) | func TestMain(m *testing.M) {
  function newTestApp (line 21) | func newTestApp() (*cli.App, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
  function toMessage (line 30) | func toMessage(t *testing.T, s string) *client.Message {

FILE: cmd/config_loader.go
  function initConfigFileInputSourceFunc (line 14) | func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag, ...
  function newYamlSourceFromFile (line 42) | func newYamlSourceFromFile(file string, flags []cli.Flag) (altsrc.InputS...

FILE: cmd/config_loader_test.go
  function TestNewYamlSourceFromFile (line 10) | func TestNewYamlSourceFromFile(t *testing.T) {

FILE: cmd/publish.go
  function init (line 18) | func init() {
  function execPublish (line 90) | func execPublish(c *cli.Context) error {
  function parseTopicMessageCommand (line 251) | func parseTopicMessageCommand(c *cli.Context) (topic string, message str...
  function parseTopicAndArgs (line 281) | func parseTopicAndArgs(c *cli.Context) (topic string, args []string, err...
  function remainingArgs (line 293) | func remainingArgs(c *cli.Context, fromIndex int) []string {
  function waitForProcess (line 300) | func waitForProcess(pid int) (message string, err error) {
  function runAndWaitForCommand (line 314) | func runAndWaitForCommand(command []string) (message string, err error) {
  function isStdinRedirected (line 337) | func isStdinRedirected() bool {

FILE: cmd/publish_test.go
  function TestCLI_Publish_Subscribe_Poll_Real_Server (line 20) | func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
  function TestCLI_Publish_Subscribe_Poll (line 39) | func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
  function TestCLI_Publish_All_The_Things (line 55) | func TestCLI_Publish_All_The_Things(t *testing.T) {
  function TestCLI_Publish_Wait_PID_And_Cmd (line 93) | func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
  function TestCLI_Publish_Default_UserPass (line 156) | func TestCLI_Publish_Default_UserPass(t *testing.T) {
  function TestCLI_Publish_Default_Token (line 180) | func TestCLI_Publish_Default_Token(t *testing.T) {
  function TestCLI_Publish_Default_UserPass_CLI_Token (line 203) | func TestCLI_Publish_Default_UserPass_CLI_Token(t *testing.T) {
  function TestCLI_Publish_Default_Token_CLI_UserPass (line 227) | func TestCLI_Publish_Default_Token_CLI_UserPass(t *testing.T) {
  function TestCLI_Publish_Default_Token_CLI_Token (line 250) | func TestCLI_Publish_Default_Token_CLI_Token(t *testing.T) {
  function TestCLI_Publish_Default_UserPass_CLI_UserPass (line 273) | func TestCLI_Publish_Default_UserPass_CLI_UserPass(t *testing.T) {
  function TestCLI_Publish_Token_And_UserPass (line 297) | func TestCLI_Publish_Token_And_UserPass(t *testing.T) {

FILE: cmd/publish_unix.go
  function processExists (line 7) | func processExists(pid int) bool {

FILE: cmd/publish_windows.go
  function processExists (line 7) | func processExists(pid int) bool {

FILE: cmd/serve.go
  function init (line 27) | func init() {
  function execServe (line 133) | func execServe(c *cli.Context) error {
  function parseIPHostPrefix (line 545) | func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
  function parseUsers (line 570) | func parseUsers(usersRaw []string) ([]*user.User, error) {
  function parseAccess (line 597) | func parseAccess(users []*user.User, accessRaw []string) (map[string][]*...
  function parseTokens (line 640) | func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*...
  function maybeFromMetadata (line 676) | func maybeFromMetadata(m map[string]any, key string) string {

FILE: cmd/serve_test.go
  function TestParseUsers_Success (line 21) | func TestParseUsers_Success(t *testing.T) {
  function TestParseUsers_Errors (line 95) | func TestParseUsers_Errors(t *testing.T) {
  function TestParseAccess_Success (line 143) | func TestParseAccess_Success(t *testing.T) {
  function TestParseAccess_Errors (line 250) | func TestParseAccess_Errors(t *testing.T) {
  function TestParseTokens_Success (line 310) | func TestParseTokens_Success(t *testing.T) {
  function TestParseTokens_Errors (line 413) | func TestParseTokens_Errors(t *testing.T) {
  function TestCLI_Serve_Unix_Curl (line 472) | func TestCLI_Serve_Unix_Curl(t *testing.T) {
  function TestCLI_Serve_WebSocket (line 492) | func TestCLI_Serve_WebSocket(t *testing.T) {
  function TestIP_Host_Parsing (line 523) | func TestIP_Host_Parsing(t *testing.T) {
  function newEmptyFile (line 539) | func newEmptyFile(t *testing.T) string {

FILE: cmd/serve_unix.go
  function sigHandlerConfigReload (line 15) | func sigHandlerConfigReload(config string) {
  function reloadLogLevel (line 31) | func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
  function maybeRunAsService (line 53) | func maybeRunAsService(conf *server.Config) (bool, error) {

FILE: cmd/serve_windows.go
  constant serviceName (line 14) | serviceName = "ntfy"
  function sigHandlerConfigReload (line 18) | func sigHandlerConfigReload(config string) {
  function runAsWindowsService (line 23) | func runAsWindowsService(conf *server.Config) error {
  type windowsService (line 28) | type windowsService struct
    method Execute (line 35) | func (s *windowsService) Execute(args []string, requests <-chan svc.Ch...
  function maybeRunAsService (line 88) | func maybeRunAsService(conf *server.Config) (bool, error) {

FILE: cmd/subscribe.go
  function init (line 17) | func init() {
  function execSubscribe (line 85) | func execSubscribe(c *cli.Context) error {
  function doPoll (line 150) | func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topi...
  function doPollSingle (line 167) | func doPollSingle(c *cli.Context, cl *client.Client, topic, command stri...
  function doSubscribe (line 178) | func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config,...
  function maybeAddAuthHeader (line 220) | func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client....
  function printMessageOrRunCommand (line 244) | func printMessageOrRunCommand(c *cli.Context, m *client.Message, command...
  function runCommand (line 253) | func runCommand(c *cli.Context, command string, m *client.Message) {
  function runCommandInternal (line 259) | func runCommandInternal(c *cli.Context, script string, m *client.Message...
  function envVars (line 276) | func envVars(m *client.Message) []string {
  function envVar (line 293) | func envVar(value string, vars ...string) []string {
  function loadConfig (line 301) | func loadConfig(c *cli.Context) (*client.Config, error) {
  function logMessagePrefix (line 316) | func logMessagePrefix(m *client.Message) string {

FILE: cmd/subscribe_darwin.go
  constant scriptExt (line 6) | scriptExt                      = "sh"
  constant scriptHeader (line 7) | scriptHeader                   = "#!/bin/sh\n"
  constant clientCommandDescriptionSuffix (line 8) | clientCommandDescriptionSuffix = `The default config file for all client...

FILE: cmd/subscribe_test.go
  function TestCLI_Subscribe_Default_UserPass_Subscription_Token (line 14) | func TestCLI_Subscribe_Default_UserPass_Subscription_Token(t *testing.T) {
  function TestCLI_Subscribe_Default_Token_Subscription_UserPass (line 42) | func TestCLI_Subscribe_Default_Token_Subscription_UserPass(t *testing.T) {
  function TestCLI_Subscribe_Default_Token_Subscription_Token (line 70) | func TestCLI_Subscribe_Default_Token_Subscription_Token(t *testing.T) {
  function TestCLI_Subscribe_Default_UserPass_Subscription_UserPass (line 97) | func TestCLI_Subscribe_Default_UserPass_Subscription_UserPass(t *testing...
  function TestCLI_Subscribe_Default_Token_Subscription_Empty (line 126) | func TestCLI_Subscribe_Default_Token_Subscription_Empty(t *testing.T) {
  function TestCLI_Subscribe_Default_UserPass_Subscription_Empty (line 152) | func TestCLI_Subscribe_Default_UserPass_Subscription_Empty(t *testing.T) {
  function TestCLI_Subscribe_Default_Empty_Subscription_Token (line 179) | func TestCLI_Subscribe_Default_Empty_Subscription_Token(t *testing.T) {
  function TestCLI_Subscribe_Default_Empty_Subscription_UserPass (line 205) | func TestCLI_Subscribe_Default_Empty_Subscription_UserPass(t *testing.T) {
  function TestCLI_Subscribe_Default_Token_CLI_Token (line 232) | func TestCLI_Subscribe_Default_Token_CLI_Token(t *testing.T) {
  function TestCLI_Subscribe_Default_Token_CLI_UserPass (line 256) | func TestCLI_Subscribe_Default_Token_CLI_UserPass(t *testing.T) {
  function TestCLI_Subscribe_Default_Token_Subscription_Token_CLI_UserPass (line 280) | func TestCLI_Subscribe_Default_Token_Subscription_Token_CLI_UserPass(t *...
  function TestCLI_Subscribe_Token_And_UserPass (line 307) | func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
  function TestCLI_Subscribe_Default_Token (line 314) | func TestCLI_Subscribe_Default_Token(t *testing.T) {
  function TestCLI_Subscribe_Default_UserPass (line 338) | func TestCLI_Subscribe_Default_UserPass(t *testing.T) {
  function TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass (line 363) | func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass(t *...
  function TestCLI_Subscribe_Override_Default_Token_With_Empty_Token (line 392) | func TestCLI_Subscribe_Override_Default_Token_With_Empty_Token(t *testin...

FILE: cmd/subscribe_unix.go
  constant scriptExt (line 6) | scriptExt                      = "sh"
  constant scriptHeader (line 7) | scriptHeader                   = "#!/bin/sh\n"
  constant clientCommandDescriptionSuffix (line 8) | clientCommandDescriptionSuffix = `The default config file for all client...

FILE: cmd/subscribe_windows.go
  constant scriptExt (line 6) | scriptExt                      = "bat"
  constant scriptHeader (line 7) | scriptHeader                   = ""
  constant clientCommandDescriptionSuffix (line 8) | clientCommandDescriptionSuffix = `The default config file for all client...

FILE: cmd/tier.go
  function init (line 13) | func init() {
  constant defaultMessageLimit (line 18) | defaultMessageLimit             = 5000
  constant defaultMessageExpiryDuration (line 19) | defaultMessageExpiryDuration    = "12h"
  constant defaultEmailLimit (line 20) | defaultEmailLimit               = 20
  constant defaultCallLimit (line 21) | defaultCallLimit                = 0
  constant defaultReservationLimit (line 22) | defaultReservationLimit         = 3
  constant defaultAttachmentFileSizeLimit (line 23) | defaultAttachmentFileSizeLimit  = "15M"
  constant defaultAttachmentTotalSizeLimit (line 24) | defaultAttachmentTotalSizeLimit = "100M"
  constant defaultAttachmentExpiryDuration (line 25) | defaultAttachmentExpiryDuration = "6h"
  constant defaultAttachmentBandwidthLimit (line 26) | defaultAttachmentBandwidthLimit = "1G"
  function execTierAdd (line 168) | func execTierAdd(c *cli.Context) error {
  function execTierChange (line 242) | func execTierChange(c *cli.Context) error {
  function execTierDel (line 323) | func execTierDel(c *cli.Context) error {
  function execTierList (line 342) | func execTierList(c *cli.Context) error {
  function printTier (line 357) | func printTier(c *cli.Context, tier *user.Tier) {

FILE: cmd/tier_test.go
  function TestCLI_Tier_AddListChangeDelete (line 11) | func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
  function runTierCommand (line 57) | func runTierCommand(app *cli.App, conf *server.Config, args ...string) e...

FILE: cmd/token.go
  function init (line 15) | func init() {
  function execTokenAdd (line 102) | func execTokenAdd(c *cli.Context) error {
  function execTokenDel (line 141) | func execTokenDel(c *cli.Context) error {
  function execTokenList (line 165) | func execTokenList(c *cli.Context) error {
  function execTokenGenerate (line 224) | func execTokenGenerate(c *cli.Context) error {

FILE: cmd/token_test.go
  function TestCLI_Token_AddListRemove (line 13) | func TestCLI_Token_AddListRemove(t *testing.T) {
  function runTokenCommand (line 41) | func runTokenCommand(app *cli.App, conf *server.Config, args ...string) ...

FILE: cmd/user.go
  constant tierReset (line 22) | tierReset = "-"
  function init (line 25) | func init() {
  function execUserAdd (line 195) | func execUserAdd(c *cli.Context) error {
  function execUserDel (line 236) | func execUserDel(c *cli.Context) error {
  function execUserChangePass (line 257) | func execUserChangePass(c *cli.Context) error {
  function execUserChangeRole (line 289) | func execUserChangeRole(c *cli.Context) error {
  function execUserHash (line 311) | func execUserHash(c *cli.Context) error {
  function execUserChangeTier (line 324) | func execUserChangeTier(c *cli.Context) error {
  function execUserList (line 355) | func execUserList(c *cli.Context) error {
  function createUserManager (line 367) | func createUserManager(c *cli.Context) (*user.Manager, error) {
  function readPasswordAndConfirm (line 397) | func readPasswordAndConfirm(c *cli.Context) (string, error) {

FILE: cmd/user_test.go
  function TestCLI_User_Add (line 14) | func TestCLI_User_Add(t *testing.T) {
  function TestCLI_User_Add_Exists (line 24) | func TestCLI_User_Add_Exists(t *testing.T) {
  function TestCLI_User_Add_Admin (line 40) | func TestCLI_User_Add_Admin(t *testing.T) {
  function TestCLI_User_Add_Password_Mismatch (line 50) | func TestCLI_User_Add_Password_Mismatch(t *testing.T) {
  function TestCLI_User_ChangePass (line 61) | func TestCLI_User_ChangePass(t *testing.T) {
  function TestCLI_User_ChangeRole (line 86) | func TestCLI_User_ChangeRole(t *testing.T) {
  function TestCLI_User_Delete (line 102) | func TestCLI_User_Delete(t *testing.T) {
  function newTestServerWithAuth (line 124) | func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server...
  function runUserCommand (line 135) | func runUserCommand(app *cli.App, conf *server.Config, args ...string) e...

FILE: cmd/webpush.go
  function init (line 19) | func init() {
  function generateWebPushKeys (line 41) | func generateWebPushKeys(c *cli.Context) error {

FILE: cmd/webpush_test.go
  function TestCLI_WebPush_GenerateKeys (line 12) | func TestCLI_WebPush_GenerateKeys(t *testing.T) {
  function TestCLI_WebPush_WriteKeysToFile (line 18) | func TestCLI_WebPush_WriteKeysToFile(t *testing.T) {
  function runWebPushCommand (line 27) | func runWebPushCommand(app *cli.App, conf *server.Config, args ...string...

FILE: db/db.go
  constant tag (line 13) | tag                            = "db"
  constant replicaHealthCheckInitialDelay (line 14) | replicaHealthCheckInitialDelay = 5 * time.Second
  constant replicaHealthCheckInterval (line 15) | replicaHealthCheckInterval     = 30 * time.Second
  constant replicaHealthCheckTimeout (line 16) | replicaHealthCheckTimeout      = 10 * time.Second
  type DB (line 22) | type DB struct
    method Query (line 46) | func (d *DB) Query(query string, args ...any) (*sql.Rows, error) {
    method QueryRow (line 51) | func (d *DB) QueryRow(query string, args ...any) *sql.Row {
    method Exec (line 56) | func (d *DB) Exec(query string, args ...any) (sql.Result, error) {
    method Begin (line 61) | func (d *DB) Begin() (*sql.Tx, error) {
    method Ping (line 66) | func (d *DB) Ping() error {
    method Primary (line 72) | func (d *DB) Primary() *sql.DB {
    method ReadOnly (line 78) | func (d *DB) ReadOnly() *sql.DB {
    method Close (line 94) | func (d *DB) Close() error {
    method healthCheckLoop (line 103) | func (d *DB) healthCheckLoop(ctx context.Context) {
    method checkReplicas (line 121) | func (d *DB) checkReplicas(ctx context.Context) {
  function New (line 32) | func New(primary *Host, replicas []*Host) *DB {

FILE: db/pg/pg.go
  function Open (line 18) | func Open(dsn string) (*db.Host, error) {
  function OpenReplica (line 31) | func OpenReplica(dsn string) (*db.Host, error) {
  function open (line 39) | func open(dsn string) (*db.Host, error) {
  function extractIntParam (line 88) | func extractIntParam(q url.Values, key string, defaultValue int) (int, e...
  function censorPassword (line 102) | func censorPassword(u *url.URL) string {
  function extractDurationParam (line 109) | func extractDurationParam(q url.Values, key string, defaultValue time.Du...

FILE: db/pg/pg_test.go
  function TestOpen_InvalidScheme (line 10) | func TestOpen_InvalidScheme(t *testing.T) {
  function TestOpen_InvalidURL (line 18) | func TestOpen_InvalidURL(t *testing.T) {
  function TestCensorPassword (line 24) | func TestCensorPassword(t *testing.T) {

FILE: db/test/test.go
  constant testPoolMaxConns (line 15) | testPoolMaxConns = "2"
  function CreateTestPostgresSchema (line 20) | func CreateTestPostgresSchema(t *testing.T) string {
  function CreateTestPostgres (line 54) | func CreateTestPostgres(t *testing.T) *db.DB {

FILE: db/types.go
  type Beginner (line 10) | type Beginner interface
  type Querier (line 16) | type Querier interface
  type Host (line 21) | type Host struct

FILE: db/util.go
  function ExecTx (line 7) | func ExecTx(db Beginner, f func(tx *sql.Tx) error) error {
  function QueryTx (line 21) | func QueryTx[T any](db Beginner, f func(tx *sql.Tx) (T, error)) (T, erro...

FILE: docs/hooks.py
  function on_post_build (line 5) | def on_post_build(config, **kwargs):

FILE: docs/static/js/bcrypt.js
  function preferDefault (line 3) | function preferDefault(exports) {
  function _interopRequireDefault (line 48) | function _interopRequireDefault(e) {
  function randomBytes (line 98) | function randomBytes(len) {
  function setRandomFallback (line 125) | function setRandomFallback(random) {
  function genSaltSync (line 136) | function genSaltSync(rounds, seed_length) {
  function genSalt (line 161) | function genSalt(rounds, seed_length, callback) {
  function hashSync (line 201) | function hashSync(password, salt) {
  function hash (line 221) | function hash(password, salt, callback, progressCallback) {
  function safeStringCompare (line 262) | function safeStringCompare(known, unknown) {
  function compareSync (line 277) | function compareSync(password, hash) {
  function compare (line 299) | function compare(password, hashValue, callback, progressCallback) {
  function getRounds (line 351) | function getRounds(hash) {
  function getSalt (line 363) | function getSalt(hash) {
  function truncates (line 377) | function truncates(password) {
  function utf8Length (line 398) | function utf8Length(string) {
  function utf8Array (line 417) | function utf8Array(string) {
  function base64_encode (line 483) | function base64_encode(b, len) {
  function base64_decode (line 520) | function base64_decode(s, len) {
  function _encipher (line 796) | function _encipher(lr, off, P, S) {
  function _streamtoword (line 919) | function _streamtoword(data, offp) {
  function _key (line 935) | function _key(key, P, S) {
  function _ekskey (line 959) | function _ekskey(data, key, P, S) {
  function _crypt (line 1003) | function _crypt(b, salt, rounds, callback, progressCallback) {
  function _hash (line 1095) | function _hash(password, salt, callback, progressCallback) {
  function encodeBase64 (line 1191) | function encodeBase64(bytes, length) {
  function decodeBase64 (line 1202) | function decodeBase64(string, length) {

FILE: docs/static/js/config-generator.js
  function cacheElements (line 159) | function cacheElements(modal) {
  function collectValues (line 186) | function collectValues(els) {
  function collectRepeatableRows (line 267) | function collectRepeatableRows(modal, selector, extractor) {
  function hashPassword (line 280) | function hashPassword(username, password) {
  function formatAuthUsers (line 289) | function formatAuthUsers(values) {
  function formatAuthAcls (line 294) | function formatAuthAcls(values) {
  function formatAuthTokens (line 299) | function formatAuthTokens(values) {
  function generateServerYml (line 306) | function generateServerYml(values) {
  function generateDockerCompose (line 383) | function generateDockerCompose(values) {
  function generateEnvVars (line 457) | function generateEnvVars(values) {
  function generateVAPIDKeys (line 486) | function generateVAPIDKeys() {
  function arrayToBase64Url (line 508) | function arrayToBase64Url(arr) {
  function updateOutput (line 518) | function updateOutput(els) {
  function validate (line 554) | function validate(values) {
  function secureRandomInt (line 656) | function secureRandomInt(max) {
  function generateToken (line 662) | function generateToken() {
  function generatePassword (line 671) | function generatePassword() {
  function escapeHtml (line 680) | function escapeHtml(str) {
  function escapeYamlValue (line 684) | function escapeYamlValue(str) {
  function escapeShellValue (line 688) | function escapeShellValue(val) {
  function prefill (line 697) | function prefill(modal, key, value) {
  function switchPanel (line 702) | function switchPanel(modal, panelId) {
  function setHidden (line 712) | function setHidden(el, hidden) {
  function syncRadiosToHiddenInputs (line 723) | function syncRadiosToHiddenInputs(els) {
  function updateFeatureVisibility (line 749) | function updateFeatureVisibility(els, flags) {
  function updatePostgresFields (line 788) | function updatePostgresFields(modal, isPostgres) {
  function prefillDefaults (line 823) | function prefillDefaults(modal, flags) {
  function autoDetectServerType (line 868) | function autoDetectServerType(els, loginModeVal) {
  function updateVisibility (line 885) | function updateVisibility(els) {
  function addRepeatableRow (line 932) | function addRepeatableRow(container, type, onUpdate) {
  function openModal (line 990) | function openModal(els) {
  function closeModal (line 997) | function closeModal(els) {
  function resetAll (line 1002) | function resetAll(els) {
  function fillVAPIDKeys (line 1045) | function fillVAPIDKeys(els) {
  function setupModalEvents (line 1058) | function setupModalEvents(els) {
  function setupAuthEvents (line 1095) | function setupAuthEvents(els) {
  function setupServerTypeEvents (line 1132) | function setupServerTypeEvents(els) {
  function setupUnifiedPushEvents (line 1184) | function setupUnifiedPushEvents(els) {
  function setupFormListeners (line 1226) | function setupFormListeners(els) {
  function setupWebPushEvents (line 1304) | function setupWebPushEvents(els) {
  function initGenerator (line 1327) | function initGenerator() {

FILE: examples/publish-go/main.go
  function main (line 9) | func main() {

FILE: examples/subscribe-go/main.go
  function main (line 9) | func main() {

FILE: log/event.go
  constant fieldTag (line 15) | fieldTag       = "tag"
  constant fieldError (line 16) | fieldError     = "error"
  constant fieldTimeTaken (line 17) | fieldTimeTaken = "time_taken_ms"
  constant fieldExitCode (line 18) | fieldExitCode  = "exit_code"
  constant tagStdLog (line 19) | tagStdLog      = "stdlog"
  type Event (line 23) | type Event struct
    method Fatal (line 43) | func (e *Event) Fatal(message string, v ...any) {
    method Error (line 50) | func (e *Event) Error(message string, v ...any) *Event {
    method Warn (line 55) | func (e *Event) Warn(message string, v ...any) *Event {
    method Info (line 60) | func (e *Event) Info(message string, v ...any) *Event {
    method Debug (line 65) | func (e *Event) Debug(message string, v ...any) *Event {
    method Trace (line 70) | func (e *Event) Trace(message string, v ...any) *Event {
    method Tag (line 75) | func (e *Event) Tag(tag string) *Event {
    method Time (line 80) | func (e *Event) Time(t time.Time) *Event {
    method Timing (line 86) | func (e *Event) Timing(f func()) *Event {
    method Err (line 93) | func (e *Event) Err(err error) *Event {
    method Field (line 103) | func (e *Event) Field(key string, value any) *Event {
    method FieldIf (line 112) | func (e *Event) FieldIf(key string, value any, level Level) *Event {
    method Fields (line 120) | func (e *Event) Fields(fields Context) *Event {
    method With (line 131) | func (e *Event) With(contexters ...Contexter) *Event {
    method Render (line 147) | func (e *Event) Render(l Level, message string, v ...any) string {
    method Log (line 165) | func (e *Event) Log(l Level, message string, v ...any) *Event {
    method Loggable (line 173) | func (e *Event) Loggable(l Level) bool {
    method IsTrace (line 178) | func (e *Event) IsTrace() bool {
    method IsDebug (line 183) | func (e *Event) IsDebug() bool {
    method JSON (line 188) | func (e *Event) JSON() string {
    method String (line 199) | func (e *Event) String() string {
    method globalLevelWithOverride (line 211) | func (e *Event) globalLevelWithOverride() Level {
    method maybeApplyContexters (line 231) | func (e *Event) maybeApplyContexters() bool {
    method applyContexters (line 241) | func (e *Event) applyContexters() {
  function newEvent (line 36) | func newEvent() *Event {

FILE: log/log.go
  function init (line 32) | func init() {
  function Fatal (line 37) | func Fatal(message string, v ...any) {
  function Error (line 42) | func Error(message string, v ...any) {
  function Warn (line 47) | func Warn(message string, v ...any) {
  function Info (line 52) | func Info(message string, v ...any) {
  function Debug (line 57) | func Debug(message string, v ...any) {
  function Trace (line 62) | func Trace(message string, v ...any) {
  function With (line 67) | func With(contexts ...Contexter) *Event {
  function Field (line 72) | func Field(key string, value any) *Event {
  function Fields (line 77) | func Fields(fields Context) *Event {
  function Tag (line 82) | func Tag(tag string) *Event {
  function Time (line 87) | func Time(time time.Time) *Event {
  function Timing (line 92) | func Timing(f func()) *Event {
  function CurrentLevel (line 97) | func CurrentLevel() Level {
  function SetLevel (line 104) | func SetLevel(newLevel Level) {
  function SetLevelOverride (line 111) | func SetLevelOverride(field string, value string, level Level) {
  function ResetLevelOverrides (line 121) | func ResetLevelOverrides() {
  function CurrentFormat (line 128) | func CurrentFormat() Format {
  function SetFormat (line 135) | func SetFormat(newFormat Format) {
  function SetOutput (line 145) | func SetOutput(w io.Writer) {
  function File (line 158) | func File() string {
  function IsFile (line 165) | func IsFile() bool {
  function DisableDates (line 172) | func DisableDates() {
  function Loggable (line 177) | func Loggable(l Level) bool {
  function IsTrace (line 182) | func IsTrace() bool {
  function IsDebug (line 187) | func IsDebug() bool {
  type peekLogWriter (line 193) | type peekLogWriter struct
    method Write (line 197) | func (w *peekLogWriter) Write(p []byte) (n int, err error) {

FILE: log/log_test.go
  function TestMain (line 15) | func TestMain(m *testing.M) {
  function TestLog_TagContextFieldFields (line 22) | func TestLog_TagContextFieldFields(t *testing.T) {
  function TestLog_NoAllocIfNotPrinted (line 68) | func TestLog_NoAllocIfNotPrinted(t *testing.T) {
  function TestLog_Timing (line 138) | func TestLog_Timing(t *testing.T) {
  function TestLog_LevelOverrideAny (line 157) | func TestLog_LevelOverrideAny(t *testing.T) {
  function TestLog_LevelOverride_ManyOnSameField (line 180) | func TestLog_LevelOverride_ManyOnSameField(t *testing.T) {
  function TestLog_FieldIf (line 201) | func TestLog_FieldIf(t *testing.T) {
  function TestLog_UsingStdLogger_JSON (line 225) | func TestLog_UsingStdLogger_JSON(t *testing.T) {
  function TestLog_UsingStdLogger_Text (line 236) | func TestLog_UsingStdLogger_Text(t *testing.T) {
  function TestLog_File (line 247) | func TestLog_File(t *testing.T) {
  type fakeError (line 268) | type fakeError struct
    method Error (line 273) | func (e fakeError) Error() string {
    method Context (line 277) | func (e fakeError) Context() Context {
  type fakeVisitor (line 284) | type fakeVisitor struct
    method Context (line 290) | func (v *fakeVisitor) Context() Context {
  function resetState (line 298) | func resetState() {

FILE: log/types.go
  type Level (line 9) | type Level
    method String (line 21) | func (l Level) String() string {
    method MarshalJSON (line 40) | func (l Level) MarshalJSON() ([]byte, error) {
  constant TraceLevel (line 13) | TraceLevel Level = iota
  constant DebugLevel (line 14) | DebugLevel
  constant InfoLevel (line 15) | InfoLevel
  constant WarnLevel (line 16) | WarnLevel
  constant ErrorLevel (line 17) | ErrorLevel
  constant FatalLevel (line 18) | FatalLevel
  function ToLevel (line 46) | func ToLevel(s string) Level {
  type Format (line 66) | type Format
    method String (line 74) | func (f Format) String() string {
  constant TextFormat (line 70) | TextFormat Format = iota
  constant JSONFormat (line 71) | JSONFormat
  function ToFormat (line 86) | func ToFormat(s string) Format {
  type Contexter (line 98) | type Contexter interface
  type Context (line 103) | type Context
    method Merge (line 106) | func (c Context) Merge(other Context) {
  type levelOverride (line 112) | type levelOverride struct

FILE: main.go
  function main (line 19) | func main() {
  function maybeShortCommit (line 44) | func maybeShortCommit(commit string) string {

FILE: message/cache.go
  constant tagMessageCache (line 19) | tagMessageCache = "message_cache"
  type queries (line 25) | type queries struct
  type Cache (line 52) | type Cache struct
    method maybeLock (line 76) | func (c *Cache) maybeLock() {
    method maybeUnlock (line 82) | func (c *Cache) maybeUnlock() {
    method AddMessage (line 90) | func (c *Cache) AddMessage(m *model.Message) error {
    method AddMessages (line 99) | func (c *Cache) AddMessages(ms []*model.Message) error {
    method addMessages (line 103) | func (c *Cache) addMessages(ms []*model.Message) error {
    method Messages (line 190) | func (c *Cache) Messages(topic string, since model.SinceMarker, schedu...
    method messagesSinceTime (line 201) | func (c *Cache) messagesSinceTime(topic string, since model.SinceMarke...
    method messagesSinceID (line 216) | func (c *Cache) messagesSinceID(topic string, since model.SinceMarker,...
    method messagesLatest (line 231) | func (c *Cache) messagesLatest(topic string) ([]*model.Message, error) {
    method MessagesDue (line 240) | func (c *Cache) MessagesDue() ([]*model.Message, error) {
    method MessagesExpired (line 249) | func (c *Cache) MessagesExpired() ([]string, error) {
    method Message (line 270) | func (c *Cache) Message(id string) (*model.Message, error) {
    method UpdateMessageTime (line 283) | func (c *Cache) UpdateMessageTime(messageID string, timestamp int64) e...
    method MarkPublished (line 291) | func (c *Cache) MarkPublished(m *model.Message) error {
    method MessagesCount (line 299) | func (c *Cache) MessagesCount() (int, error) {
    method Topics (line 316) | func (c *Cache) Topics() ([]string, error) {
    method DeleteMessages (line 337) | func (c *Cache) DeleteMessages(ids ...string) error {
    method DeleteScheduledBySequenceID (line 352) | func (c *Cache) DeleteScheduledBySequenceID(topic, sequenceID string) ...
    method ExpireMessages (line 381) | func (c *Cache) ExpireMessages(topics ...string) error {
    method AttachmentsExpired (line 395) | func (c *Cache) AttachmentsExpired() ([]string, error) {
    method MarkAttachmentsDeleted (line 416) | func (c *Cache) MarkAttachmentsDeleted(ids ...string) error {
    method AttachmentBytesUsedBySender (line 430) | func (c *Cache) AttachmentBytesUsedBySender(sender string) (int64, err...
    method AttachmentBytesUsedByUser (line 439) | func (c *Cache) AttachmentBytesUsedByUser(userID string) (int64, error) {
    method readAttachmentBytesUsed (line 447) | func (c *Cache) readAttachmentBytesUsed(rows *sql.Rows) (int64, error) {
    method UpdateStats (line 462) | func (c *Cache) UpdateStats(messages int64) error {
    method Stats (line 470) | func (c *Cache) Stats() (messages int64, err error) {
    method Close (line 486) | func (c *Cache) Close() error {
    method processMessageBatches (line 490) | func (c *Cache) processMessageBatches() {
  function newCache (line 60) | func newCache(db *db.DB, queries queries, mu *sync.Mutex, batchSize int,...
  function readMessages (line 501) | func readMessages(rows *sql.Rows) ([]*model.Message, error) {
  function readMessage (line 517) | func readMessage(rows *sql.Rows) (*model.Message, error) {

FILE: message/cache_postgres.go
  constant postgresInsertMessageQuery (line 11) | postgresInsertMessageQuery = `
  constant postgresDeleteMessageQuery (line 15) | postgresDeleteMessageQuery                    = `DELETE FROM message WHE...
  constant postgresSelectScheduledMessageIDsBySeqIDQuery (line 16) | postgresSelectScheduledMessageIDsBySeqIDQuery = `SELECT mid FROM message...
  constant postgresDeleteScheduledBySequenceIDQuery (line 17) | postgresDeleteScheduledBySequenceIDQuery      = `DELETE FROM message WHE...
  constant postgresUpdateMessagesForTopicExpiryQuery (line 18) | postgresUpdateMessagesForTopicExpiryQuery     = `UPDATE message SET expi...
  constant postgresSelectMessagesByIDQuery (line 19) | postgresSelectMessagesByIDQuery               = `
  constant postgresSelectMessagesSinceTimeQuery (line 24) | postgresSelectMessagesSinceTimeQuery = `
  constant postgresSelectMessagesSinceTimeIncludeScheduledQuery (line 30) | postgresSelectMessagesSinceTimeIncludeScheduledQuery = `
  constant postgresSelectMessagesSinceIDQuery (line 36) | postgresSelectMessagesSinceIDQuery = `
  constant postgresSelectMessagesSinceIDIncludeScheduledQuery (line 44) | postgresSelectMessagesSinceIDIncludeScheduledQuery = `
  constant postgresSelectMessagesLatestQuery (line 51) | postgresSelectMessagesLatestQuery = `
  constant postgresSelectMessagesDueQuery (line 58) | postgresSelectMessagesDueQuery = `
  constant postgresSelectMessagesExpiredQuery (line 64) | postgresSelectMessagesExpiredQuery  = `SELECT mid FROM message WHERE exp...
  constant postgresUpdateMessagePublishedQuery (line 65) | postgresUpdateMessagePublishedQuery = `UPDATE message SET published = TR...
  constant postgresSelectMessagesCountQuery (line 66) | postgresSelectMessagesCountQuery    = `SELECT COUNT(*) FROM message`
  constant postgresSelectTopicsQuery (line 67) | postgresSelectTopicsQuery           = `SELECT topic FROM message GROUP B...
  constant postgresUpdateAttachmentDeletedQuery (line 69) | postgresUpdateAttachmentDeletedQuery       = `UPDATE message SET attachm...
  constant postgresSelectAttachmentsExpiredQuery (line 70) | postgresSelectAttachmentsExpiredQuery      = `SELECT mid FROM message WH...
  constant postgresSelectAttachmentsSizeBySenderQuery (line 71) | postgresSelectAttachmentsSizeBySenderQuery = `SELECT COALESCE(SUM(attach...
  constant postgresSelectAttachmentsSizeByUserIDQuery (line 72) | postgresSelectAttachmentsSizeByUserIDQuery = `SELECT COALESCE(SUM(attach...
  constant postgresSelectStatsQuery (line 74) | postgresSelectStatsQuery       = `SELECT value FROM message_stats WHERE ...
  constant postgresUpdateStatsQuery (line 75) | postgresUpdateStatsQuery       = `UPDATE message_stats SET value = $1 WH...
  constant postgresUpdateMessageTimeQuery (line 76) | postgresUpdateMessageTimeQuery = `UPDATE message SET time = $1 WHERE mid...
  function NewPostgresStore (line 106) | func NewPostgresStore(d *db.DB, batchSize int, batchTimeout time.Duratio...

FILE: message/cache_postgres_schema.go
  constant postgresCreateTablesQuery (line 12) | postgresCreateTablesQuery = `
  constant postgresCurrentSchemaVersion (line 60) | postgresCurrentSchemaVersion     = 14
  constant postgresInsertSchemaVersionQuery (line 61) | postgresInsertSchemaVersionQuery = `INSERT INTO schema_version (store, v...
  constant postgresSelectSchemaVersionQuery (line 62) | postgresSelectSchemaVersionQuery = `SELECT version FROM schema_version W...
  function setupPostgres (line 65) | func setupPostgres(db *sql.DB) error {
  function setupNewPostgresDB (line 75) | func setupNewPostgresDB(sqlDB *sql.DB) error {

FILE: message/cache_sqlite.go
  constant sqliteInsertMessageQuery (line 17) | sqliteInsertMessageQuery = `
  constant sqliteDeleteMessageQuery (line 21) | sqliteDeleteMessageQuery                    = `DELETE FROM messages WHER...
  constant sqliteSelectScheduledMessageIDsBySeqIDQuery (line 22) | sqliteSelectScheduledMessageIDsBySeqIDQuery = `SELECT mid FROM messages ...
  constant sqliteDeleteScheduledBySequenceIDQuery (line 23) | sqliteDeleteScheduledBySequenceIDQuery      = `DELETE FROM messages WHER...
  constant sqliteUpdateMessagesForTopicExpiryQuery (line 24) | sqliteUpdateMessagesForTopicExpiryQuery     = `UPDATE messages SET expir...
  constant sqliteSelectMessagesByIDQuery (line 25) | sqliteSelectMessagesByIDQuery               = `
  constant sqliteSelectMessagesSinceTimeQuery (line 30) | sqliteSelectMessagesSinceTimeQuery = `
  constant sqliteSelectMessagesSinceTimeIncludeScheduledQuery (line 36) | sqliteSelectMessagesSinceTimeIncludeScheduledQuery = `
  constant sqliteSelectMessagesSinceIDQuery (line 42) | sqliteSelectMessagesSinceIDQuery = `
  constant sqliteSelectMessagesSinceIDIncludeScheduledQuery (line 48) | sqliteSelectMessagesSinceIDIncludeScheduledQuery = `
  constant sqliteSelectMessagesLatestQuery (line 54) | sqliteSelectMessagesLatestQuery = `
  constant sqliteSelectMessagesDueQuery (line 61) | sqliteSelectMessagesDueQuery = `
  constant sqliteSelectMessagesExpiredQuery (line 67) | sqliteSelectMessagesExpiredQuery  = `SELECT mid FROM messages WHERE expi...
  constant sqliteUpdateMessagePublishedQuery (line 68) | sqliteUpdateMessagePublishedQuery = `UPDATE messages SET published = 1 W...
  constant sqliteSelectMessagesCountQuery (line 69) | sqliteSelectMessagesCountQuery    = `SELECT COUNT(*) FROM messages`
  constant sqliteSelectTopicsQuery (line 70) | sqliteSelectTopicsQuery           = `SELECT topic FROM messages GROUP BY...
  constant sqliteUpdateAttachmentDeletedQuery (line 72) | sqliteUpdateAttachmentDeletedQuery       = `UPDATE messages SET attachme...
  constant sqliteSelectAttachmentsExpiredQuery (line 73) | sqliteSelectAttachmentsExpiredQuery      = `SELECT mid FROM messages WHE...
  constant sqliteSelectAttachmentsSizeBySenderQuery (line 74) | sqliteSelectAttachmentsSizeBySenderQuery = `SELECT IFNULL(SUM(attachment...
  constant sqliteSelectAttachmentsSizeByUserIDQuery (line 75) | sqliteSelectAttachmentsSizeByUserIDQuery = `SELECT IFNULL(SUM(attachment...
  constant sqliteSelectStatsQuery (line 77) | sqliteSelectStatsQuery       = `SELECT value FROM stats WHERE key = 'mes...
  constant sqliteUpdateStatsQuery (line 78) | sqliteUpdateStatsQuery       = `UPDATE stats SET value = ? WHERE key = '...
  constant sqliteUpdateMessageTimeQuery (line 79) | sqliteUpdateMessageTimeQuery = `UPDATE messages SET time = ? WHERE mid = ?`
  function NewSQLiteStore (line 109) | func NewSQLiteStore(filename, startupQueries string, cacheDuration time....
  function NewMemStore (line 125) | func NewMemStore() (*Cache, error) {
  function NewNopStore (line 131) | func NewNopStore() (*Cache, error) {
  function createMemoryFilename (line 141) | func createMemoryFilename() string {

FILE: message/cache_sqlite_schema.go
  constant sqliteCreateTablesQuery (line 14) | sqliteCreateTablesQuery = `
  constant sqliteCurrentSchemaVersion (line 60) | sqliteCurrentSchemaVersion          = 14
  constant sqliteCreateSchemaVersionTableQuery (line 61) | sqliteCreateSchemaVersionTableQuery = `
  constant sqliteInsertSchemaVersionQuery (line 67) | sqliteInsertSchemaVersionQuery = `INSERT INTO schemaVersion VALUES (1, ?)`
  constant sqliteUpdateSchemaVersionQuery (line 68) | sqliteUpdateSchemaVersionQuery = `UPDATE schemaVersion SET version = ? W...
  constant sqliteSelectSchemaVersionQuery (line 69) | sqliteSelectSchemaVersionQuery = `SELECT version FROM schemaVersion WHER...
  constant sqliteMigrate0To1AlterMessagesTableQuery (line 75) | sqliteMigrate0To1AlterMessagesTableQuery = `
  constant sqliteMigrate1To2AlterMessagesTableQuery (line 82) | sqliteMigrate1To2AlterMessagesTableQuery = `
  constant sqliteMigrate2To3AlterMessagesTableQuery (line 87) | sqliteMigrate2To3AlterMessagesTableQuery = `
  constant sqliteMigrate3To4AlterMessagesTableQuery (line 97) | sqliteMigrate3To4AlterMessagesTableQuery = `
  constant sqliteMigrate4To5AlterMessagesTableQuery (line 102) | sqliteMigrate4To5AlterMessagesTableQuery = `
  constant sqliteMigrate5To6AlterMessagesTableQuery (line 137) | sqliteMigrate5To6AlterMessagesTableQuery = `
  constant sqliteMigrate6To7AlterMessagesTableQuery (line 142) | sqliteMigrate6To7AlterMessagesTableQuery = `
  constant sqliteMigrate7To8AlterMessagesTableQuery (line 147) | sqliteMigrate7To8AlterMessagesTableQuery = `
  constant sqliteMigrate8To9AlterMessagesTableQuery (line 152) | sqliteMigrate8To9AlterMessagesTableQuery = `
  constant sqliteMigrate9To10AlterMessagesTableQuery (line 157) | sqliteMigrate9To10AlterMessagesTableQuery = `
  constant sqliteMigrate9To10UpdateMessageExpiryQuery (line 166) | sqliteMigrate9To10UpdateMessageExpiryQuery = `UPDATE messages SET expire...
  constant sqliteMigrate10To11AlterMessagesTableQuery (line 169) | sqliteMigrate10To11AlterMessagesTableQuery = `
  constant sqliteMigrate11To12AlterMessagesTableQuery (line 178) | sqliteMigrate11To12AlterMessagesTableQuery = `
  constant sqliteMigrate12To13AlterMessagesTableQuery (line 183) | sqliteMigrate12To13AlterMessagesTableQuery = `
  constant sqliteMigrate13To14AlterMessagesTableQuery (line 188) | sqliteMigrate13To14AlterMessagesTableQuery = `
  function setupSQLite (line 214) | func setupSQLite(db *sql.DB, startupQueries string, cacheDuration time.D...
  function setupNewSQLite (line 243) | func setupNewSQLite(sqlDB *sql.DB) error {
  function runSQLiteStartupQueries (line 258) | func runSQLiteStartupQueries(db *sql.DB, startupQueries string) error {
  function sqliteMigrateFrom0 (line 267) | func sqliteMigrateFrom0(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom1 (line 283) | func sqliteMigrateFrom1(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom2 (line 296) | func sqliteMigrateFrom2(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom3 (line 309) | func sqliteMigrateFrom3(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom4 (line 322) | func sqliteMigrateFrom4(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom5 (line 335) | func sqliteMigrateFrom5(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom6 (line 348) | func sqliteMigrateFrom6(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom7 (line 361) | func sqliteMigrateFrom7(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom8 (line 374) | func sqliteMigrateFrom8(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom9 (line 387) | func sqliteMigrateFrom9(sqlDB *sql.DB, cacheDuration time.Duration) error {
  function sqliteMigrateFrom10 (line 403) | func sqliteMigrateFrom10(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom11 (line 416) | func sqliteMigrateFrom11(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom12 (line 429) | func sqliteMigrateFrom12(sqlDB *sql.DB, _ time.Duration) error {
  function sqliteMigrateFrom13 (line 442) | func sqliteMigrateFrom13(sqlDB *sql.DB, _ time.Duration) error {

FILE: message/cache_sqlite_test.go
  function TestSqliteStore_Migration_From0 (line 16) | func TestSqliteStore_Migration_From0(t *testing.T) {
  function TestSqliteStore_Migration_From1 (line 56) | func TestSqliteStore_Migration_From1(t *testing.T) {
  function TestSqliteStore_Migration_From9 (line 121) | func TestSqliteStore_Migration_From9(t *testing.T) {
  function TestSqliteStore_StartupQueries_WAL (line 224) | func TestSqliteStore_StartupQueries_WAL(t *testing.T) {
  function TestSqliteStore_StartupQueries_None (line 238) | func TestSqliteStore_StartupQueries_None(t *testing.T) {
  function TestSqliteStore_StartupQueries_Fail (line 249) | func TestSqliteStore_StartupQueries_Fail(t *testing.T) {
  function TestNopStore (line 255) | func TestNopStore(t *testing.T) {
  function newSqliteTestStoreFile (line 270) | func newSqliteTestStoreFile(t *testing.T) string {
  function newSqliteTestStoreFromFile (line 274) | func newSqliteTestStoreFromFile(t *testing.T, filename, startupQueries s...
  function checkSqliteSchemaVersion (line 281) | func checkSqliteSchemaVersion(t *testing.T, filename string) {

FILE: message/cache_test.go
  function newSqliteTestStore (line 18) | func newSqliteTestStore(t *testing.T) *message.Cache {
  function newMemTestStore (line 26) | func newMemTestStore(t *testing.T) *message.Cache {
  function newTestPostgresStore (line 33) | func newTestPostgresStore(t *testing.T) *message.Cache {
  function forEachBackend (line 40) | func forEachBackend(t *testing.T, f func(t *testing.T, s *message.Cache)) {
  function TestStore_Messages (line 52) | func TestStore_Messages(t *testing.T) {
  function TestStore_MessagesLock (line 115) | func TestStore_MessagesLock(t *testing.T) {
  function TestStore_MessagesScheduled (line 129) | func TestStore_MessagesScheduled(t *testing.T) {
  function TestStore_Topics (line 157) | func TestStore_Topics(t *testing.T) {
  function TestStore_MessagesTagsPrioAndTitle (line 174) | func TestStore_MessagesTagsPrioAndTitle(t *testing.T) {
  function TestStore_MessagesSinceID (line 189) | func TestStore_MessagesSinceID(t *testing.T) {
  function TestStore_Prune (line 253) | func TestStore_Prune(t *testing.T) {
  function TestStore_Attachments (line 292) | func TestStore_Attachments(t *testing.T) {
  function TestStore_AttachmentsExpired (line 371) | func TestStore_AttachmentsExpired(t *testing.T) {
  function TestStore_Sender (line 424) | func TestStore_Sender(t *testing.T) {
  function TestStore_DeleteScheduledBySequenceID (line 441) | func TestStore_DeleteScheduledBySequenceID(t *testing.T) {
  function TestStore_MessageByID (line 508) | func TestStore_MessageByID(t *testing.T) {
  function TestStore_MarkPublished (line 533) | func TestStore_MarkPublished(t *testing.T) {
  function TestStore_ExpireMessages (line 561) | func TestStore_ExpireMessages(t *testing.T) {
  function TestStore_MarkAttachmentsDeleted (line 602) | func TestStore_MarkAttachmentsDeleted(t *testing.T) {
  function TestStore_Stats (line 661) | func TestStore_Stats(t *testing.T) {
  function TestStore_AddMessages (line 682) | func TestStore_AddMessages(t *testing.T) {
  function TestStore_MessagesDue (line 713) | func TestStore_MessagesDue(t *testing.T) {
  function TestStore_MessageFieldRoundTrip (line 757) | func TestStore_MessageFieldRoundTrip(t *testing.T) {
  function TestStore_AddMessage_InvalidUTF8 (line 831) | func TestStore_AddMessage_InvalidUTF8(t *testing.T) {
  function TestStore_AddMessage_NullByte (line 920) | func TestStore_AddMessage_NullByte(t *testing.T) {
  function TestStore_AddMessage_InvalidUTF8InTitleAndTags (line 933) | func TestStore_AddMessage_InvalidUTF8InTitleAndTags(t *testing.T) {
  function TestStore_AddMessage_InvalidUTF8BatchDoesNotDropValidMessages (line 952) | func TestStore_AddMessage_InvalidUTF8BatchDoesNotDropValidMessages(t *te...

FILE: model/model.go
  constant OpenEvent (line 14) | OpenEvent          = "open"
  constant KeepaliveEvent (line 15) | KeepaliveEvent     = "keepalive"
  constant MessageEvent (line 16) | MessageEvent       = "message"
  constant MessageDeleteEvent (line 17) | MessageDeleteEvent = "message_delete"
  constant MessageClearEvent (line 18) | MessageClearEvent  = "message_clear"
  constant PollRequestEvent (line 19) | PollRequestEvent   = "poll_request"
  constant MessageIDLength (line 23) | MessageIDLength = 12
  type Message (line 32) | type Message struct
    method Context (line 55) | func (m *Message) Context() log.Context {
    method SanitizeUTF8 (line 76) | func (m *Message) SanitizeUTF8() {
    method ForJSON (line 95) | func (m *Message) ForJSON() *Message {
  type Attachment (line 105) | type Attachment struct
  type Action (line 114) | type Action struct
  function NewAction (line 129) | func NewAction() *Action {
  function NewMessage (line 137) | func NewMessage(event, topic, msg string) *Message {
  function NewOpenMessage (line 148) | func NewOpenMessage(topic string) *Message {
  function NewKeepaliveMessage (line 153) | func NewKeepaliveMessage(topic string) *Message {
  function NewDefaultMessage (line 158) | func NewDefaultMessage(topic, msg string) *Message {
  function NewActionMessage (line 163) | func NewActionMessage(event, topic, sequenceID string) *Message {
  function NewPollRequestMessage (line 170) | func NewPollRequestMessage(topic, pollID string) *Message {
  function ValidMessageID (line 177) | func ValidMessageID(s string) bool {
  type SinceMarker (line 182) | type SinceMarker struct
    method IsAll (line 198) | func (t SinceMarker) IsAll() bool {
    method IsNone (line 203) | func (t SinceMarker) IsNone() bool {
    method IsLatest (line 208) | func (t SinceMarker) IsLatest() bool {
    method IsID (line 213) | func (t SinceMarker) IsID() bool {
    method Time (line 218) | func (t SinceMarker) Time() time.Time {
    method ID (line 223) | func (t SinceMarker) ID() string {
  function NewSinceTime (line 188) | func NewSinceTime(timestamp int64) SinceMarker {
  function NewSinceID (line 193) | func NewSinceID(id string) SinceMarker {

FILE: payments/payments.go
  constant Available (line 9) | Available = true
  type SubscriptionStatus (line 12) | type SubscriptionStatus
  type PriceRecurringInterval (line 15) | type PriceRecurringInterval
  function Setup (line 18) | func Setup(stripeSecretKey string) {

FILE: payments/payments_dummy.go
  constant Available (line 7) | Available = false
  type SubscriptionStatus (line 10) | type SubscriptionStatus
  type PriceRecurringInterval (line 13) | type PriceRecurringInterval
  function Setup (line 16) | func Setup(stripeSecretKey string) {

FILE: server/actions.go
  constant actionIDLength (line 16) | actionIDLength = 10
  constant actionEOF (line 17) | actionEOF      = rune(0)
  constant actionsMax (line 18) | actionsMax     = 3
  constant actionView (line 22) | actionView      = "view"
  constant actionBroadcast (line 23) | actionBroadcast = "broadcast"
  constant actionHTTP (line 24) | actionHTTP      = "http"
  constant actionCopy (line 25) | actionCopy      = "copy"
  type actionParser (line 35) | type actionParser struct
    method Parse (line 123) | func (p *actionParser) Parse() ([]*model.Action, error) {
    method parseAction (line 138) | func (p *actionParser) parseAction() (*model.Action, error) {
    method parseSection (line 212) | func (p *actionParser) parseSection() (key string, value string, last ...
    method parseKey (line 230) | func (p *actionParser) parseKey() string {
    method parseValue (line 242) | func (p *actionParser) parseValue() (value string, last bool) {
    method parseQuotedValue (line 258) | func (p *actionParser) parseQuotedValue(quote rune) (value string, las...
    method slurpSpaces (line 288) | func (p *actionParser) slurpSpaces() {
    method peek (line 299) | func (p *actionParser) peek() (rune, int) {
    method eof (line 307) | func (p *actionParser) eof() bool {
  function parseActions (line 43) | func parseActions(s string) (actions []*model.Action, err error) {
  function parseActionsFromJSON (line 84) | func parseActionsFromJSON(s string) ([]*model.Action, error) {
  function parseActionsFromSimple (line 111) | func parseActionsFromSimple(s string) ([]*model.Action, error) {
  function populateAction (line 159) | func populateAction(newAction *model.Action, section int, key, value str...
  function isSpace (line 311) | func isSpace(r rune) bool {
  function isSectionEnd (line 315) | func isSectionEnd(r rune) bool {
  function isLastSection (line 319) | func isLastSection(r rune) bool {

FILE: server/actions_test.go
  function TestParseActions (line 9) | func TestParseActions(t *testing.T) {

FILE: server/config.go
  constant DefaultListenHTTP (line 18) | DefaultListenHTTP                           = ":80"
  constant DefaultCacheDuration (line 19) | DefaultCacheDuration                        = 12 * time.Hour
  constant DefaultCacheBatchTimeout (line 20) | DefaultCacheBatchTimeout                    = time.Duration(0)
  constant DefaultKeepaliveInterval (line 21) | DefaultKeepaliveInterval                    = 45 * time.Second
  constant DefaultManagerInterval (line 22) | DefaultManagerInterval                      = time.Minute
  constant DefaultDelayedSenderInterval (line 23) | DefaultDelayedSenderInterval                = 10 * time.Second
  constant DefaultMessageDelayMin (line 24) | DefaultMessageDelayMin                      = 10 * time.Second
  constant DefaultMessageDelayMax (line 25) | DefaultMessageDelayMax                      = 3 * 24 * time.Hour
  constant DefaultFirebaseKeepaliveInterval (line 26) | DefaultFirebaseKeepaliveInterval            = 3 * time.Hour
  constant DefaultFirebasePollInterval (line 27) | DefaultFirebasePollInterval                 = 20 * time.Minute
  constant DefaultFirebaseQuotaExceededPenaltyDuration (line 28) | DefaultFirebaseQuotaExceededPenaltyDuration = 10 * time.Minute
  constant DefaultStripePriceCacheDuration (line 29) | DefaultStripePriceCacheDuration             = 3 * time.Hour
  constant DefaultWebPushExpiryWarningDuration (line 40) | DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour
  constant DefaultWebPushExpiryDuration (line 41) | DefaultWebPushExpiryDuration        = 60 * 24 * time.Hour
  constant DefaultMessageSizeLimit (line 49) | DefaultMessageSizeLimit         = 4096
  constant DefaultTotalTopicLimit (line 50) | DefaultTotalTopicLimit          = 15000
  constant DefaultAttachmentTotalSizeLimit (line 51) | DefaultAttachmentTotalSizeLimit = int64(5 * 1024 * 1024 * 1024)
  constant DefaultAttachmentFileSizeLimit (line 52) | DefaultAttachmentFileSizeLimit  = int64(15 * 1024 * 1024)
  constant DefaultAttachmentExpiryDuration (line 53) | DefaultAttachmentExpiryDuration = 3 * time.Hour
  constant DefaultVisitorSubscriptionLimit (line 63) | DefaultVisitorSubscriptionLimit             = 30
  constant DefaultVisitorRequestLimitBurst (line 64) | DefaultVisitorRequestLimitBurst             = 60
  constant DefaultVisitorRequestLimitReplenish (line 65) | DefaultVisitorRequestLimitReplenish         = 5 * time.Second
  constant DefaultVisitorMessageDailyLimit (line 66) | DefaultVisitorMessageDailyLimit             = 0
  constant DefaultVisitorEmailLimitBurst (line 67) | DefaultVisitorEmailLimitBurst               = 16
  constant DefaultVisitorEmailLimitReplenish (line 68) | DefaultVisitorEmailLimitReplenish           = time.Hour
  constant DefaultVisitorAccountCreationLimitBurst (line 69) | DefaultVisitorAccountCreationLimitBurst     = 3
  constant DefaultVisitorAccountCreationLimitReplenish (line 70) | DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour
  constant DefaultVisitorAuthFailureLimitBurst (line 71) | DefaultVisitorAuthFailureLimitBurst         = 30
  constant DefaultVisitorAuthFailureLimitReplenish (line 72) | DefaultVisitorAuthFailureLimitReplenish     = time.Minute
  constant DefaultVisitorAttachmentTotalSizeLimit (line 73) | DefaultVisitorAttachmentTotalSizeLimit      = 100 * 1024 * 1024
  constant DefaultVisitorAttachmentDailyBandwidthLimit (line 74) | DefaultVisitorAttachmentDailyBandwidthLimit = 500 * 1024 * 1024
  constant DefaultVisitorPrefixBitsIPv4 (line 75) | DefaultVisitorPrefixBitsIPv4                = 32
  constant DefaultVisitorPrefixBitsIPv6 (line 76) | DefaultVisitorPrefixBitsIPv6                = 64
  type Config (line 89) | type Config struct
    method Hash (line 293) | func (c *Config) Hash() string {
  function NewConfig (line 194) | func NewConfig() *Config {

FILE: server/config_test.go
  function TestConfig_New (line 9) | func TestConfig_New(t *testing.T) {

FILE: server/config_unix.go
  function init (line 5) | func init() {

FILE: server/config_windows.go
  function init (line 10) | func init() {

FILE: server/errors.go
  type errHTTP (line 12) | type errHTTP struct
    method Error (line 20) | func (e errHTTP) Error() string {
    method JSON (line 24) | func (e errHTTP) JSON() string {
    method Context (line 29) | func (e errHTTP) Context() log.Context {
    method Wrap (line 41) | func (e errHTTP) Wrap(message string, args ...any) *errHTTP {
    method With (line 47) | func (e errHTTP) With(contexters ...log.Contexter) *errHTTP {
    method Fields (line 58) | func (e errHTTP) Fields(context log.Context) *errHTTP {
    method clone (line 67) | func (e errHTTP) clone() errHTTP {
  type errWebSocketPostUpgrade (line 84) | type errWebSocketPostUpgrade struct
    method Error (line 88) | func (e *errWebSocketPostUpgrade) Error() string {
    method Unwrap (line 92) | func (e *errWebSocketPostUpgrade) Unwrap() error {

FILE: server/file_cache.go
  type fileCache (line 22) | type fileCache struct
    method Write (line 44) | func (c *fileCache) Write(id string, in io.Reader, limiters ...util.Li...
    method Remove (line 76) | func (c *fileCache) Remove(ids ...string) error {
    method Size (line 98) | func (c *fileCache) Size() int64 {
    method Remaining (line 104) | func (c *fileCache) Remaining() int64 {
  function newFileCache (line 29) | func newFileCache(dir string, totalSizeLimit int64) (*fileCache, error) {
  function dirSize (line 114) | func dirSize(dir string) (int64, error) {

FILE: server/file_cache_test.go
  function TestFileCache_Write_Success (line 17) | func TestFileCache_Write_Success(t *testing.T) {
  function TestFileCache_Write_Remove_Success (line 27) | func TestFileCache_Write_Remove_Success(t *testing.T) {
  function TestFileCache_Write_FailedTotalSizeLimit (line 46) | func TestFileCache_Write_FailedTotalSizeLimit(t *testing.T) {
  function TestFileCache_Write_FailedAdditionalLimiter (line 58) | func TestFileCache_Write_FailedAdditionalLimiter(t *testing.T) {
  function newTestFileCache (line 65) | func newTestFileCache(t *testing.T) (dir string, cache *fileCache) {
  function readFile (line 72) | func readFile(t *testing.T, f string) string {

FILE: server/log.go
  constant tagStartup (line 19) | tagStartup      = "startup"
  constant tagHTTP (line 20) | tagHTTP         = "http"
  constant tagPublish (line 21) | tagPublish      = "publish"
  constant tagSubscribe (line 22) | tagSubscribe    = "subscribe"
  constant tagFirebase (line 23) | tagFirebase     = "firebase"
  constant tagSMTP (line 24) | tagSMTP         = "smtp"
  constant tagEmail (line 25) | tagEmail        = "email"
  constant tagTwilio (line 26) | tagTwilio       = "twilio"
  constant tagFileCache (line 27) | tagFileCache    = "file_cache"
  constant tagMessageCache (line 28) | tagMessageCache = "message_cache"
  constant tagStripe (line 29) | tagStripe       = "stripe"
  constant tagAccount (line 30) | tagAccount      = "account"
  constant tagManager (line 31) | tagManager      = "manager"
  constant tagResetter (line 32) | tagResetter     = "resetter"
  constant tagWebsocket (line 33) | tagWebsocket    = "websocket"
  constant tagMatrix (line 34) | tagMatrix       = "matrix"
  constant tagWebPush (line 35) | tagWebPush      = "webpush"
  function logr (line 44) | func logr(r *http.Request) *log.Event {
  function logv (line 49) | func logv(v *visitor) *log.Event {
  function logvr (line 54) | func logvr(v *visitor, r *http.Request) *log.Event {
  function logvrm (line 59) | func logvrm(v *visitor, r *http.Request, m *model.Message) *log.Event {
  function logvm (line 64) | func logvm(v *visitor, m *model.Message) *log.Event {
  function logem (line 69) | func logem(smtpConn *smtp.Conn) *log.Event {
  function httpContext (line 77) | func httpContext(r *http.Request) log.Context {
  function websocketErrorContext (line 88) | func websocketErrorContext(err error) log.Context {
  function renderHTTPRequest (line 102) | func renderHTTPRequest(r *http.Request) string {

FILE: server/server.go
  type Server (line 48) | type Server struct
    method Run (line 307) | func (s *Server) Run() error {
    method Stop (line 410) | func (s *Server) Stop() {
    method closeDatabases (line 429) | func (s *Server) closeDatabases() {
    method handle (line 443) | func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
    method handleError (line 468) | func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v...
    method handleInternal (line 512) | func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request...
    method handleRoot (line 631) | func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request, v ...
    method handleTopic (line 636) | func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request, v...
    method handleEmpty (line 648) | func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _...
    method handleTopicAuth (line 652) | func (s *Server) handleTopicAuth(w http.ResponseWriter, _ *http.Reques...
    method handleHealth (line 656) | func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, ...
    method handleConfig (line 663) | func (s *Server) handleConfig(w http.ResponseWriter, _ *http.Request, ...
    method handleWebConfig (line 668) | func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Reques...
    method configResponse (line 679) | func (s *Server) configResponse() *apiConfigResponse {
    method handleWebManifest (line 699) | func (s *Server) handleWebManifest(w http.ResponseWriter, _ *http.Requ...
    method handleMetrics (line 719) | func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request,...
    method handleStatic (line 725) | func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, ...
    method handleDocs (line 732) | func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request, _ ...
    method handleStats (line 738) | func (s *Server) handleStats(w http.ResponseWriter, _ *http.Request, _...
    method handleFile (line 755) | func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v ...
    method handleMatrixDiscovery (line 825) | func (s *Server) handleMatrixDiscovery(w http.ResponseWriter) error {
    method handlePublishInternal (line 832) | func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*...
    method handlePublish (line 954) | func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request,...
    method handlePublishMatrix (line 964) | func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Re...
    method handleDelete (line 989) | func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, ...
    method handleClear (line 993) | func (s *Server) handleClear(w http.ResponseWriter, r *http.Request, v...
    method handleActionMessage (line 997) | func (s *Server) handleActionMessage(w http.ResponseWriter, r *http.Re...
    method sendToFirebase (line 1054) | func (s *Server) sendToFirebase(v *visitor, m *model.Message) {
    method sendEmail (line 1068) | func (s *Server) sendEmail(v *visitor, m *model.Message, email string) {
    method forwardPollRequest (line 1078) | func (s *Server) forwardPollRequest(v *visitor, m *model.Message) {
    method parsePublishParams (line 1110) | func (s *Server) parsePublishParams(r *http.Request, m *model.Message)...
    method handlePublishBody (line 1261) | func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *mod...
    method handleBodyDiscard (line 1278) | func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error {
    method handleBodyAsMessageAutoDetect (line 1284) | func (s *Server) handleBodyAsMessageAutoDetect(m *model.Message, body ...
    method handleBodyAsTextMessage (line 1294) | func (s *Server) handleBodyAsTextMessage(m *model.Message, body *util....
    method handleBodyAsTemplatedTextMessage (line 1307) | func (s *Server) handleBodyAsTemplatedTextMessage(m *model.Message, te...
    method renderTemplateFromFile (line 1332) | func (s *Server) renderTemplateFromFile(m *model.Message, templateName...
    method renderTemplateFromParams (line 1374) | func (s *Server) renderTemplateFromParams(m *model.Message, peekedBody...
    method renderTemplate (line 1395) | func (s *Server) renderTemplate(name, tpl, source string) (string, err...
    method handleBodyAsAttachment (line 1415) | func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m...
    method handleSubscribeJSON (line 1465) | func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Re...
    method handleSubscribeSSE (line 1476) | func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Req...
    method handleSubscribeRaw (line 1490) | func (s *Server) handleSubscribeRaw(w http.ResponseWriter, r *http.Req...
    method handleSubscribeHTTP (line 1500) | func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Re...
    method handleSubscribeWS (line 1600) | func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Requ...
    method maybeSetRateVisitors (line 1759) | func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, top...
    method setRateVisitors (line 1804) | func (s *Server) setRateVisitors(r *http.Request, v *visitor, rateTopi...
    method sendOldMessages (line 1817) | func (s *Server) sendOldMessages(topics []*topic, since model.SinceMar...
    method handleOptions (line 1872) | func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request,...
    method topicFromPath (line 1880) | func (s *Server) topicFromPath(path string) (*topic, error) {
    method topicsFromPath (line 1889) | func (s *Server) topicsFromPath(path string) ([]*topic, string, error) {
    method sequenceIDFromPath (line 1903) | func (s *Server) sequenceIDFromPath(path string) (string, *errHTTP) {
    method topicsFromIDs (line 1912) | func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
    method topicFromID (line 1932) | func (s *Server) topicFromID(id string) (*topic, error) {
    method topicsFromPattern (line 1941) | func (s *Server) topicsFromPattern(pattern string) ([]*topic, error) {
    method runSMTPServer (line 1957) | func (s *Server) runSMTPServer() error {
    method runManager (line 1970) | func (s *Server) runManager() {
    method runStatsResetter (line 1986) | func (s *Server) runStatsResetter() {
    method resetStats (line 2003) | func (s *Server) resetStats() {
    method runFirebaseKeepaliver (line 2017) | func (s *Server) runFirebaseKeepaliver() {
    method runDelayedSender (line 2040) | func (s *Server) runDelayedSender() {
    method sendDelayedMessages (line 2053) | func (s *Server) sendDelayedMessages() error {
    method sendDelayedMessage (line 2075) | func (s *Server) sendDelayedMessage(v *visitor, m *model.Message) error {
    method transformBodyJSON (line 2105) | func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
    method transformMatrixJSON (line 2172) | func (s *Server) transformMatrixJSON(next handleFunc) handleFunc {
    method authorizeTopicWrite (line 2190) | func (s *Server) authorizeTopicWrite(next handleFunc) handleFunc {
    method authorizeTopicRead (line 2194) | func (s *Server) authorizeTopicRead(next handleFunc) handleFunc {
    method authorizeTopic (line 2198) | func (s *Server) authorizeTopic(next handleFunc, perm user.Permission)...
    method maybeAuthenticate (line 2229) | func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) {
    method authenticate (line 2260) | func (s *Server) authenticate(r *http.Request, header string) (user *u...
    method authenticateBasicAuth (line 2290) | func (s *Server) authenticateBasicAuth(r *http.Request, value string) ...
    method authenticateBearerAuth (line 2301) | func (s *Server) authenticateBearerAuth(r *http.Request, token string)...
    method visitor (line 2314) | func (s *Server) visitor(ip netip.Addr, user *user.User) *visitor {
    method writeJSON (line 2328) | func (s *Server) writeJSON(w http.ResponseWriter, v any) error {
    method writeJSONWithContentType (line 2332) | func (s *Server) writeJSONWithContentType(w http.ResponseWriter, v any...
    method updateAndWriteStats (line 2341) | func (s *Server) updateAndWriteStats(messagesCount int64) {
  type handleFunc (line 76) | type handleFunc
  constant firebaseControlTopic (line 148) | firebaseControlTopic     = "~control"
  constant firebasePollTopic (line 149) | firebasePollTopic        = "~poll"
  constant emptyMessageBody (line 150) | emptyMessageBody         = "triggered"
  constant newMessageBody (line 151) | newMessageBody           = "New message"
  constant defaultAttachmentMessage (line 152) | defaultAttachmentMessage = "You received a file: %s"
  constant encodingBase64 (line 153) | encodingBase64           = "base64"
  constant jsonBodyBytesLimit (line 154) | jsonBodyBytesLimit       = 131072
  constant unifiedPushTopicPrefix (line 155) | unifiedPushTopicPrefix   = "up"
  constant unifiedPushTopicLength (line 156) | unifiedPushTopicLength   = 14
  constant messagesHistoryMax (line 157) | messagesHistoryMax       = 10
  constant templateMaxExecutionTime (line 158) | templateMaxExecutionTime = 100 * time.Millisecond
  constant templateMaxOutputBytes (line 159) | templateMaxOutputBytes   = 1024 * 1024
  constant templateFileExtension (line 160) | templateFileExtension    = ".yml"
  constant wsWriteWait (line 165) | wsWriteWait  = 2 * time.Second
  constant wsBufferSize (line 166) | wsBufferSize = 1024
  constant wsReadLimit (line 167) | wsReadLimit  = 64
  constant wsPongWait (line 168) | wsPongWait   = 15 * time.Second
  function New (line 173) | func New(conf *Config) (*Server, error) {
  function createMessageCache (line 294) | func createMessageCache(conf *Config, pool *db.DB) (*message.Cache, erro...
  function parseSubscribeParams (line 1736) | func parseSubscribeParams(r *http.Request) (poll bool, since model.Since...
  function parseSince (line 1844) | func parseSince(r *http.Request, poll bool) (model.SinceMarker, error) {
  function readAuthHeader (line 2269) | func readAuthHeader(r *http.Request) (string, error) {
  function supportedAuthHeader (line 2285) | func supportedAuthHeader(value string) bool {

FILE: server/server_account.go
  constant syncTopicAccountSyncEvent (line 18) | syncTopicAccountSyncEvent = "sync"
  constant tokenExpiryDuration (line 19) | tokenExpiryDuration       = 72 * time.Hour
  method handleAccountCreate (line 22) | func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Requ...
  method handleAccountGet (line 52) | func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request...
  method handleAccountDelete (line 170) | func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Requ...
  method handleAccountPasswordChange (line 208) | func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *h...
  method handleAccountTokenCreate (line 229) | func (s *Server) handleAccountTokenCreate(w http.ResponseWriter, r *http...
  method handleAccountTokenUpdate (line 264) | func (s *Server) handleAccountTokenUpdate(w http.ResponseWriter, r *http...
  method handleAccountTokenDelete (line 305) | func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http...
  method handleAccountSettingsChange (line 327) | func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *h...
  method handleAccountSubscriptionAdd (line 361) | func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *...
  method handleAccountSubscriptionChange (line 384) | func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, ...
  method handleAccountSubscriptionDelete (line 412) | func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, ...
  method handleAccountReservationAdd (line 441) | func (s *Server) handleAccountReservationAdd(w http.ResponseWriter, r *h...
  method handleAccountReservationDelete (line 488) | func (s *Server) handleAccountReservationDelete(w http.ResponseWriter, r...
  method maybeRemoveMessagesAndExcessReservations (line 527) | func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Reques...
  method handleAccountPhoneNumberVerify (line 543) | func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r...
  method handleAccountPhoneNumberAdd (line 574) | func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *h...
  method handleAccountPhoneNumberDelete (line 593) | func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r...
  method publishSyncEventAsync (line 610) | func (s *Server) publishSyncEventAsync(v *visitor) {
  method publishSyncEvent (line 619) | func (s *Server) publishSyncEvent(v *visitor) error {

FILE: server/server_account_test.go
  function TestAccount_Signup_Success (line 18) | func TestAccount_Signup_Success(t *testing.T) {
  function TestAccount_Signup_UserExists (line 57) | func TestAccount_Signup_UserExists(t *testing.T) {
  function TestAccount_Signup_LimitReached (line 73) | func TestAccount_Signup_LimitReached(t *testing.T) {
  function TestAccount_Signup_AsUser (line 90) | func TestAccount_Signup_AsUser(t *testing.T) {
  function TestAccount_Signup_Disabled (line 114) | func TestAccount_Signup_Disabled(t *testing.T) {
  function TestAccount_Signup_Rate_Limit (line 127) | func TestAccount_Signup_Rate_Limit(t *testing.T) {
  function TestAccount_Get_Anonymous (line 143) | func TestAccount_Get_Anonymous(t *testing.T) {
  function TestAccount_ChangeSettings (line 188) | func TestAccount_ChangeSettings(t *testing.T) {
  function TestAccount_Subscription_AddUpdateDelete (line 219) | func TestAccount_Subscription_AddUpdateDelete(t *testing.T) {
  function TestAccount_ChangePassword (line 272) | func TestAccount_ChangePassword(t *testing.T) {
  function TestAccount_ChangePassword_NoAccount (line 317) | func TestAccount_ChangePassword_NoAccount(t *testing.T) {
  function TestAccount_ExtendToken (line 327) | func TestAccount_ExtendToken(t *testing.T) {
  function TestAccount_ExtendToken_NoTokenProvided (line 366) | func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) {
  function TestAccount_DeleteToken (line 381) | func TestAccount_DeleteToken(t *testing.T) {
  function TestAccount_Delete_Success (line 423) | func TestAccount_Delete_Success(t *testing.T) {
  function TestAccount_Delete_Not_Allowed (line 454) | func TestAccount_Delete_Not_Allowed(t *testing.T) {
  function TestAccount_Reservation_AddWithoutTierFails (line 477) | func TestAccount_Reservation_AddWithoutTierFails(t *testing.T) {
  function TestAccount_Reservation_AddAdminSuccess (line 493) | func TestAccount_Reservation_AddAdminSuccess(t *testing.T) {
  function TestAccount_Reservation_AddRemoveUserWithTierSuccess (line 547) | func TestAccount_Reservation_AddRemoveUserWithTierSuccess(t *testing.T) {
  function TestAccount_Reservation_PublishByAnonymousFails (line 635) | func TestAccount_Reservation_PublishByAnonymousFails(t *testing.T) {
  function TestAccount_Reservation_Delete_Messages_And_Attachments (line 671) | func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing....

FILE: server/server_admin.go
  method handleVersion (line 9) | func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request, v...
  method handleUsersGet (line 17) | func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, ...
  method handleUsersAdd (line 49) | func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, ...
  method handleUsersUpdate (line 86) | func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Reques...
  method handleUsersDelete (line 133) | func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Reques...
  method handleAccessAllow (line 155) | func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Reques...
  method handleAccessReset (line 176) | func (s *Server) handleAccessReset(w http.ResponseWriter, r *http.Reques...
  method killUserSubscriber (line 194) | func (s *Server) killUserSubscriber(u *user.User, topicPattern string) e...

FILE: server/server_admin_test.go
  function TestVersion_Admin (line 13) | func TestVersion_Admin(t *testing.T) {
  function TestUser_AddRemove (line 50) | func TestUser_AddRemove(t *testing.T) {
  function TestUser_AddWithPasswordHash (line 108) | func TestUser_AddWithPasswordHash(t *testing.T) {
  function TestUser_ChangeUserPassword (line 139) | func TestUser_ChangeUserPassword(t *testing.T) {
  function TestUser_ChangeUserTier (line 179) | func TestUser_ChangeUserTier(t *testing.T) {
  function TestUser_ChangeUserPasswordAndTier (line 221) | func TestUser_ChangeUserPasswordAndTier(t *testing.T) {
  function TestUser_ChangeUserPasswordWithHash (line 275) | func TestUser_ChangeUserPasswordWithHash(t *testing.T) {
  function TestUser_DontChangeAdminPassword (line 309) | func TestUser_DontChangeAdminPassword(t *testing.T) {
  function TestUser_AddRemove_Failures (line 326) | func TestUser_AddRemove_Failures(t *testing.T) {
  function TestAccess_AllowReset (line 367) | func TestAccess_AllowReset(t *testing.T) {
  function TestAccess_AllowReset_NonAdminAttempt (line 410) | func TestAccess_AllowReset_NonAdminAttempt(t *testing.T) {
  function TestAccess_AllowReset_KillConnection (line 428) | func TestAccess_AllowReset_KillConnection(t *testing.T) {

FILE: server/server_firebase.go
  constant FirebaseAvailable (line 22) | FirebaseAvailable = true
  constant fcmMessageLimit (line 24) | fcmMessageLimit         = 4000
  constant fcmApnsBodyMessageLimit (line 25) | fcmApnsBodyMessageLimit = 100
  type firebaseClient (line 35) | type firebaseClient struct
    method Send (line 47) | func (c *firebaseClient) Send(v *visitor, m *model.Message) error {
  function newFirebaseClient (line 40) | func newFirebaseClient(sender firebaseSender, auther user.Auther) *fireb...
  type firebaseSender (line 72) | type firebaseSender interface
  type firebaseSenderImpl (line 79) | type firebaseSenderImpl struct
    method Send (line 97) | func (c *firebaseSenderImpl) Send(m *messaging.Message) error {
  function newFirebaseSender (line 83) | func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
  function toFirebaseMessage (line 125) | func toFirebaseMessage(m *model.Message, auther user.Auther) (*messaging...
  function maybeTruncateFCMMessage (line 220) | func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message {
  function createAPNSAlertConfig (line 239) | func createAPNSAlertConfig(m *model.Message, data map[string]string) *me...
  function createAPNSBackgroundConfig (line 263) | func createAPNSBackgroundConfig(data map[string]string) *messaging.APNSC...
  function maybeTruncateAPNSBodyMessage (line 288) | func maybeTruncateAPNSBodyMessage(s string) string {
  function toPollRequest (line 300) | func toPollRequest(m *model.Message) *model.Message {

FILE: server/server_firebase_dummy.go
  constant FirebaseAvailable (line 14) | FirebaseAvailable = false
  type firebaseClient (line 22) | type firebaseClient struct
    method Send (line 25) | func (c *firebaseClient) Send(v *visitor, m *model.Message) error {
  type firebaseSender (line 29) | type firebaseSender interface
  function newFirebaseClient (line 33) | func newFirebaseClient(sender firebaseSender, auther user.Auther) *fireb...
  function newFirebaseSender (line 37) | func newFirebaseSender(credentialsFile string) (firebaseSender, error) {

FILE: server/server_firebase_test.go
  type testAuther (line 20) | type testAuther struct
    method Authenticate (line 26) | func (t testAuther) Authenticate(_, _ string) (*user.User, error) {
    method Authorize (line 30) | func (t testAuther) Authorize(_ *user.User, _ string, _ user.Permissio...
  type testFirebaseSender (line 37) | type testFirebaseSender struct
    method Send (line 50) | func (s *testFirebaseSender) Send(m *messaging.Message) error {
    method Messages (line 60) | func (s *testFirebaseSender) Messages() []*messaging.Message {
  function newTestFirebaseSender (line 43) | func newTestFirebaseSender(allowed int) *testFirebaseSender {
  function TestToFirebaseMessage_Keepalive (line 66) | func TestToFirebaseMessage_Keepalive(t *testing.T) {
  function TestToFirebaseMessage_Open (line 97) | func TestToFirebaseMessage_Open(t *testing.T) {
  function TestToFirebaseMessage_Message_Normal_Allowed (line 128) | func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
  function TestToFirebaseMessage_Message_Normal_Not_Allowed (line 222) | func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) {
  function TestToFirebaseMessage_PollRequest (line 253) | func TestToFirebaseMessage_PollRequest(t *testing.T) {
  function TestMaybeTruncateFCMMessage (line 288) | func TestMaybeTruncateFCMMessage(t *testing.T) {
  function TestMaybeTruncateFCMMessage_NotTooLong (line 318) | func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) {
  function TestToFirebaseSender_Abuse (line 345) | func TestToFirebaseSender_Abuse(t *testing.T) {

FILE: server/server_manager.go
  method execManager (line 9) | func (s *Server) execManager() {
  method pruneVisitors (line 104) | func (s *Server) pruneVisitors() {
  method pruneTokens (line 123) | func (s *Server) pruneTokens() {
  method pruneAttachments (line 139) | func (s *Server) pruneAttachments() {
  method pruneMessages (line 166) | func (s *Server) pruneMessages() {

FILE: server/server_manager_test.go
  function TestServer_Manager_Prune_Messages_Without_Attachments_DoesNotPanic (line 9) | func TestServer_Manager_Prune_Messages_Without_Attachments_DoesNotPanic(...

FILE: server/server_matrix.go
  type matrixRequest (line 61) | type matrixRequest struct
  type matrixResponse (line 71) | type matrixResponse struct
  constant matrixRejectPushKeyForUnifiedPushTopicWithoutRateVisitorAfter (line 80) | matrixRejectPushKeyForUnifiedPushTopicWithoutRateVisitorAfter = 12 * tim...
  type errMatrixPushkeyRejected (line 87) | type errMatrixPushkeyRejected struct
    method Error (line 92) | func (e errMatrixPushkeyRejected) Error() string {
  function newRequestFromMatrixJSON (line 108) | func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLi...
  function writeMatrixDiscoveryResponse (line 146) | func writeMatrixDiscoveryResponse(w http.ResponseWriter) error {
  function writeMatrixSuccess (line 153) | func writeMatrixSuccess(w http.ResponseWriter) error {
  function writeMatrixResponse (line 159) | func writeMatrixResponse(w http.ResponseWriter, rejectedPushKey string) ...

FILE: server/server_matrix_test.go
  function TestMatrix_NewRequestFromMatrixJSON_Success (line 12) | func TestMatrix_NewRequestFromMatrixJSON_Success(t *testing.T) {
  function TestMatrix_NewRequestFromMatrixJSON_TooLarge (line 24) | func TestMatrix_NewRequestFromMatrixJSON_TooLarge(t *testing.T) {
  function TestMatrix_NewRequestFromMatrixJSON_InvalidJSON (line 33) | func TestMatrix_NewRequestFromMatrixJSON_InvalidJSON(t *testing.T) {
  function TestMatrix_NewRequestFromMatrixJSON_NotAMatrixMessage (line 42) | func TestMatrix_NewRequestFromMatrixJSON_NotAMatrixMessage(t *testing.T) {
  function TestMatrix_NewRequestFromMatrixJSON_MismatchingPushKey (line 51) | func TestMatrix_NewRequestFromMatrixJSON_MismatchingPushKey(t *testing.T) {
  function TestMatrix_WriteMatrixDiscoveryResponse (line 63) | func TestMatrix_WriteMatrixDiscoveryResponse(t *testing.T) {
  function TestMatrix_WriteMatrixError (line 70) | func TestMatrix_WriteMatrixError(t *testing.T) {
  function TestMatrix_WriteMatrixSuccess (line 77) | func TestMatrix_WriteMatrixSuccess(t *testing.T) {

FILE: server/server_metrics.go
  function initMetrics (line 31) | func initMetrics() {
  function minc (line 121) | func minc(counter prometheus.Counter) {
  function mset (line 128) | func mset[T int | int64 | float64](gauge prometheus.Gauge, value T) {

FILE: server/server_middleware.go
  type contextKey (line 9) | type contextKey
  constant contextRateVisitor (line 12) | contextRateVisitor contextKey = iota + 2586
  constant contextTopic (line 13) | contextTopic
  constant contextMatrixPushKey (line 14) | contextMatrixPushKey
  method limitRequests (line 17) | func (s *Server) limitRequests(next handleFunc) handleFunc {
  method limitRequestsWithTopic (line 29) | func (s *Server) limitRequestsWithTopic(next handleFunc) handleFunc {
  method ensureWebEnabled (line 52) | func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
  method ensureWebPushEnabled (line 61) | func (s *Server) ensureWebPushEnabled(next handleFunc) handleFunc {
  method ensureUserManager (line 70) | func (s *Server) ensureUserManager(next handleFunc) handleFunc {
  method ensureUser (line 79) | func (s *Server) ensureUser(next handleFunc) handleFunc {
  method ensureAdmin (line 88) | func (s *Server) ensureAdmin(next handleFunc) handleFunc {
  method ensureCallsEnabled (line 97) | func (s *Server) ensureCallsEnabled(next handleFunc) handleFunc {
  method ensurePaymentsEnabled (line 106) | func (s *Server) ensurePaymentsEnabled(next handleFunc) handleFunc {
  method ensureStripeCustomer (line 115) | func (s *Server) ensureStripeCustomer(next handleFunc) handleFunc {
  method withAccountSync (line 124) | func (s *Server) withAccountSync(next handleFunc) handleFunc {

FILE: server/server_payments.go
  method handleBillingTiersGet (line 60) | func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Re...
  method handleAccountBillingSubscriptionCreate (line 116) | func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseW...
  method handleAccountBillingSubscriptionCreateSuccess (line 185) | func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.Re...
  method handleAccountBillingSubscriptionUpdate (line 246) | func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseW...
  method handleAccountBillingSubscriptionDelete (line 303) | func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseW...
  method handleAccountBillingPortalSessionCreate (line 320) | func (s *Server) handleAccountBillingPortalSessionCreate(w http.Response...
  method handleAccountBillingWebhook (line 343) | func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *h...
  method handleAccountBillingWebhookSubscriptionUpdated (line 374) | func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(r *http....
  method handleAccountBillingWebhookSubscriptionDeleted (line 418) | func (s *Server) handleAccountBillingWebhookSubscriptionDeleted(r *http....
  method updateSubscriptionAndTier (line 441) | func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, ...
  method fetchStripePrices (line 483) | func (s *Server) fetchStripePrices() (map[string]int64, error) {
  type stripeAPI (line 499) | type stripeAPI interface
  type realStripeAPI (line 513) | type realStripeAPI struct
    method NewCheckoutSession (line 521) | func (s *realStripeAPI) NewCheckoutSession(params *stripe.CheckoutSess...
    method NewPortalSession (line 525) | func (s *realStripeAPI) NewPortalSession(params *stripe.BillingPortalS...
    method ListPrices (line 529) | func (s *realStripeAPI) ListPrices(params *stripe.PriceListParams) ([]...
    method GetCustomer (line 541) | func (s *realStripeAPI) GetCustomer(id string) (*stripe.Customer, erro...
    method GetSession (line 545) | func (s *realStripeAPI) GetSession(id string) (*stripe.CheckoutSession...
    method GetSubscription (line 549) | func (s *realStripeAPI) GetSubscription(id string) (*stripe.Subscripti...
    method UpdateCustomer (line 553) | func (s *realStripeAPI) UpdateCustomer(id string, params *stripe.Custo...
    method UpdateSubscription (line 557) | func (s *realStripeAPI) UpdateSubscription(id string, params *stripe.S...
    method CancelSubscription (line 561) | func (s *realStripeAPI) CancelSubscription(id string) (*stripe.Subscri...
    method ConstructWebhookEvent (line 565) | func (s *realStripeAPI) ConstructWebhookEvent(payload []byte, header s...
  function newStripeAPI (line 517) | func newStripeAPI() stripeAPI {

FILE: server/server_payments_dummy.go
  type stripeAPI (line 9) | type stripeAPI interface
  function newStripeAPI (line 13) | func newStripeAPI() stripeAPI {
  method fetchStripePrices (line 17) | func (s *Server) fetchStripePrices() (map[string]int64, error) {
  method handleBillingTiersGet (line 21) | func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Re...
  method handleAccountBillingSubscriptionCreate (line 25) | func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseW...
  method handleAccountBillingSubscriptionCreateSuccess (line 29) | func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.Re...
  method handleAccountBillingSubscriptionUpdate (line 33) | func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseW...
  method handleAccountBillingSubscriptionDelete (line 37) | func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseW...
  method handleAccountBillingPortalSessionCreate (line 41) | func (s *Server) handleAccountBillingPortalSessionCreate(w http.Response...
  method handleAccountBillingWebhook (line 45) | func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *h...

FILE: server/server_payments_test.go
  function TestPayments_Tiers (line 24) | func TestPayments_Tiers(t *testing.T) {
  function TestPayments_SubscriptionCreate_NotAStripeCustomer_Success (line 136) | func TestPayments_SubscriptionCreate_NotAStripeCustomer_Success(t *testi...
  function TestPayments_SubscriptionCreate_StripeCustomer_Success (line 171) | func TestPayments_SubscriptionCreate_StripeCustomer_Success(t *testing.T) {
  function TestPayments_AccountDelete_Cancels_Subscription (line 217) | func TestPayments_AccountDelete_Cancels_Subscription(t *testing.T) {
  function TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor (line 264) | func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visito...
  function TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active (line 431) | func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To...
  function TestPayments_Webhook_Subscription_Deleted (line 562) | func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
  function TestPayments_Subscription_Update_Different_Tier (line 629) | func TestPayments_Subscription_Update_Different_Tier(t *testing.T) {
  function TestPayments_Subscription_Delete_At_Period_End (line 695) | func TestPayments_Subscription_Delete_At_Period_End(t *testing.T) {
  function TestPayments_CreatePortalSession (line 728) | func TestPayments_CreatePortalSession(t *testing.T) {
  type testStripeAPI (line 766) | type testStripeAPI struct
    method NewCheckoutSession (line 772) | func (s *testStripeAPI) NewCheckoutSession(params *stripe.CheckoutSess...
    method NewPortalSession (line 777) | func (s *testStripeAPI) NewPortalSession(params *stripe.BillingPortalS...
    method ListPrices (line 782) | func (s *testStripeAPI) ListPrices(params *stripe.PriceListParams) ([]...
    method GetCustomer (line 787) | func (s *testStripeAPI) GetCustomer(id string) (*stripe.Customer, erro...
    method GetSession (line 792) | func (s *testStripeAPI) GetSession(id string) (*stripe.CheckoutSession...
    method GetSubscription (line 797) | func (s *testStripeAPI) GetSubscription(id string) (*stripe.Subscripti...
    method UpdateCustomer (line 802) | func (s *testStripeAPI) UpdateCustomer(id string, params *stripe.Custo...
    method UpdateSubscription (line 807) | func (s *testStripeAPI) UpdateSubscription(id string, params *stripe.S...
    method CancelSubscription (line 812) | func (s *testStripeAPI) CancelSubscription(id string) (*stripe.Subscri...
    method ConstructWebhookEvent (line 817) | func (s *testStripeAPI) ConstructWebhookEvent(payload []byte, header s...
  function jsonToStripeEvent (line 822) | func jsonToStripeEvent(t *testing.T, v string) stripe.Event {
  constant subscriptionUpdatedEventJSON (line 830) | subscriptionUpdatedEventJSON = `
  constant subscriptionDeletedEventJSON (line 856) | subscriptionDeletedEventJSON = `

FILE: server/server_race_off_test.go
  constant raceEnabled (line 5) | raceEnabled = false

FILE: server/server_race_on_test.go
  constant raceEnabled (line 5) | raceEnabled = true

FILE: server/server_test.go
  function TestMain (line 35) | func TestMain(m *testing.M) {
  function TestServer_PublishAndPoll (line 40) | func TestServer_PublishAndPoll(t *testing.T) {
  function TestServer_PublishWithFirebase (line 76) | func TestServer_PublishWithFirebase(t *testing.T) {
  function TestServer_PublishWithoutFirebase (line 95) | func TestServer_PublishWithoutFirebase(t *testing.T) {
  function TestServer_PublishWithFirebase_WithoutUsers_AndWithoutPanic (line 113) | func TestServer_PublishWithFirebase_WithoutUsers_AndWithoutPanic(t *test...
  function TestServer_SubscribeOpenAndKeepalive (line 141) | func TestServer_SubscribeOpenAndKeepalive(t *testing.T) {
  function TestServer_PublishAndSubscribe (line 182) | func TestServer_PublishAndSubscribe(t *testing.T) {
  function TestServer_Publish_Disallowed_Topic (line 224) | func TestServer_Publish_Disallowed_Topic(t *testing.T) {
  function TestServer_StaticSites (line 239) | func TestServer_StaticSites(t *testing.T) {
  function TestServer_WebEnabled (line 267) | func TestServer_WebEnabled(t *testing.T) {
  function TestServer_PublishLargeMessage (line 305) | func TestServer_PublishLargeMessage(t *testing.T) {
  function TestServer_PublishPriority (line 317) | func TestServer_PublishPriority(t *testing.T) {
  function TestServer_PublishPriority_SpecialHTTPHeader (line 350) | func TestServer_PublishPriority_SpecialHTTPHeader(t *testing.T) {
  function TestServer_PublishGETOnlyOneTopic (line 373) | func TestServer_PublishGETOnlyOneTopic(t *testing.T) {
  function TestServer_PublishNoCache (line 383) | func TestServer_PublishNoCache(t *testing.T) {
  function TestServer_PublishAt (line 401) | func TestServer_PublishAt(t *testing.T) {
  function TestServer_PublishAt_FromUser (line 437) | func TestServer_PublishAt_FromUser(t *testing.T) {
  function TestServer_PublishAt_Expires (line 476) | func TestServer_PublishAt_Expires(t *testing.T) {
  function TestServer_PublishAtWithCacheError (line 490) | func TestServer_PublishAtWithCacheError(t *testing.T) {
  function TestServer_PublishAtTooShortDelay (line 503) | func TestServer_PublishAtTooShortDelay(t *testing.T) {
  function TestServer_PublishAtTooLongDelay (line 514) | func TestServer_PublishAtTooLongDelay(t *testing.T) {
  function TestServer_PublishAtInvalidDelay (line 524) | func TestServer_PublishAtInvalidDelay(t *testing.T) {
  function TestServer_PublishAtTooLarge (line 534) | func TestServer_PublishAtTooLarge(t *testing.T) {
  function TestServer_PublishAtAndPrune (line 544) | func TestServer_PublishAtAndPrune(t *testing.T) {
  function TestServer_PublishAndMultiPoll (line 563) | func TestServer_PublishAndMultiPoll(t *testing.T) {
  function TestServer_PublishWithNopCache (line 595) | func TestServer_PublishWithNopCache(t *testing.T) {
  function TestServer_PublishAndPollSince (line 620) | func TestServer_PublishAndPollSince(t *testing.T) {
  function newMessageWithTimestamp (line 656) | func newMessageWithTimestamp(topic, msg string, timestamp int64) *model....
  function TestServer_PollSinceID_MultipleTopics (line 662) | func TestServer_PollSinceID_MultipleTopics(t *testing.T) {
  function TestServer_PollSinceID_MultipleTopics_IDDoesNotMatch (line 688) | func TestServer_PollSinceID_MultipleTopics_IDDoesNotMatch(t *testing.T) {
  function TestServer_PublishViaGET (line 707) | func TestServer_PublishViaGET(t *testing.T) {
  function TestServer_PublishMessageInHeaderWithNewlines (line 727) | func TestServer_PublishMessageInHeaderWithNewlines(t *testing.T) {
  function TestServer_PublishInvalidTopic (line 740) | func TestServer_PublishInvalidTopic(t *testing.T) {
  function TestServer_PublishWithSIDInPath (line 749) | func TestServer_PublishWithSIDInPath(t *testing.T) {
  function TestServer_PublishWithSIDInHeader (line 760) | func TestServer_PublishWithSIDInHeader(t *testing.T) {
  function TestServer_PublishWithSIDInPathAndHeader (line 773) | func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) {
  function TestServer_PublishWithSIDInQuery (line 786) | func TestServer_PublishWithSIDInQuery(t *testing.T) {
  function TestServer_PublishWithSIDViaGet (line 797) | func TestServer_PublishWithSIDViaGet(t *testing.T) {
  function TestServer_PublishAsJSON_WithSequenceID (line 808) | func TestServer_PublishAsJSON_WithSequenceID(t *testing.T) {
  function TestServer_PublishWithInvalidSIDInPath (line 822) | func TestServer_PublishWithInvalidSIDInPath(t *testing.T) {
  function TestServer_PublishWithInvalidSIDInHeader (line 832) | func TestServer_PublishWithInvalidSIDInHeader(t *testing.T) {
  function TestServer_PollWithQueryFilters (line 845) | func TestServer_PollWithQueryFilters(t *testing.T) {
  function TestServer_SubscribeWithQueryFilters (line 916) | func TestServer_SubscribeWithQueryFilters(t *testing.T) {
  function TestServer_Auth_Success_Admin (line 945) | func TestServer_Auth_Success_Admin(t *testing.T) {
  function TestServer_Auth_Success_User (line 960) | func TestServer_Auth_Success_User(t *testing.T) {
  function TestServer_Auth_Success_User_MultipleTopics (line 976) | func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
  function TestServer_Auth_Fail_InvalidPass (line 998) | func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
  function TestServer_Auth_Fail_Unauthorized (line 1013) | func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
  function TestServer_Auth_Fail_CannotPublish (line 1029) | func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
  function TestServer_Auth_Fail_Rate_Limiting (line 1061) | func TestServer_Auth_Fail_Rate_Limiting(t *testing.T) {
  function TestServer_Auth_ViaQuery (line 1082) | func TestServer_Auth_ViaQuery(t *testing.T) {
  function TestServer_Auth_NonBasicHeader (line 1100) | func TestServer_Auth_NonBasicHeader(t *testing.T) {
  function TestServer_StatsResetter (line 1121) | func TestServer_StatsResetter(t *testing.T) {
  function TestServer_StatsResetter_MessageLimiter_EmailsLimiter (line 1227) | func TestServer_StatsResetter_MessageLimiter_EmailsLimiter(t *testing.T) {
  function TestServer_DailyMessageQuotaFromDatabase (line 1274) | func TestServer_DailyMessageQuotaFromDatabase(t *testing.T) {
  type testMailer (line 1317) | type testMailer struct
    method Send (line 1322) | func (t *testMailer) Send(v *visitor, m *model.Message, to string) err...
    method Counts (line 1329) | func (t *testMailer) Counts() (total int64, success int64, failure int...
    method Count (line 1333) | func (t *testMailer) Count() int {
  function TestServer_PublishTooManyRequests_Defaults (line 1339) | func TestServer_PublishTooManyRequests_Defaults(t *testing.T) {
  function TestServer_PublishTooManyRequests_Defaults_IPv6 (line 1351) | func TestServer_PublishTooManyRequests_Defaults_IPv6(t *testing.T) {
  function TestServer_PublishTooManyRequests_IPv6_Slash48 (line 1373) | func TestServer_PublishTooManyRequests_IPv6_Slash48(t *testing.T) {
  function TestServer_PublishTooManyRequests_Defaults_ExemptHosts (line 1398) | func TestServer_PublishTooManyRequests_Defaults_ExemptHosts(t *testing.T) {
  function TestServer_PublishTooManyRequests_Defaults_ExemptHosts_IPv6 (line 1411) | func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_IPv6(t *test...
  function TestServer_PublishTooManyRequests_Defaults_ExemptHosts_MessageDailyLimit (line 1427) | func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_MessageDaily...
  function TestServer_PublishTooManyRequests_ShortReplenish (line 1441) | func TestServer_PublishTooManyRequests_ShortReplenish(t *testing.T) {
  function TestServer_PublishTooManyEmails_Defaults (line 1461) | func TestServer_PublishTooManyEmails_Defaults(t *testing.T) {
  function TestServer_PublishTooManyEmails_Replenish (line 1478) | func TestServer_PublishTooManyEmails_Replenish(t *testing.T) {
  function TestServer_PublishDelayedEmail_Fail (line 1509) | func TestServer_PublishDelayedEmail_Fail(t *testing.T) {
  function TestServer_PublishDelayedCall_Fail (line 1521) | func TestServer_PublishDelayedCall_Fail(t *testing.T) {
  function TestServer_PublishEmailNoMailer_Fail (line 1536) | func TestServer_PublishEmailNoMailer_Fail(t *testing.T) {
  function TestServer_PublishEmailAddressInvalid (line 1546) | func TestServer_PublishEmailAddressInvalid(t *testing.T) {
  function TestServer_PublishAndExpungeTopicAfter16Hours (line 1570) | func TestServer_PublishAndExpungeTopicAfter16Hours(t *testing.T) {
  function TestServer_TopicKeepaliveOnPoll (line 1621) | func TestServer_TopicKeepaliveOnPoll(t *testing.T) {
  function TestServer_UnifiedPushDiscovery (line 1641) | func TestServer_UnifiedPushDiscovery(t *testing.T) {
  function TestServer_PublishUnifiedPushBinary_AndPoll (line 1650) | func TestServer_PublishUnifiedPushBinary_AndPoll(t *testing.T) {
  function TestServer_PublishUnifiedPushBinary_Truncated (line 1683) | func TestServer_PublishUnifiedPushBinary_Truncated(t *testing.T) {
  function TestServer_PublishUnifiedPushText (line 1708) | func TestServer_PublishUnifiedPushText(t *testing.T) {
  function TestServer_MatrixGateway_Discovery_Success (line 1726) | func TestServer_MatrixGateway_Discovery_Success(t *testing.T) {
  function TestServer_MatrixGateway_Discovery_Failure_Unconfigured (line 1735) | func TestServer_MatrixGateway_Discovery_Failure_Unconfigured(t *testing....
  function TestServer_MatrixGateway_Push_Success (line 1747) | func TestServer_MatrixGateway_Push_Success(t *testing.T) {
  function TestServer_MatrixGateway_Push_Failure_NoSubscriber (line 1766) | func TestServer_MatrixGateway_Push_Failure_NoSubscriber(t *testing.T) {
  function TestServer_MatrixGateway_Push_Failure_NoSubscriber_After13Hours (line 1778) | func TestServer_MatrixGateway_Push_Failure_NoSubscriber_After13Hours(t *...
  function TestServer_MatrixGateway_Push_Failure_InvalidPushkey (line 1806) | func TestServer_MatrixGateway_Push_Failure_InvalidPushkey(t *testing.T) {
  function TestServer_MatrixGateway_Push_Failure_EverythingIsWrong (line 1820) | func TestServer_MatrixGateway_Push_Failure_EverythingIsWrong(t *testing....
  function TestServer_MatrixGateway_Push_Failure_Unconfigured (line 1835) | func TestServer_MatrixGateway_Push_Failure_Unconfigured(t *testing.T) {
  function TestServer_PublishActions_AndPoll (line 1847) | func TestServer_PublishActions_AndPoll(t *testing.T) {
  function TestServer_PublishMarkdown (line 1869) | func TestServer_PublishMarkdown(t *testing.T) {
  function TestServer_PublishMarkdown_QueryParam (line 1883) | func TestServer_PublishMarkdown_QueryParam(t *testing.T) {
  function TestServer_PublishMarkdown_NotMarkdown (line 1895) | func TestServer_PublishMarkdown_NotMarkdown(t *testing.T) {
  function TestServer_PublishAsJSON (line 1908) | func TestServer_PublishAsJSON(t *testing.T) {
  function TestServer_PublishAsJSON_Markdown (line 1934) | func TestServer_PublishAsJSON_Markdown(t *testing.T) {
  function TestServer_PublishAsJSON_RateLimit_MessageDailyLimit (line 1948) | func TestServer_PublishAsJSON_RateLimit_MessageDailyLimit(t *testing.T) {
  function TestServer_PublishAsJSON_WithEmail (line 1966) | func TestServer_PublishAsJSON_WithEmail(t *testing.T) {
  function TestServer_PublishAsJSON_WithActions (line 1984) | func TestServer_PublishAsJSON_WithActions(t *testing.T) {
  function TestServer_PublishAsJSON_NoCache (line 2021) | func TestServer_PublishAsJSON_NoCache(t *testing.T) {
  function TestServer_PublishAsJSON_WithoutFirebase (line 2037) | func TestServer_PublishAsJSON_WithoutFirebase(t *testing.T) {
  function TestServer_PublishAsJSON_Invalid (line 2054) | func TestServer_PublishAsJSON_Invalid(t *testing.T) {
  function TestServer_PublishWithTierBasedMessageLimitAndExpiry (line 2063) | func TestServer_PublishWithTierBasedMessageLimitAndExpiry(t *testing.T) {
  function TestServer_PublishAttachment (line 2101) | func TestServer_PublishAttachment(t *testing.T) {
  function TestServer_PublishAttachmentShortWithFilename (line 2135) | func TestServer_PublishAttachmentShortWithFilename(t *testing.T) {
  function TestServer_PublishAttachmentExternalWithoutFilename (line 2166) | func TestServer_PublishAttachmentExternalWithoutFilename(t *testing.T) {
  function TestServer_PublishAttachmentExternalWithFilename (line 2188) | func TestServer_PublishAttachmentExternalWithFilename(t *testing.T) {
  function TestServer_PublishAttachmentBadURL (line 2206) | func TestServer_PublishAttachmentBadURL(t *testing.T) {
  function TestServer_PublishAttachmentTooLargeContentLength (line 2217) | func TestServer_PublishAttachmentTooLargeContentLength(t *testing.T) {
  function TestServer_PublishAttachmentTooLargeBodyAttachmentFileSizeLimit (line 2231) | func TestServer_PublishAttachmentTooLargeBodyAttachmentFileSizeLimit(t *...
  function TestServer_PublishAttachmentExpiryBeforeDelivery (line 2245) | func TestServer_PublishAttachmentExpiryBeforeDelivery(t *testing.T) {
  function TestServer_PublishAttachmentTooLargeBodyVisitorAttachmentTotalSizeLimit (line 2260) | func TestServer_PublishAttachmentTooLargeBodyVisitorAttachmentTotalSizeL...
  function TestServer_PublishAttachmentAndExpire (line 2281) | func TestServer_PublishAttachmentAndExpire(t *testing.T) {
  function TestServer_PublishAttachmentWithTierBasedExpiry (line 2312) | func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) {
  function TestServer_PublishAttachmentWithTierBasedBandwidthLimit (line 2361) | func TestServer_PublishAttachmentWithTierBasedBandwidthLimit(t *testing....
  function TestServer_PublishAttachmentWithTierBasedLimits (line 2400) | func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) {
  function TestServer_PublishAttachmentBandwidthLimit (line 2458) | func TestServer_PublishAttachmentBandwidthLimit(t *testing.T) {
  function TestServer_PublishAttachmentBandwidthLimitUploadOnly (line 2487) | func TestServer_PublishAttachmentBandwidthLimitUploadOnly(t *testing.T) {
  function TestServer_PublishAttachmentAndImmediatelyGetItWithCacheTimeout (line 2510) | func TestServer_PublishAttachmentAndImmediatelyGetItWithCacheTimeout(t *...
  function TestServer_PublishAttachmentAccountStats (line 2531) | func TestServer_PublishAttachmentAccountStats(t *testing.T) {
  function TestServer_Visitor_XForwardedFor_None (line 2558) | func TestServer_Visitor_XForwardedFor_None(t *testing.T) {
  function TestServer_Visitor_XForwardedFor_Single (line 2572) | func TestServer_Visitor_XForwardedFor_Single(t *testing.T) {
  function TestServer_Visitor_XForwardedFor_Multiple (line 2586) | func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) {
  function TestServer_Visitor_Custom_ClientIP_Header (line 2600) | func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) {
  function TestServer_Visitor_Custom_ClientIP_Header_IPv6 (line 2615) | func TestServer_Visitor_Custom_ClientIP_Header_IPv6(t *testing.T) {
  function TestServer_Visitor_Custom_Forwarded_Header (line 2630) | func TestServer_Visitor_Custom_Forwarded_Header(t *testing.T) {
  function TestServer_Visitor_Custom_Forwarded_Header_IPv6 (line 2646) | func TestServer_Visitor_Custom_Forwarded_Header_IPv6(t *testing.T) {
  function TestServer_PublishWhileUpdatingStatsWithLotsOfMessages (line 2662) | func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
  function TestServer_AnonymousUser_And_NonTierUser_Are_Same_Visitor (line 2718) | func TestServer_AnonymousUser_And_NonTierUser_Are_Same_Visitor(t *testin...
  function TestServer_SubscriberRateLimiting_Success (line 2751) | func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
  function TestServer_SubscriberRateLimiting_NotWrongTopic (line 2807) | func TestServer_SubscriberRateLimiting_NotWrongTopic(t *testing.T) {
  function TestServer_SubscriberRateLimiting_NotEnabled_Failed (line 2825) | func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
  function TestServer_SubscriberRateLimiting_UP_Only (line 2862) | func TestServer_SubscriberRateLimiting_UP_Only(t *testing.T) {
  function TestServer_Matrix_SubscriberRateLimiting_UP_Only (line 2888) | func TestServer_Matrix_SubscriberRateLimiting_UP_Only(t *testing.T) {
  function TestServer_SubscriberRateLimiting_VisitorExpiration (line 2918) | func TestServer_SubscriberRateLimiting_VisitorExpiration(t *testing.T) {
  function TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite (line 2954) | func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWr...
  function TestServer_MessageHistoryAndStatsEndpoint (line 2976) | func TestServer_MessageHistoryAndStatsEndpoint(t *testing.T) {
  function TestServer_MessageHistoryMaxSize (line 3024) | func TestServer_MessageHistoryMaxSize(t *testing.T) {
  function TestServer_MessageCountPersistence (line 3035) | func TestServer_MessageCountPersistence(t *testing.T) {
  function TestServer_PublishWithUTF8MimeHeader (line 3053) | func TestServer_PublishWithUTF8MimeHeader(t *testing.T) {
  function TestServer_UpstreamBaseURL_Success (line 3081) | func TestServer_UpstreamBaseURL_Success(t *testing.T) {
  function TestServer_UpstreamBaseURL_With_Access_Token_Success (line 3113) | func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) {
  function TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush (line 3147) | func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) {
  function TestServer_MessageTemplate (line 3173) | func TestServer_MessageTemplate(t *testing.T) {
  function TestServer_MessageTemplate_RepeatPlaceholder (line 3190) | func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) {
  function TestServer_MessageTemplate_JSONBody (line 3207) | func TestServer_MessageTemplate_JSONBody(t *testing.T) {
  function TestServer_MessageTemplate_MalformedJSONBody (line 3225) | func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) {
  function TestServer_MessageTemplate_PlaceholderTypo (line 3241) | func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) {
  function TestServer_MessageTemplate_MultiplePlaceholders (line 3258) | func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) {
  function TestServer_MessageTemplate_Range (line 3273) | func TestServer_MessageTemplate_Range(t *testing.T) {
  function TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK (line 3289) | func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *...
  function TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong (line 3308) | func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLon...
  function TestServer_MessageTemplate_Grafana (line 3324) | func TestServer_MessageTemplate_Grafana(t *testing.T) {
  function TestServer_MessageTemplate_GitHub (line 3348) | func TestServer_MessageTemplate_GitHub(t *testing.T) {
  function TestServer_MessageTemplate_GitHub2 (line 3361) | func TestServer_MessageTemplate_GitHub2(t *testing.T) {
  function TestServer_MessageTemplate_DisallowedCalls (line 3374) | func TestServer_MessageTemplate_DisallowedCalls(t *testing.T) {
  function TestServer_MessageTemplate_SprigFunctions (line 3404) | func TestServer_MessageTemplate_SprigFunctions(t *testing.T) {
  function TestServer_MessageTemplate_UnsafeSprigFunctions (line 3439) | func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) {
  function TestServer_MessageTemplate_InlineNewlines (line 3453) | func TestServer_MessageTemplate_InlineNewlines(t *testing.T) {
  function TestServer_MessageTemplate_InlineNewlinesOutsideOfTemplate (line 3472) | func TestServer_MessageTemplate_InlineNewlinesOutsideOfTemplate(t *testi...
  function TestServer_MessageTemplate_TemplateFileNewlines (line 3491) | func TestServer_MessageTemplate_TemplateFileNewlines(t *testing.T) {
  function TestServer_MessageTemplate_FromNamedTemplate_GitHubCommentCreated (line 3522) | func TestServer_MessageTemplate_FromNamedTemplate_GitHubCommentCreated(t...
  function TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened (line 3543) | func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened(t *t...
  function TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened_OverrideConfigTemplate (line 3573) | func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened_Over...
  function TestServer_MessageTemplate_Repeat9999_TooLarge (line 3594) | func TestServer_MessageTemplate_Repeat9999_TooLarge(t *testing.T) {
  function TestServer_MessageTemplate_Repeat10001_TooLarge (line 3608) | func TestServer_MessageTemplate_Repeat10001_TooLarge(t *testing.T) {
  function TestServer_MessageTemplate_Until100_000 (line 3622) | func TestServer_MessageTemplate_Until100_000(t *testing.T) {
  function TestServer_MessageTemplate_Priority (line 3636) | func TestServer_MessageTemplate_Priority(t *testing.T) {
  function TestServer_MessageTemplate_Priority_Conditional (line 3653) | func TestServer_MessageTemplate_Priority_Conditional(t *testing.T) {
  function TestServer_MessageTemplate_Priority_NamedValue (line 3682) | func TestServer_MessageTemplate_Priority_NamedValue(t *testing.T) {
  function TestServer_MessageTemplate_Priority_Invalid (line 3698) | func TestServer_MessageTemplate_Priority_Invalid(t *testing.T) {
  function TestServer_MessageTemplate_Priority_QueryParam (line 3713) | func TestServer_MessageTemplate_Priority_QueryParam(t *testing.T) {
  function TestServer_MessageTemplate_Priority_FromTemplateFile (line 3725) | func TestServer_MessageTemplate_Priority_FromTemplateFile(t *testing.T) {
  function TestServer_DeleteMessage (line 3759) | func TestServer_DeleteMessage(t *testing.T) {
  function TestServer_ClearMessage (line 3793) | func TestServer_ClearMessage(t *testing.T) {
  function TestServer_ClearMessage_ReadEndpoint (line 3827) | func TestServer_ClearMessage_ReadEndpoint(t *testing.T) {
  function TestServer_UpdateMessage (line 3846) | func TestServer_UpdateMessage(t *testing.T) {
  function TestServer_UpdateMessage_UsingMessageID (line 3881) | func TestServer_UpdateMessage_UsingMessageID(t *testing.T) {
  function TestServer_DeleteAndClear_InvalidSequenceID (line 3917) | func TestServer_DeleteAndClear_InvalidSequenceID(t *testing.T) {
  function TestServer_DeleteMessage_WithFirebase (line 3932) | func TestServer_DeleteMessage_WithFirebase(t *testing.T) {
  function TestServer_ClearMessage_WithFirebase (line 3957) | func TestServer_ClearMessage_WithFirebase(t *testing.T) {
  function TestServer_UpdateScheduledMessage (line 3981) | func TestServer_UpdateScheduledMessage(t *testing.T) {
  function TestServer_DeleteScheduledMessage (line 4018) | func TestServer_DeleteScheduledMessage(t *testing.T) {
  function TestServer_UpdateScheduledMessage_TopicScoped (line 4053) | func TestServer_UpdateScheduledMessage_TopicScoped(t *testing.T) {
  function TestServer_UpdateScheduledMessage_WithAttachment (line 4085) | func TestServer_UpdateScheduledMessage_WithAttachment(t *testing.T) {
  function TestServer_DeleteScheduledMessage_WithAttachment (line 4119) | func TestServer_DeleteScheduledMessage_WithAttachment(t *testing.T) {
  function newMemTestCache (line 4147) | func newMemTestCache(t *testing.T) *message.Cache {
  function forEachBackend (line 4153) | func forEachBackend(t *testing.T, f func(t *testing.T, databaseURL strin...
  function newTestConfig (line 4162) | func newTestConfig(t *testing.T, databaseURL string) *Config {
  function configureAuth (line 4176) | func configureAuth(t *testing.T, conf *Config) *Config {
  function newTestConfigWithAuthFile (line 4185) | func newTestConfigWithAuthFile(t *testing.T, databaseURL string) *Config {
  function newTestServer (line 4191) | func newTestServer(t *testing.T, config *Config) *Server {
  function request (line 4198) | func request(t *testing.T, s *Server, method, url, body string, headers ...
  function subscribe (line 4215) | func subscribe(t *testing.T, s *Server, url string, rr *httptest.Respons...
  function toMessages (line 4235) | func toMessages(t *testing.T, s string) []*model.Message {
  function toMessage (line 4244) | func toMessage(t *testing.T, s string) *model.Message {
  function toHTTPError (line 4250) | func toHTTPError(t *testing.T, s string) *errHTTP {
  function readAll (line 4256) | func readAll(t *testing.T, rc io.ReadCloser) string {
  function waitFor (line 4264) | func waitFor(t *testing.T, f func() bool) {
  function waitForWithMaxWait (line 4268) | func waitForWithMaxWait(t *testing.T, maxWait time.Duration, f func() bo...
  type mockResponseWriter (line 4280) | type mockResponseWriter struct
    method Header (line 4293) | func (m *mockResponseWriter) Header() http.Header {
    method Write (line 4297) | func (m *mockResponseWriter) Write(b []byte) (int, error) {
    method WriteHeader (line 4302) | func (m *mockResponseWriter) WriteHeader(statusCode int) {
  function newMockResponseWriter (line 4287) | func newMockResponseWriter() *mockResponseWriter {
  type closableResponseWriter (line 4312) | type closableResponseWriter struct
    method Header (line 4325) | func (w *closableResponseWriter) Header() http.Header {
    method Write (line 4329) | func (w *closableResponseWriter) Write(b []byte) (int, error) {
    method WriteHeader (line 4339) | func (w *closableResponseWriter) WriteHeader(statusCode int) {}
    method Flush (line 4341) | func (w *closableResponseWriter) Flush() {
    method Close (line 4350) | func (w *closableResponseWriter) Close() {
  function newClosableResponseWriter (line 4319) | func newClosableResponseWriter() *closableResponseWriter {
  function TestServer_SubscribeHTTP_NoWriteAfterHandlerReturn (line 4356) | func TestServer_SubscribeHTTP_NoWriteAfterHandlerReturn(t *testing.T) {
  function TestServer_HandleError_SkipsWriteHeaderOnHijackedConnection (line 4437) | func TestServer_HandleError_SkipsWriteHeaderOnHijackedConnection(t *test...
  function TestServer_Publish_InvalidUTF8InBody (line 4469) | func TestServer_Publish_InvalidUTF8InBody(t *testing.T) {
  function TestServer_Publish_InvalidUTF8InTitle (line 4511) | func TestServer_Publish_InvalidUTF8InTitle(t *testing.T) {
  function TestServer_Publish_InvalidUTF8InTags (line 4522) | func TestServer_Publish_InvalidUTF8InTags(t *testing.T) {
  function TestServer_Publish_InvalidUTF8WithFirebase (line 4533) | func TestServer_Publish_InvalidUTF8WithFirebase(t *testing.T) {

FILE: server/server_twilio.go
  type twilioCallData (line 43) | type twilioCallData struct
  method convertPhoneNumber (line 55) | func (s *Server) convertPhoneNumber(u *user.User, phoneNumber string) (s...
  method callPhone (line 80) | func (s *Server) callPhone(v *visitor, r *http.Request, m *model.Message...
  method callPhoneInternal (line 123) | func (s *Server) callPhoneInternal(data url.Values) (string, error) {
  method verifyPhoneNumber (line 143) | func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNum...
  method verifyPhoneNumberCheck (line 169) | func (s *Server) verifyPhoneNumberCheck(v *visitor, r *http.Request, pho...
  function xmlEscapeText (line 211) | func xmlEscapeText(text string) string {

FILE: server/server_twilio_test.go
  function TestServer_Twilio_Call_Add_Verify_Call_Delete_Success (line 16) | func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
  function TestServer_Twilio_Call_Success (line 119) | func TestServer_Twilio_Call_Success(t *testing.T) {
  function TestServer_Twilio_Call_Success_With_Yes (line 166) | func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
  function TestServer_Twilio_Call_Success_with_custom_twiml (line 213) | func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
  function TestServer_Twilio_Call_UnverifiedNumber (line 276) | func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
  function TestServer_Twilio_Call_InvalidNumber (line 303) | func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
  function TestServer_Twilio_Call_Anonymous (line 319) | func TestServer_Twilio_Call_Anonymous(t *testing.T) {
  function TestServer_Twilio_Call_Unconfigured (line 335) | func TestServer_Twilio_Call_Unconfigured(t *testing.T) {

FILE: server/server_webpush.go
  constant WebPushAvailable (line 22) | WebPushAvailable = true
  constant webPushTopicSubscribeLimit (line 24) | webPushTopicSubscribeLimit = 50
  function init (line 40) | func init() {
  method handleWebPushUpdate (line 48) | func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Requ...
  method handleWebPushDelete (line 76) | func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Requ...
  method publishToWebPushEndpoints (line 87) | func (s *Server) publishToWebPushEndpoints(v *visitor, m *model.Message) {
  method pruneAndNotifyWebPushSubscriptions (line 106) | func (s *Server) pruneAndNotifyWebPushSubscriptions() {
  method pruneAndNotifyWebPushSubscriptionsInternal (line 117) | func (s *Server) pruneAndNotifyWebPushSubscriptionsInternal() error {
  method sendWebPushNotification (line 148) | func (s *Server) sendWebPushNotification(sub *wpush.Subscription, messag...

FILE: server/server_webpush_dummy.go
  constant WebPushAvailable (line 14) | WebPushAvailable = false
  method handleWebPushUpdate (line 17) | func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Requ...
  method handleWebPushDelete (line 21) | func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Requ...
  method publishToWebPushEndpoints (line 25) | func (s *Server) publishToWebPushEndpoints(v *visitor, m *model.Message) {
  method pruneAndNotifyWebPushSubscriptions (line 29) | func (s *Server) pruneAndNotifyWebPushSubscriptions() {

FILE: server/server_webpush_test.go
  constant testWebPushEndpoint (line 25) | testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v...
  function TestServer_WebPush_Enabled (line 28) | func TestServer_WebPush_Enabled(t *testing.T) {
  function TestServer_WebPush_Disabled (line 52) | func TestServer_WebPush_Disabled(t *testing.T) {
  function TestServer_WebPush_TopicAdd (line 61) | func TestServer_WebPush_TopicAdd(t *testing.T) {
  function TestServer_WebPush_TopicAdd_InvalidEndpoint (line 80) | func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) {
  function TestServer_WebPush_TopicAdd_TooManyTopics (line 90) | func TestServer_WebPush_TopicAdd_TooManyTopics(t *testing.T) {
  function TestServer_WebPush_TopicUnsubscribe (line 105) | func TestServer_WebPush_TopicUnsubscribe(t *testing.T) {
  function TestServer_WebPush_Delete (line 120) | func TestServer_WebPush_Delete(t *testing.T) {
  function TestServer_WebPush_TopicSubscribeProtected_Allowed (line 135) | func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) {
  function TestServer_WebPush_TopicSubscribeProtected_Denied (line 157) | func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) {
  function TestServer_WebPush_DeleteAccountUnsubscribe (line 170) | func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) {
  function TestServer_WebPush_Publish (line 195) | func TestServer_WebPush_Publish(t *testing.T) {
  function TestServer_WebPush_Publish_RemoveOnError (line 219) | func TestServer_WebPush_Publish_RemoveOnError(t *testing.T) {
  function TestServer_WebPush_Expiry (line 249) | func TestServer_WebPush_Expiry(t *testing.T) {
  function payloadForTopics (line 288) | func payloadForTopics(t *testing.T, topics []string, endpoint string) st...
  function addSubscription (line 300) | func addSubscription(t *testing.T, s *Server, endpoint string, topics .....
  function requireSubscriptionCount (line 304) | func requireSubscriptionCount(t *testing.T, s *Server, topic string, exp...
  function newTestConfigWithWebPush (line 310) | func newTestConfigWithWebPush(t *testing.T, databaseURL string) *Config {

FILE: server/smtp_sender.go
  type mailer (line 19) | type mailer interface
  type smtpSender (line 24) | type smtpSender struct
    method Send (line 31) | func (s *smtpSender) Send(v *visitor, m *model.Message, to string) err...
    method Counts (line 61) | func (s *smtpSender) Counts() (total int64, success int64, failure int...
    method withCount (line 67) | func (s *smtpSender) withCount(v *visitor, m *model.Message, fn func()...
  function formatMail (line 80) | func formatMail(baseURL, senderIP, from, to string, m *model.Message) (s...
  function toEmojis (line 143) | func toEmojis(tags []string) (emojisOut []string, tagsOut []string, err ...

FILE: server/smtp_sender_test.go
  function TestFormatMail_Basic (line 10) | func TestFormatMail_Basic(t *testing.T) {
  function TestFormatMail_JustEmojis (line 31) | func TestFormatMail_JustEmojis(t *testing.T) {
  function TestFormatMail_JustOtherTags (line 53) | func TestFormatMail_JustOtherTags(t *testing.T) {
  function TestFormatMail_JustPriority (line 77) | func TestFormatMail_JustPriority(t *testing.T) {
  function TestFormatMail_UTF8Subject (line 101) | func TestFormatMail_UTF8Subject(t *testing.T) {
  function TestFormatMail_WithAllTheThings (line 123) | func TestFormatMail_WithAllTheThings(t *testing.T) {

FILE: server/smtp_server.go
  constant maxMultipartDepth (line 41) | maxMultipartDepth = 2
  type smtpBackend (line 45) | type smtpBackend struct
    method NewSession (line 63) | func (b *smtpBackend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
    method Counts (line 68) | func (b *smtpBackend) Counts() (total int64, success int64, failure in...
  function newMailBackend (line 56) | func newMailBackend(conf *Config, handler func(http.ResponseWriter, *htt...
  type smtpSession (line 75) | type smtpSession struct
    method AuthPlain (line 84) | func (s *smtpSession) AuthPlain(username, password string) error {
    method Mail (line 92) | func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
    method Rcpt (line 97) | func (s *smtpSession) Rcpt(to string) error {
    method Data (line 138) | func (s *smtpSession) Data(r io.Reader) error {
    method publishMessage (line 188) | func (s *smtpSession) publishMessage(m *model.Message) error {
    method Reset (line 219) | func (s *smtpSession) Reset() {
    method Logout (line 225) | func (s *smtpSession) Logout() error {
    method withFailCount (line 232) | func (s *smtpSession) withFailCount(fn func() error) error {
  function readMailBody (line 246) | func readMailBody(body io.Reader, header mail.Header) (string, error) {
  function readMultipartMailBody (line 263) | func readMultipartMailBody(body io.Reader, params map[string]string) (st...
  function readMultipartMailBodyParts (line 275) | func readMultipartMailBodyParts(body io.Reader, params map[string]string...
  function readTextMailBody (line 305) | func readTextMailBody(reader io.Reader, contentType, transferEncoding st...
  function readPlainTextMailBody (line 314) | func readPlainTextMailBody(reader io.Reader, transferEncoding string) (s...
  function readHTMLMailBody (line 327) | func readHTMLMailBody(reader io.Reader, transferEncoding string) (string...
  function removeExtraEmptyLines (line 342) | func removeExtraEmptyLines(s string) string {

FILE: server/smtp_server_test.go
  function TestSmtpBackend_Multipart (line 15) | func TestSmtpBackend_Multipart(t *testing.T) {
  function TestSmtpBackend_MultipartNoBody (line 51) | func TestSmtpBackend_MultipartNoBody(t *testing.T) {
  function TestSmtpBackend_Plaintext (line 87) | func TestSmtpBackend_Plaintext(t *testing.T) {
  function TestSmtpBackend_Plaintext_No_ContentType (line 113) | func TestSmtpBackend_Plaintext_No_ContentType(t *testing.T) {
  function TestSmtpBackend_Plaintext_EncodedSubject (line 134) | func TestSmtpBackend_Plaintext_EncodedSubject(t *testing.T) {
  function TestSmtpBackend_Plaintext_TooLongTruncate (line 156) | func TestSmtpBackend_Plaintext_TooLongTruncate(t *testing.T) {
  function TestSmtpBackend_Plaintext_QuotedPrintable (line 306) | func TestSmtpBackend_Plaintext_QuotedPrintable(t *testing.T) {
  function TestSmtpBackend_Unsupported (line 339) | func TestSmtpBackend_Unsupported(t *testing.T) {
  function TestSmtpBackend_InvalidAddress (line 362) | func TestSmtpBackend_InvalidAddress(t *testing.T) {
  function TestSmtpBackend_Base64Body (line 384) | func TestSmtpBackend_Base64Body(t *testing.T) {
  function TestSmtpBackend_MultipartQuotedPrintable (line 426) | func TestSmtpBackend_MultipartQuotedPrintable(t *testing.T) {
  function TestSmtpBackend_NestedMultipartBase64 (line 469) | func TestSmtpBackend_NestedMultipartBase64(t *testing.T) {
  function TestSmtpBackend_NestedMultipartTooDeep (line 518) | func TestSmtpBackend_NestedMultipartTooDeep(t *testing.T) {
  function TestSmtpBackend_HTMLEmail (line 571) | func TestSmtpBackend_HTMLEmail(t *testing.T) {
  constant spamEmail (line 709) | spamEmail = `
  function TestSmtpBackend_Spam_Text (line 1276) | func TestSmtpBackend_Spam_Text(t *testing.T) {
  function TestSmtpBackend_Spam_HTML (line 1290) | func TestSmtpBackend_Spam_HTML(t *testing.T) {
  function TestSmtpBackend_HTMLOnly_FromDiskStation (line 1339) | func TestSmtpBackend_HTMLOnly_FromDiskStation(t *testing.T) {
  function TestSmtpBackend_HTMLEmail_BrTagsPreserved (line 1371) | func TestSmtpBackend_HTMLEmail_BrTagsPreserved(t *testing.T) {
  function TestSmtpBackend_PlaintextWithToken (line 1401) | func TestSmtpBackend_PlaintextWithToken(t *testing.T) {
  function TestSmtpBackend_PlaintextWithPlainAuth (line 1422) | func TestSmtpBackend_PlaintextWithPlainAuth(t *testing.T) {
  type smtpHandlerFunc (line 1444) | type smtpHandlerFunc
  function newTestSMTPServer (line 1446) | func newTestSMTPServer(t *testing.T, handler smtpHandlerFunc) (s *smtp.S...
  function writeAndReadUntilLine (line 1470) | func writeAndReadUntilLine(t *testing.T, email string, conn net.Conn, sc...
  function readUntilLine (line 1476) | func readUntilLine(t *testing.T, conn net.Conn, scanner *bufio.Scanner, ...

FILE: server/topic.go
  constant topicExpungeAfter (line 17) | topicExpungeAfter = 16 * time.Hour
  type topic (line 22) | type topic struct
    method Subscribe (line 49) | func (t *topic) Subscribe(s subscriber, userID string, cancel func()) ...
    method Stale (line 68) | func (t *topic) Stale() bool {
    method LastAccess (line 77) | func (t *topic) LastAccess() time.Time {
    method SetRateVisitor (line 83) | func (t *topic) SetRateVisitor(v *visitor) {
    method RateVisitor (line 90) | func (t *topic) RateVisitor() *visitor {
    method Unsubscribe (line 100) | func (t *topic) Unsubscribe(id int) {
    method Publish (line 107) | func (t *topic) Publish(v *visitor, m *model.Message) error {
    method Stats (line 132) | func (t *topic) Stats() (int, time.Time) {
    method Keepalive (line 139) | func (t *topic) Keepalive() {
    method CancelSubscribersExceptUser (line 146) | func (t *topic) CancelSubscribersExceptUser(exceptUserID string) {
    method CancelSubscriberUser (line 157) | func (t *topic) CancelSubscriberUser(userID string) {
    method cancelUserSubscriber (line 168) | func (t *topic) cancelUserSubscriber(s *topicSubscriber) {
    method Context (line 179) | func (t *topic) Context() log.Context {
    method subscribersCopy (line 196) | func (t *topic) subscribersCopy() map[int]*topicSubscriber {
  type topicSubscriber (line 30) | type topicSubscriber struct
  type subscriber (line 37) | type subscriber
  function newTopic (line 40) | func newTopic(id string) *topic {

FILE: server/topic_test.go
  function TestTopic_CancelSubscribersExceptUser (line 13) | func TestTopic_CancelSubscribersExceptUser(t *testing.T) {
  function TestTopic_CancelSubscribersUser (line 34) | func TestTopic_CancelSubscribersUser(t *testing.T) {
  function TestTopic_Keepalive (line 57) | func TestTopic_Keepalive(t *testing.T) {
  function TestTopic_Subscribe_DuplicateID (line 67) | func TestTopic_Subscribe_DuplicateID(t *testing.T) {

FILE: server/types.go
  type publishMessage (line 12) | type publishMessage struct
  type messageEncoder (line 33) | type messageEncoder
  type queryFilter (line 35) | type queryFilter struct
    method Pass (line 65) | func (q *queryFilter) Pass(msg *model.Message) bool {
  function parseQueryFilters (line 43) | func parseQueryFilters(r *http.Request) (*queryFilter, error) {
  type templateMode (line 94) | type templateMode
    method Enabled (line 97) | func (t templateMode) Enabled() bool {
    method InlineMode (line 102) | func (t templateMode) InlineMode() bool {
    method FileMode (line 107) | func (t templateMode) FileMode() bool {
    method FileName (line 112) | func (t templateMode) FileName() string {
  type templateFile (line 129) | type templateFile struct
  type apiHealthResponse (line 135) | type apiHealthResponse struct
  type apiVersionResponse (line 139) | type apiVersionResponse struct
  type apiStatsResponse (line 145) | type apiStatsResponse struct
  type apiUserAddOrUpdateRequest (line 150) | type apiUserAddOrUpdateRequest struct
  type apiUserResponse (line 158) | type apiUserResponse struct
  type apiUserGrantResponse (line 165) | type apiUserGrantResponse struct
  type apiUserDeleteRequest (line 170) | type apiUserDeleteRequest struct
  type apiAccessAllowRequest (line 174) | type apiAccessAllowRequest struct
  type apiAccessResetRequest (line 180) | type apiAccessResetRequest struct
  type apiAccountCreateRequest (line 185) | type apiAccountCreateRequest struct
  type apiAccountPasswordChangeRequest (line 190) | type apiAccountPasswordChangeRequest struct
  type apiAccountDeleteRequest (line 195) | type apiAccountDeleteRequest struct
  type apiAccountTokenIssueRequest (line 199) | type apiAccountTokenIssueRequest struct
  type apiAccountTokenUpdateRequest (line 204) | type apiAccountTokenUpdateRequest struct
  type apiAccountTokenResponse (line 210) | type apiAccountTokenResponse struct
  type apiAccountPhoneNumberVerifyRequest (line 219) | type apiAccountPhoneNumberVerifyRequest struct
  type apiAccountPhoneNumberAddRequest (line 224) | type apiAccountPhoneNumberAddRequest struct
  type apiAccountTier (line 229) | type apiAccountTier struct
  type apiAccountLimits (line 234) | type apiAccountLimits struct
  type apiAccountStats (line 247) | type apiAccountStats struct
  type apiAccountReservation (line 260) | type apiAccountReservation struct
  type apiAccountBilling (line 265) | type apiAccountBilling struct
  type apiAccountResponse (line 274) | type apiAccountResponse struct
  type apiAccountReservationRequest (line 291) | type apiAccountReservationRequest struct
  type apiConfigResponse (line 296) | type apiConfigResponse struct
  type apiAccountBillingPrices (line 313) | type apiAccountBillingPrices struct
  type apiAccountBillingTier (line 318) | type apiAccountBillingTier struct
  type apiAccountBillingSubscriptionCreateResponse (line 325) | type apiAccountBillingSubscriptionCreateResponse struct
  type apiAccountBillingSubscriptionChangeRequest (line 329) | type apiAccountBillingSubscriptionChangeRequest struct
  type apiAccountBillingPortalRedirectResponse (line 334) | type apiAccountBillingPortalRedirectResponse struct
  type apiAccountSyncTopicResponse (line 338) | type apiAccountSyncTopicResponse struct
  type apiSuccessResponse (line 342) | type apiSuccessResponse struct
  function newSuccessResponse (line 346) | func newSuccessResponse() *apiSuccessResponse {
  type apiStripeSubscriptionUpdatedEvent (line 352) | type apiStripeSubscriptionUpdatedEvent struct
  type apiStripeSubscriptionDeletedEvent (line 370) | type apiStripeSubscriptionDeletedEvent struct
  type apiWebPushUpdateSubscriptionRequest (line 375) | type apiWebPushUpdateSubscriptionRequest struct
  constant webPushMessageEvent (line 384) | webPushMessageEvent  = "message"
  constant webPushExpiringEvent (line 385) | webPushExpiringEvent = "subscription_expiring"
  type webPushPayload (line 388) | type webPushPayload struct
  function newWebPushPayload (line 394) | func newWebPushPayload(subscriptionID string, message *model.Message) *w...
  type webPushControlMessagePayload (line 402) | type webPushControlMessagePayload struct
  function newWebPushSubscriptionExpiringPayload (line 406) | func newWebPushSubscriptionExpiringPayload() *webPushControlMessagePaylo...
  type webManifestResponse (line 413) | type webManifestResponse struct
  type webManifestIcon (line 425) | type webManifestIcon struct

FILE: server/util.go
  function readBoolParam (line 33) | func readBoolParam(r *http.Request, defaultValue bool, names ...string) ...
  function isBoolValue (line 41) | func isBoolValue(value string) bool {
  function toBool (line 45) | func toBool(value string) bool {
  function readCommaSeparatedParam (line 49) | func readCommaSeparatedParam(r *http.Request, names ...string) []string {
  function readParam (line 56) | func readParam(r *http.Request, names ...string) string {
  function readHeaderParam (line 64) | func readHeaderParam(r *http.Request, names ...string) string {
  function readQueryParam (line 74) | func readQueryParam(r *http.Request, names ...string) string {
  function extractIPAddress (line 86) | func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedH...
  function extractIPAddressFromHeader (line 111) | func extractIPAddressFromHeader(r *http.Request, forwardedHeader string,...
  function readJSONWithLimit (line 148) | func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty boo...
  function withContext (line 160) | func withContext(r *http.Request, ctx map[contextKey]any) *http.Request {
  function fromContext (line 168) | func fromContext[T any](r *http.Request, key contextKey) (T, error) {
  function maybeDecodeHeader (line 179) | func maybeDecodeHeader(name, value string) string {
  function maybeIgnoreSpecialHeader (line 193) | func maybeIgnoreSpecialHeader(name, value string) string {

FILE: server/util_test.go
  function TestReadBoolParam (line 16) | func TestReadBoolParam(t *testing.T) {
  function TestRenderHTTPRequest_ValidShort (line 38) | func TestRenderHTTPRequest_ValidShort(t *testing.T) {
  function TestRenderHTTPRequest_ValidLong (line 48) | func TestRenderHTTPRequest_ValidLong(t *testing.T) {
  function TestRenderHTTPRequest_InvalidShort (line 59) | func TestRenderHTTPRequest_InvalidShort(t *testing.T) {
  function TestRenderHTTPRequest_InvalidLong (line 70) | func TestRenderHTTPRequest_InvalidLong(t *testing.T) {
  function TestMaybeIgnoreSpecialHeader (line 82) | func TestMaybeIgnoreSpecialHeader(t *testing.T) {
  function TestMaybeDecodeHeaders (line 88) | func TestMaybeDecodeHeaders(t *testing.T) {
  function TestExtractIPAddress (line 95) | func TestExtractIPAddress(t *testing.T) {
  function TestExtractIPAddress_UnixSocket (line 112) | func TestExtractIPAddress_UnixSocket(t *testing.T) {
  function TestExtractIPAddress_MixedIPv4IPv6 (line 125) | func TestExtractIPAddress_MixedIPv4IPv6(t *testing.T) {
  function TestExtractIPAddress_TrustedIPv6Prefix (line 133) | func TestExtractIPAddress_TrustedIPv6Prefix(t *testing.T) {
  function TestVisitorID (line 141) | func TestVisitorID(t *testing.T) {

FILE: server/visitor.go
  constant oneDay (line 18) | oneDay = 24 * time.Hour
  constant visitorExpungeAfter (line 23) | visitorExpungeAfter = oneDay
  constant visitorDefaultReservationsLimit (line 27) | visitorDefaultReservationsLimit = int64(0)
  constant visitorDefaultCallsLimit (line 31) | visitorDefaultCallsLimit = int64(0)
  constant visitorMessageToRequestLimitBurstRate (line 41) | visitorMessageToRequestLimitBurstRate       = 0.05
  constant visitorMessageToRequestLimitBurstMax (line 42) | visitorMessageToRequestLimitBurstMax        = 1000
  constant visitorMessageToRequestLimitReplenishFactor (line 43) | visitorMessageToRequestLimitReplenishFactor = 2
  constant visitorEmailLimitBurstRate (line 50) | visitorEmailLimitBurstRate = 0.2
  constant visitorEmailLimitBurstMax (line 51) | visitorEmailLimitBurstMax  = 150
  type visitor (line 55) | type visitor struct
    method Context (line 146) | func (v *visitor) Context() log.Context {
    method contextNoLock (line 152) | func (v *visitor) contextNoLock() log.Context {
    method RequestAllowed (line 207) | func (v *visitor) RequestAllowed() bool {
    method FirebaseAllowed (line 213) | func (v *visitor) FirebaseAllowed() bool {
    method FirebaseTemporarilyDeny (line 219) | func (v *visitor) FirebaseTemporarilyDeny() {
    method MessageAllowed (line 225) | func (v *visitor) MessageAllowed() bool {
    method EmailAllowed (line 231) | func (v *visitor) EmailAllowed() bool {
    method CallAllowed (line 237) | func (v *visitor) CallAllowed() bool {
    method SubscriptionAllowed (line 243) | func (v *visitor) SubscriptionAllowed() bool {
    method AuthAllowed (line 250) | func (v *visitor) AuthAllowed() bool {
    method AuthFailed (line 260) | func (v *visitor) AuthFailed() {
    method AccountCreationAllowed (line 269) | func (v *visitor) AccountCreationAllowed() bool {
    method AccountCreated (line 279) | func (v *visitor) AccountCreated() {
    method BandwidthAllowed (line 287) | func (v *visitor) BandwidthAllowed(bytes int64) bool {
    method RemoveSubscription (line 293) | func (v *visitor) RemoveSubscription() {
    method Keepalive (line 299) | func (v *visitor) Keepalive() {
    method BandwidthLimiter (line 305) | func (v *visitor) BandwidthLimiter() util.Limiter {
    method Stale (line 311) | func (v *visitor) Stale() bool {
    method Stats (line 317) | func (v *visitor) Stats() *user.Stats {
    method ResetStats (line 327) | func (v *visitor) ResetStats() {
    method User (line 336) | func (v *visitor) User() *user.User {
    method IP (line 343) | func (v *visitor) IP() netip.Addr {
    method Authenticated (line 350) | func (v *visitor) Authenticated() bool {
    method SetUser (line 357) | func (v *visitor) SetUser(u *user.User) {
    method MaybeUserID (line 373) | func (v *visitor) MaybeUserID() string {
    method resetLimitersNoLock (line 382) | func (v *visitor) resetLimitersNoLock(messages, emails, calls int64, e...
    method Limits (line 406) | func (v *visitor) Limits() *visitorLimits {
    method limitsNoLock (line 412) | func (v *visitor) limitsNoLock() *visitorLimits {
    method Info (line 461) | func (v *visitor) Info() (*visitorInfo, error) {
    method infoLightNoLock (line 495) | func (v *visitor) infoLightNoLock() *visitorInfo {
  type visitorInfo (line 74) | type visitorInfo struct
  type visitorLimits (line 79) | type visitorLimits struct
  type visitorStats (line 96) | type visitorStats struct
  type visitorLimitBasis (line 111) | type visitorLimitBasis
  constant visitorLimitBasisIP (line 114) | visitorLimitBasisIP   = visitorLimitBasis("ip")
  constant visitorLimitBasisTier (line 115) | visitorLimitBasisTier = visitorLimitBasis("tier")
  function newVisitor (line 118) | func newVisitor(conf *Config, messageCache *message.Cache, userManager *...
  function visitorExtendedInfoContext (line 196) | func visitorExtendedInfoContext(info *visitorInfo) log.Context {
  function tierBasedVisitorLimits (line 419) | func tierBasedVisitorLimits(conf *Config, tier *user.Tier) *visitorLimits {
  function configBasedVisitorLimits (line 438) | func configBasedVisitorLimits(conf *Config) *visitorLimits {
  function zeroIfNegative (line 513) | func zeroIfNegative(value int64) int64 {
  function replenishDurationToDailyLimit (line 520) | func replenishDurationToDailyLimit(duration time.Duration) int64 {
  function dailyLimitToRate (line 524) | func dailyLimitToRate(limit int64) rate.Limit {
  function visitorID (line 529) | func visitorID(ip netip.Addr, u *user.User, conf *Config) string {

FILE: test/server.go
  function StartServer (line 13) | func StartServer(t *testing.T) (*server.Server, int) {
  function StartServerWithConfig (line 18) | func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.S...
  function findAvailablePort (line 37) | func findAvailablePort(t *testing.T) int {
  function StopServer (line 48) | func StopServer(t *testing.T, s *server.Server, port int) {

FILE: test/util.go
  function WaitForPortUp (line 11) | func WaitForPortUp(t *testing.T, port int) {
  function WaitForPortDown (line 31) | func WaitForPortDown(t *testing.T, port int) {

FILE: tools/fbsend/main.go
  function main (line 16) | func main() {
  function fail (line 49) | func fail(s string) {

FILE: tools/loadgen/main.go
  function main (line 12) | func main() {
  function subscribe (line 31) | func subscribe(worker int, baseURL string) {
  function poll (line 50) | func poll(worker int, baseURL string) {

FILE: tools/loadtest/main.go
  function main (line 65) | func main() {
  function trackError (line 205) | func trackError(category string, err error) {
  function trackErrorMsg (line 213) | func trackErrorMsg(category string, msg string) {
  function truncateErr (line 221) | func truncateErr(err error) string {
  function setAuth (line 229) | func setAuth(req *http.Request) {
  function generateTopics (line 235) | func generateTopics(n int) []string {
  function pickTopic (line 245) | func pickTopic(topics []string) string {
  function randomSince (line 250) | func randomSince() string {
  function randomMessage (line 256) | func randomMessage() string {
  function runAtRate (line 271) | func runAtRate(ctx context.Context, rate float64, fn func()) {
  function doPoll (line 288) | func doPoll(ctx context.Context, client *http.Client, topics []string) {
  function doPublishPost (line 294) | func doPublishPost(ctx context.Context, client *http.Client, topics []st...
  function doPublishPut (line 323) | func doPublishPut(ctx context.Context, client *http.Client, topics []str...
  function doConfig (line 345) | func doConfig(ctx context.Context, client *http.Client, topics []string) {
  function doAccountCheck (line 350) | func doAccountCheck(ctx context.Context, client *http.Client, topics []s...
  function doOtherGet (line 355) | func doOtherGet(ctx context.Context, client *http.Client, topics []strin...
  function doGet (line 361) | func doGet(ctx context.Context, client *http.Client, url string) {
  function streamSubscription (line 383) | func streamSubscription(ctx context.Context, client *http.Client, topics...
  function wsSubscription (line 435) | func wsSubscription(ctx context.Context, topics []string) {
  function reportStats (line 498) | func reportStats(ctx context.Context) {

FILE: tools/pgimport/main.go
  constant batchSize (line 21) | batchSize = 1000
  constant expectedMessageSchemaVersion (line 23) | expectedMessageSchemaVersion = 14
  constant expectedUserSchemaVersion (line 24) | expectedUserSchemaVersion    = 6
  constant expectedWebPushSchemaVersion (line 25) | expectedWebPushSchemaVersion = 1
  constant everyoneID (line 27) | everyoneID = "u_everyone"
  constant createMessageSchemaQuery (line 30) | createMessageSchemaQuery = `
  constant createUserSchemaQuery (line 77) | createUserSchemaQuery = `
  constant createWebPushSchemaQuery (line 153) | createWebPushSchemaQuery = `
  function main (line 191) | func main() {
  function execImport (line 206) | func execImport(c *cli.Context) error {
  function execPreImport (line 312) | func execPreImport(c *cli.Context, databaseURL, cacheFile string) error {
  function createSchema (line 360) | func createSchema(pgDB *sql.DB, cacheFile, authFile, webPushFile string)...
  function loadConfigFile (line 388) | func loadConfigFile(configFlag string, flags []cli.Flag) cli.BeforeFunc {
  function newYamlSourceFromFile (line 405) | func newYamlSourceFromFile(file string, flags []cli.Flag) (altsrc.InputS...
  function verifySchemaVersion (line 425) | func verifySchemaVersion(pgDB *sql.DB, store string, expected int) error {
  function printSource (line 437) | func printSource(label, path string) {
  function maskPassword (line 447) | func maskPassword(databaseURL string) string {
  function openSQLite (line 464) | func openSQLite(filename string) (*sql.DB, error) {
  function importUsers (line 473) | func importUsers(sqliteFile string, pgDB *sql.DB) error {
  function importTiers (line 515) | func importTiers(sqlDB, pgDB *sql.DB) (int, error) {
  function importUserRows (line 551) | func importUserRows(sqlDB, pgDB *sql.DB) (int, error) {
  function importUserAccess (line 596) | func importUserAccess(sqlDB, pgDB *sql.DB) (int, error) {
  function importUserTokens (line 634) | func importUserTokens(sqlDB, pgDB *sql.DB) (int, error) {
  function importUserPhones (line 670) | func importUserPhones(sqlDB, pgDB *sql.DB) (int, error) {
  constant preImportTimeDelta (line 705) | preImportTimeDelta = 30
  function maxMessageTime (line 710) | func maxMessageTime(pgDB *sql.DB) int64 {
  function importMessages (line 723) | func importMessages(sqliteFile string, pgDB *sql.DB, sinceTime int64) er...
  function importWebPush (line 838) | func importWebPush(sqliteFile string, pgDB *sql.DB) error {
  function toUTF8 (line 921) | func toUTF8(s string) string {
  function verifyUsers (line 929) | func verifyUsers(sqliteFile string, pgDB *sql.DB, failed *bool) error {
  function verifyMessages (line 969) | func verifyMessages(sqliteFile string, pgDB *sql.DB, failed *bool) error {
  function verifyWebPush (line 981) | func verifyWebPush(sqliteFile string, pgDB *sql.DB, failed *bool) error {
  function verifyCount (line 1003) | func verifyCount(sqlDB, pgDB *sql.DB, table, sqliteQuery, pgQuery string...
  function verifyContent (line 1023) | func verifyContent(sqlDB, pgDB *sql.DB, table, sqliteQuery, pgQuery stri...
  function verifySampledMessages (line 1095) | func verifySampledMessages(sqlDB, pgDB *sql.DB, failed *bool) {
  function makeStringSlice (line 1158) | func makeStringSlice(n int) []any {

FILE: user/manager.go
  constant tierIDPrefix (line 23) | tierIDPrefix                    = "ti_"
  constant tierIDLength (line 24) | tierIDLength                    = 8
  constant syncTopicPrefix (line 25) | syncTopicPrefix                 = "st_"
  constant syncTopicLength (line 26) | syncTopicLength                 = 16
  constant userIDPrefix (line 27) | userIDPrefix                    = "u_"
  constant userIDLength (line 28) | userIDLength                    = 12
  constant userAuthIntentionalSlowDownHash (line 29) | userAuthIntentionalSlowDownHash = "$2a$10$YFCQvqQDwIIwnJM1xkAYOeih0dg17U...
  constant userHardDeleteAfterDuration (line 30) | userHardDeleteAfterDuration     = 7 * 24 * time.Hour
  constant tokenPrefix (line 31) | tokenPrefix                     = "tk_"
  constant tokenLength (line 32) | tokenLength                     = 32
  constant tokenMaxCount (line 33) | tokenMaxCount                   = 60
  constant tag (line 34) | tag                             = "user_manager"
  constant DefaultUserStatsQueueWriterInterval (line 39) | DefaultUserStatsQueueWriterInterval = 33 * time.Second
  constant DefaultUserPasswordBcryptCost (line 40) | DefaultUserPasswordBcryptCost       = 10
  type Manager (line 50) | type Manager struct
    method Authenticate (line 85) | func (a *Manager) Authenticate(username, password string) (*User, erro...
    method AuthenticateToken (line 107) | func (a *Manager) AuthenticateToken(token string) (*User, error) {
    method AddUser (line 121) | func (a *Manager) AddUser(username, password string, role Role, hashed...
    method addUserTx (line 132) | func (a *Manager) addUserTx(tx *sql.Tx, username, hash string, role Ro...
    method RemoveUser (line 150) | func (a *Manager) RemoveUser(username string) error {
    method removeUserTx (line 160) | func (a *Manager) removeUserTx(tx *sql.Tx, username string) error {
    method MarkUserRemoved (line 173) | func (a *Manager) MarkUserRemoved(user *User) error {
    method RemoveDeletedUsers (line 193) | func (a *Manager) RemoveDeletedUsers() error {
    method ChangePassword (line 201) | func (a *Manager) ChangePassword(username, password string, hashed boo...
    method changePasswordHashTx (line 215) | func (a *Manager) changePasswordHashTx(tx *sql.Tx, username, hash stri...
    method ChangeRole (line 224) | func (a *Manager) ChangeRole(username string, role Role) error {
    method changeRoleTx (line 234) | func (a *Manager) changeRoleTx(tx *sql.Tx, username string, role Role)...
    method CanChangeUser (line 252) | func (a *Manager) CanChangeUser(username string) error {
    method changeProvisionedTx (line 263) | func (a *Manager) changeProvisionedTx(tx *sql.Tx, username string, pro...
    method ChangeSettings (line 271) | func (a *Manager) ChangeSettings(userID string, prefs *Prefs) error {
    method ChangeTier (line 284) | func (a *Manager) ChangeTier(username, tier string) error {
    method ResetTier (line 304) | func (a *Manager) ResetTier(username string) error {
    method checkReservationsLimitTx (line 319) | func (a *Manager) checkReservationsLimitTx(tx *sql.Tx, username string...
    method ResetStats (line 336) | func (a *Manager) ResetStats() error {
    method EnqueueUserStats (line 348) | func (a *Manager) EnqueueUserStats(userID string, stats *Stats) {
    method asyncQueueWriter (line 354) | func (a *Manager) asyncQueueWriter(interval time.Duration) {
    method writeUserStatsQueue (line 366) | func (a *Manager) writeUserStatsQueue() error {
    method User (line 398) | func (a *Manager) User(username string) (*User, error) {
    method userTx (line 402) | func (a *Manager) userTx(tx db.Querier, username string) (*User, error) {
    method UserByID (line 411) | func (a *Manager) UserByID(id string) (*User, error) {
    method userByToken (line 420) | func (a *Manager) userByToken(token string) (*User, error) {
    method UserByStripeCustomer (line 429) | func (a *Manager) UserByStripeCustomer(customerID string) (*User, erro...
    method Users (line 439) | func (a *Manager) Users() ([]*User, error) {
    method UsersCount (line 448) | func (a *Manager) UsersCount() (int64, error) {
    method readUser (line 464) | func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
    method readUsers (line 476) | func (a *Manager) readUsers(rows *sql.Rows) ([]*User, error) {
    method scanUser (line 489) | func (a *Manager) scanUser(rows *sql.Rows) (*User, error) {
    method maybeHashPassword (line 548) | func (a *Manager) maybeHashPassword(password string, hashed bool) (str...
    method Authorize (line 560) | func (a *Manager) Authorize(user *User, topic string, perm Permission)...
    method resolvePerms (line 578) | func (a *Manager) resolvePerms(base, perm Permission) error {
    method AllowAccess (line 590) | func (a *Manager) AllowAccess(username string, topicPattern string, pe...
    method allowAccessTx (line 596) | func (a *Manager) allowAccessTx(tx *sql.Tx, username string, topicPatt...
    method ResetAccess (line 608) | func (a *Manager) ResetAccess(username string, topicPattern string) er...
    method resetAccessTx (line 614) | func (a *Manager) resetAccessTx(tx *sql.Tx, username string, topicPatt...
    method DefaultAccess (line 630) | func (a *Manager) DefaultAccess() Permission {
    method AllowReservation (line 636) | func (a *Manager) AllowReservation(username string, topic string) error {
    method authorizeTopicAccess (line 656) | func (a *Manager) authorizeTopicAccess(usernameOrEveryone, topic strin...
    method AllGrants (line 674) | func (a *Manager) AllGrants() (map[string][]Grant, error) {
    method Grants (line 702) | func (a *Manager) Grants(username string) ([]Grant, error) {
    method AddReservation (line 730) | func (a *Manager) AddReservation(username string, topic string, everyo...
    method RemoveReservations (line 763) | func (a *Manager) RemoveReservations(username string, topics ...string...
    method Reservations (line 783) | func (a *Manager) Reservations(username string) ([]Reservation, error) {
    method reservationsTx (line 787) | func (a *Manager) reservationsTx(tx db.Querier, username string) ([]Re...
    method HasReservation (line 813) | func (a *Manager) HasReservation(username, topic string) (bool, error) {
    method hasReservationTx (line 817) | func (a *Manager) hasReservationTx(tx db.Querier, username, topic stri...
    method ReservationsCount (line 834) | func (a *Manager) ReservationsCount(username string) (int64, error) {
    method reservationsCountTx (line 838) | func (a *Manager) reservationsCountTx(tx db.Querier, username string) ...
    method ReservationOwner (line 855) | func (a *Manager) ReservationOwner(topic string) (string, error) {
    method RemoveExcessReservations (line 874) | func (a *Manager) RemoveExcessReservations(username string, limit int6...
    method otherAccessCount (line 896) | func (a *Manager) otherAccessCount(username, topic string) (int, error) {
    method removeReservationAccessTx (line 912) | func (a *Manager) removeReservationAccessTx(tx *sql.Tx, username, topi...
    method resetUserAccessTx (line 919) | func (a *Manager) resetUserAccessTx(tx *sql.Tx, username string) error {
    method resetTopicAccessTx (line 927) | func (a *Manager) resetTopicAccessTx(tx *sql.Tx, username, topicPatter...
    method CreateToken (line 940) | func (a *Manager) CreateToken(userID, label string, expires time.Time,...
    method createTokenTx (line 948) | func (a *Manager) createTokenTx(tx *sql.Tx, userID, token, label strin...
    method ChangeToken (line 976) | func (a *Manager) ChangeToken(userID, token string, label *string, exp...
    method RemoveToken (line 1000) | func (a *Manager) RemoveToken(userID, token string) error {
    method canChangeToken (line 1014) | func (a *Manager) canChangeToken(userID, token string) error {
    method Token (line 1025) | func (a *Manager) Token(userID, token string) (*Token, error) {
    method Tokens (line 1035) | func (a *Manager) Tokens(userID string) ([]*Token, error) {
    method allProvisionedTokens (line 1054) | func (a *Manager) allProvisionedTokens() ([]*Token, error) {
    method RemoveExpiredTokens (line 1074) | func (a *Manager) RemoveExpiredTokens() error {
    method EnqueueTokenUpdate (line 1083) | func (a *Manager) EnqueueTokenUpdate(tokenID string, update *TokenUpda...
    method writeTokenUpdateQueue (line 1089) | func (a *Manager) writeTokenUpdateQueue() error {
    method updateTokenLastAccessTx (line 1112) | func (a *Manager) updateTokenLastAccessTx(tx *sql.Tx, token string, la...
    method readToken (line 1119) | func (a *Manager) readToken(rows *sql.Rows) (*Token, error) {
    method AddTier (line 1146) | func (a *Manager) AddTier(tier *Tier) error {
    method UpdateTier (line 1157) | func (a *Manager) UpdateTier(tier *Tier) error {
    method RemoveTier (line 1165) | func (a *Manager) RemoveTier(code string) error {
    method Tiers (line 1177) | func (a *Manager) Tiers() ([]*Tier, error) {
    method Tier (line 1197) | func (a *Manager) Tier(code string) (*Tier, error) {
    method TierByStripePrice (line 1207) | func (a *Manager) TierByStripePrice(priceID string) (*Tier, error) {
    method readTier (line 1216) | func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) {
    method PhoneNumbers (line 1248) | func (a *Manager) PhoneNumbers(userID string) ([]string, error) {
    method AddPhoneNumber (line 1268) | func (a *Manager) AddPhoneNumber(userID, phoneNumber string) error {
    method RemovePhoneNumber (line 1279) | func (a *Manager) RemovePhoneNumber(userID, phoneNumber string) error {
    method readPhoneNumber (line 1284) | func (a *Manager) readPhoneNumber(rows *sql.Rows) (string, error) {
    method ChangeBilling (line 1298) | func (a *Manager) ChangeBilling(username string, billing *Billing) err...
    method maybeProvisionUsersAccessAndTokens (line 1306) | func (a *Manager) maybeProvisionUsersAccessAndTokens() error {
    method removeAllProvisioned (line 1343) | func (a *Manager) removeAllProvisioned() error {
    method maybeProvisionUsers (line 1360) | func (a *Manager) maybeProvisionUsers(tx *sql.Tx, provisionUsernames [...
    method maybeProvisionGrants (line 1408) | func (a *Manager) maybeProvisionGrants(tx *sql.Tx) error {
    method maybeProvisionTokens (line 1435) | func (a *Manager) maybeProvisionTokens(tx *sql.Tx, provisionUsernames ...
    method Close (line 1469) | func (a *Manager) Close() error {
  function newManager (line 61) | func newManager(d *db.DB, queries queries, config *Config) (*Manager, er...
  function isUniqueConstraintError (line 1474) | func isUniqueConstraintError(err error) bool {

FILE: user/manager_postgres.go
  constant postgresSelectUsersQuery (line 10) | postgresSelectUsersQuery = `
  constant postgresSelectUserByIDQuery (line 21) | postgresSelectUserByIDQuery = `
  constant postgresSelectUserByNameQuery (line 27) | postgresSelectUserByNameQuery = `
  constant postgresSelectUserByTokenQuery (line 33) | postgresSelectUserByTokenQuery = `
  constant postgresSelectUserByStripeCustomerIDQuery (line 40) | postgresSelectUserByStripeCustomerIDQuery = `
  constant postgresSelectUsernamesQuery (line 46) | postgresSelectUsernamesQuery = `
  constant postgresSelectUserCountQuery (line 56) | postgresSelectUserCountQuery          = `SELECT COUNT(*) FROM "user"`
  constant postgresSelectUserIDFromUsernameQuery (line 57) | postgresSelectUserIDFromUsernameQuery = `SELECT id FROM "user" WHERE use...
  constant postgresInsertUserQuery (line 58) | postgresInsertUserQuery               = `INSERT INTO "user" (id, user_na...
  constant postgresUpdateUserPassQuery (line 59) | postgresUpdateUserPassQuery           = `UPDATE "user" SET pass = $1 WHE...
  constant postgresUpdateUserRoleQuery (line 60) | postgresUpdateUserRoleQuery           = `UPDATE "user" SET role = $1 WHE...
  constant postgresUpdateUserProvisionedQuery (line 61) | postgresUpdateUserProvisionedQuery    = `UPDATE "user" SET provisioned =...
  constant postgresUpdateUserPrefsQuery (line 62) | postgresUpdateUserPrefsQuery          = `UPDATE "user" SET prefs = $1 WH...
  constant postgresUpdateUserStatsQuery (line 63) | postgresUpdateUserStatsQuery          = `UPDATE "user" SET stats_message...
  constant postgresUpdateUserStatsResetAllQuery (line 64) | postgresUpdateUserStatsResetAllQuery  = `UPDATE "user" SET stats_message...
  constant postgresUpdateUserTierQuery (line 65) | postgresUpdateUserTierQuery           = `UPDATE "user" SET tier_id = (SE...
  constant postgresUpdateUserDeletedQuery (line 66) | postgresUpdateUserDeletedQuery        = `UPDATE "user" SET deleted = $1 ...
  constant postgresDeleteUserQuery (line 67) | postgresDeleteUserQuery               = `DELETE FROM "user" WHERE user_n...
  constant postgresDeleteUserTierQuery (line 68) | postgresDeleteUserTierQuery           = `UPDATE "user" SET tier_id = nul...
  constant postgresDeleteUsersMarkedQuery (line 69) | postgresDeleteUsersMarkedQuery        = `DELETE FROM "user" WHERE delete...
  constant postgresDeleteUsersProvisionedQuery (line 70) | postgresDeleteUsersProvisionedQuery   = `DELETE FROM "user" WHERE provis...
  constant postgresSelectTopicPermsQuery (line 73) | postgresSelectTopicPermsQuery = `
  constant postgresSelectUserAllAccessQuery (line 80) | postgresSelectUserAllAccessQuery = `
  constant postgresSelectUserAccessQuery (line 85) | postgresSelectUserAccessQuery = `
  constant postgresSelectUserReservationsQuery (line 91) | postgresSelectUserReservationsQuery = `
  constant postgresSelectUserReservationsCountQuery (line 99) | postgresSelectUserReservationsCountQuery = `
  constant postgresSelectUserReservationsOwnerQuery (line 105) | postgresSelectUserReservationsOwnerQuery = `
  constant postgresSelectUserHasReservationQuery (line 111) | postgresSelectUserHasReservationQuery = `
  constant postgresSelectOtherAccessCountQuery (line 118) | postgresSelectOtherAccessCountQuery = `
  constant postgresUpsertUserAccessQuery (line 124) | postgresUpsertUserAccessQuery = `
  constant postgresDeleteUserAccessQuery (line 137) | postgresDeleteUserAccessQuery = `
  constant postgresDeleteUserAccessProvisionedQuery (line 142) | postgresDeleteUserAccessProvisionedQuery = `DELETE FROM user_access WHER...
  constant postgresDeleteTopicAccessQuery (line 143) | postgresDeleteTopicAccessQuery           = `
  constant postgresDeleteAllAccessQuery (line 148) | postgresDeleteAllAccessQuery = `DELETE FROM user_access`
  constant postgresSelectTokenQuery (line 151) | postgresSelectTokenQuery                = `SELECT token, label, last_acc...
  constant postgresSelectTokensQuery (line 152) | postgresSelectTokensQuery               = `SELECT token, label, last_acc...
  constant postgresSelectTokenCountQuery (line 153) | postgresSelectTokenCountQuery           = `SELECT COUNT(*) FROM user_tok...
  constant postgresSelectAllProvisionedTokensQuery (line 154) | postgresSelectAllProvisionedTokensQuery = `SELECT token, label, last_acc...
  constant postgresUpsertTokenQuery (line 155) | postgresUpsertTokenQuery                = `
  constant postgresUpdateTokenQuery (line 161) | postgresUpdateTokenQuery                = `UPDATE user_token SET label =...
  constant postgresUpdateTokenLastAccessQuery (line 162) | postgresUpdateTokenLastAccessQuery      = `UPDATE user_token SET last_ac...
  constant postgresDeleteTokenQuery (line 163) | postgresDeleteTokenQuery                = `DELETE FROM user_token WHERE ...
  constant postgresDeleteProvisionedTokenQuery (line 164) | postgresDeleteProvisionedTokenQuery     = `DELETE FROM user_token WHERE ...
  constant postgresDeleteAllProvisionedTokensQuery (line 165) | postgresDeleteAllProvisionedTokensQuery = `DELETE FROM user_token WHERE ...
  constant postgresDeleteAllTokenQuery (line 166) | postgresDeleteAllTokenQuery             = `DELETE FROM user_token WHERE ...
  constant postgresDeleteExpiredTokensQuery (line 167) | postgresDeleteExpiredTokensQuery        = `DELETE FROM user_token WHERE ...
  constant postgresDeleteExcessTokensQuery (line 168) | postgresDeleteExcessTokensQuery         = `
  constant postgresInsertTierQuery (line 181) | postgresInsertTierQuery = `
  constant postgresUpdateTierQuery (line 185) | postgresUpdateTierQuery = `
  constant postgresSelectTiersQuery (line 190) | postgresSelectTiersQuery = `
  constant postgresSelectTierByCodeQuery (line 194) | postgresSelectTierByCodeQuery = `
  constant postgresSelectTierByPriceIDQuery (line 199) | postgresSelectTierByPriceIDQuery = `
  constant postgresDeleteTierQuery (line 204) | postgresDeleteTierQuery = `DELETE FROM tier WHERE code = $1`
  constant postgresSelectPhoneNumbersQuery (line 207) | postgresSelectPhoneNumbersQuery = `SELECT phone_number FROM user_phone W...
  constant postgresInsertPhoneNumberQuery (line 208) | postgresInsertPhoneNumberQuery  = `INSERT INTO user_phone (user_id, phon...
  constant postgresDeletePhoneNumberQuery (line 209) | postgresDeletePhoneNumberQuery  = `DELETE FROM user_phone WHERE user_id ...
  constant postgresUpdateBillingQuery (line 212) | postgresUpdateBillingQuery = `
  function NewPostgresManager (line 281) | func NewPostgresManager(d *db.DB, config *Config) (*Manager, error) {

FILE: user/manager_postgres_schema.go
  constant postgresCreateTablesQueries (line 10) | postgresCreateTablesQueries = `
  constant postgresCurrentSchemaVersion (line 87) | postgresCurrentSchemaVersion     = 6
  constant postgresSelectSchemaVersionQuery (line 88) | postgresSelectSchemaVersionQuery = `SELECT version FROM schema_version W...
  constant postgresInsertSchemaVersionQuery (line 89) | postgresInsertSchemaVersionQuery = `INSERT INTO schema_version (store, v...
  function setupPostgres (line 92) | func setupPostgres(db *sql.DB) error {
  function setupNewPostgres (line 105) | func setupNewPostgres(db *sql.DB) error {

FILE: user/manager_sqlite.go
  constant sqliteSelectUsersQuery (line 16) | sqliteSelectUsersQuery = `
  constant sqliteSelectUserByIDQuery (line 27) | sqliteSelectUserByIDQuery = `
  constant sqliteSelectUserByNameQuery (line 33) | sqliteSelectUserByNameQuery = `
  constant sqliteSelectUserByTokenQuery (line 39) | sqliteSelectUserByTokenQuery = `
  constant sqliteSelectUserByStripeCustomerIDQuery (line 46) | sqliteSelectUserByStripeCustomerIDQuery = `
  constant sqliteSelectUsernamesQuery (line 52) | sqliteSelectUsernamesQuery = `
  constant sqliteSelectUserCountQuery (line 62) | sqliteSelectUserCountQuery          = `SELECT COUNT(*) FROM user`
  constant sqliteSelectUserIDFromUsernameQuery (line 63) | sqliteSelectUserIDFromUsernameQuery = `SELECT id FROM user WHERE user = ?`
  constant sqliteInsertUserQuery (line 64) | sqliteInsertUserQuery               = `INSERT INTO user (id, user, pass,...
  constant sqliteUpdateUserPassQuery (line 65) | sqliteUpdateUserPassQuery           = `UPDATE user SET pass = ? WHERE us...
  constant sqliteUpdateUserRoleQuery (line 66) | sqliteUpdateUserRoleQuery           = `UPDATE user SET role = ? WHERE us...
  constant sqliteUpdateUserProvisionedQuery (line 67) | sqliteUpdateUserProvisionedQuery    = `UPDATE user SET provisioned = ? W...
  constant sqliteUpdateUserPrefsQuery (line 68) | sqliteUpdateUserPrefsQuery          = `UPDATE user SET prefs = ? WHERE i...
  constant sqliteUpdateUserStatsQuery (line 69) | sqliteUpdateUserStatsQuery          = `UPDATE user SET stats_messages = ...
  constant sqliteUpdateUserStatsResetAllQuery (line 70) | sqliteUpdateUserStatsResetAllQuery  = `UPDATE user SET stats_messages = ...
  constant sqliteUpdateUserTierQuery (line 71) | sqliteUpdateUserTierQuery           = `UPDATE user SET tier_id = (SELECT...
  constant sqliteUpdateUserDeletedQuery (line 72) | sqliteUpdateUserDeletedQuery        = `UPDATE user SET deleted = ? WHERE...
  constant sqliteDeleteUserQuery (line 73) | sqliteDeleteUserQuery               = `DELETE FROM user WHERE user = ?`
  constant sqliteDeleteUserTierQuery (line 74) | sqliteDeleteUserTierQuery           = `UPDATE user SET tier_id = null WH...
  constant sqliteDeleteUsersMarkedQuery (line 75) | sqliteDeleteUsersMarkedQuery        = `DELETE FROM user WHERE deleted < ?`
  constant sqliteDeleteUsersProvisionedQuery (line 76) | sqliteDeleteUsersProvisionedQuery   = `DELETE FROM user WHERE provisione...
  constant sqliteSelectTopicPermsQuery (line 79) | sqliteSelectTopicPermsQuery = `
  constant sqliteSelectUserAllAccessQuery (line 86) | sqliteSelectUserAllAccessQuery = `
  constant sqliteSelectUserAccessQuery (line 91) | sqliteSelectUserAccessQuery = `
  constant sqliteSelectUserReservationsQuery (line 97) | sqliteSelectUserReservationsQuery = `
  constant sqliteSelectUserReservationsCountQuery (line 105) | sqliteSelectUserReservationsCountQuery = `
  constant sqliteSelectUserReservationsOwnerQuery (line 111) | sqliteSelectUserReservationsOwnerQuery = `
  constant sqliteSelectUserHasReservationQuery (line 117) | sqliteSelectUserHasReservationQuery = `
  constant sqliteSelectOtherAccessCountQuery (line 124) | sqliteSelectOtherAccessCountQuery = `
  constant sqliteUpsertUserAccessQuery (line 130) | sqliteUpsertUserAccessQuery = `
  constant sqliteDeleteUserAccessQuery (line 136) | sqliteDeleteUserAccessQuery = `
  constant sqliteDeleteUserAccessProvisionedQuery (line 141) | sqliteDeleteUserAccessProvisionedQuery = `DELETE FROM user_access WHERE ...
  constant sqliteDeleteTopicAccessQuery (line 142) | sqliteDeleteTopicAccessQuery           = `
  constant sqliteDeleteAllAccessQuery (line 147) | sqliteDeleteAllAccessQuery = `DELETE FROM user_access`
  constant sqliteSelectTokenQuery (line 150) | sqliteSelectTokenQuery                = `SELECT token, label, last_acces...
  constant sqliteSelectTokensQuery (line 151) | sqliteSelectTokensQuery               = `SELECT token, label, last_acces...
  constant sqliteSelectTokenCountQuery (line 152) | sqliteSelectTokenCountQuery           = `SELECT COUNT(*) FROM user_token...
  constant sqliteSelectAllProvisionedTokensQuery (line 153) | sqliteSelectAllProvisionedTokensQuery = `SELECT token, label, last_acces...
  constant sqliteUpsertTokenQuery (line 154) | sqliteUpsertTokenQuery                = `
  constant sqliteUpdateTokenQuery (line 160) | sqliteUpdateTokenQuery                = `UPDATE user_token SET label = ?...
  constant sqliteUpdateTokenLastAccessQuery (line 161) | sqliteUpdateTokenLastAccessQuery      = `UPDATE user_token SET last_acce...
  constant sqliteDeleteTokenQuery (line 162) | sqliteDeleteTokenQuery                = `DELETE FROM user_token WHERE us...
  constant sqliteDeleteProvisionedTokenQuery (line 163) | sqliteDeleteProvisionedTokenQuery     = `DELETE FROM user_token WHERE to...
  constant sqliteDeleteAllProvisionedTokensQuery (line 164) | sqliteDeleteAllProvisionedTokensQuery = `DELETE FROM user_token WHERE pr...
  constant sqliteDeleteAllTokenQuery (line 165) | sqliteDeleteAllTokenQuery             = `DELETE FROM user_token WHERE us...
  constant sqliteDeleteExpiredTokensQuery (line 166) | sqliteDeleteExpiredTokensQuery        = `DELETE FROM user_token WHERE ex...
  constant sqliteDeleteExcessTokensQuery (line 167) | sqliteDeleteExcessTokensQuery         = `
  constant sqliteInsertTierQuery (line 180) | sqliteInsertTierQuery = `
  constant sqliteUpdateTierQuery (line 184) | sqliteUpdateTierQuery = `
  constant sqliteSelectTiersQuery (line 189) | sqliteSelectTiersQuery = `
  constant sqliteSelectTierByCodeQuery (line 193) | sqliteSelectTierByCodeQuery = `
  constant sqliteSelectTierByPriceIDQuery (line 198) | sqliteSelectTierByPriceIDQuery = `
  constant sqliteDeleteTierQuery (line 203) | sqliteDeleteTierQuery = `DELETE FROM tier WHERE code = ?`
  constant sqliteSelectPhoneNumbersQuery (line 206) | sqliteSelectPhoneNumbersQuery = `SELECT phone_number FROM user_phone WHE...
  constant sqliteInsertPhoneNumberQuery (line 207) | sqliteInsertPhoneNumberQuery  = `INSERT INTO user_phone (user_id, phone_...
  constant sqliteDeletePhoneNumberQuery (line 208) | sqliteDeletePhoneNumberQuery  = `DELETE FROM user_phone WHERE user_id = ...
  constant sqliteUpdateBillingQuery (line 211) | sqliteUpdateBillingQuery = `
  function NewSQLiteManager (line 279) | func NewSQLiteManager(filename, startupQueries string, config *Config) (...

FILE: user/manager_sqlite_schema.go
  constant sqliteCreateTablesQueries (line 14) | sqliteCreateTablesQueries = `
  constant sqliteBuiltinStartupQueries (line 99) | sqliteBuiltinStartupQueries = `PRAGMA foreign_keys = ON;`
  constant sqliteCurrentSchemaVersion (line 104) | sqliteCurrentSchemaVersion     = 6
  constant sqliteInsertSchemaVersionQuery (line 105) | sqliteInsertSchemaVersionQuery = `INSERT INTO schemaVersion VALUES (1, ?)`
  constant sqliteUpdateSchemaVersionQuery (line 106) | sqliteUpdateSchemaVersionQuery = `UPDATE schemaVersion SET version = ? W...
  constant sqliteSelectSchemaVersionQuery (line 107) | sqliteSelectSchemaVersionQuery = `SELECT version FROM schemaVersion WHER...
  constant sqliteMigrate1To2CreateTablesQueries (line 113) | sqliteMigrate1To2CreateTablesQueries = `
  constant sqliteMigrate1To2SelectAllOldUsernamesNoTxQuery (line 181) | sqliteMigrate1To2SelectAllOldUsernamesNoTxQuery = `SELECT user FROM user...
  constant sqliteMigrate1To2InsertUserNoTxQuery (line 182) | sqliteMigrate1To2InsertUserNoTxQuery            = `
  constant sqliteMigrate1To2InsertFromOldTablesAndDropNoTxQuery (line 186) | sqliteMigrate1To2InsertFromOldTablesAndDropNoTxQuery = `
  constant sqliteMigrate2To3UpdateQueries (line 197) | sqliteMigrate2To3UpdateQueries = `
  constant sqliteMigrate3To4UpdateQueries (line 207) | sqliteMigrate3To4UpdateQueries = `
  constant sqliteMigrate4To5UpdateQueries (line 219) | sqliteMigrate4To5UpdateQueries = `
  constant sqliteMigrate5To6UpdateQueries (line 224) | sqliteMigrate5To6UpdateQueries = `
  function setupSQLite (line 328) | func setupSQLite(db *sql.DB) error {
  function setupNewSQLite (line 349) | func setupNewSQLite(sqlDB *sql.DB) error {
  function runSQLiteStartupQueries (line 361) | func runSQLiteStartupQueries(db *sql.DB, startupQueries string) error {
  function sqliteMigrateFrom1 (line 373) | func sqliteMigrateFrom1(sqlDB *sql.DB) error {
  function sqliteMigrateFrom2 (line 415) | func sqliteMigrateFrom2(sqlDB *sql.DB) error {
  function sqliteMigrateFrom3 (line 428) | func sqliteMigrateFrom3(sqlDB *sql.DB) error {
  function sqliteMigrateFrom4 (line 441) | func sqliteMigrateFrom4(sqlDB *sql.DB) error {
  function sqliteMigrateFrom5 (line 454) | func sqliteMigrateFrom5(sqlDB *sql.DB) error {

FILE: user/manager_test.go
  constant minBcryptTimingMillis (line 20) | minBcryptTimingMillis = int64(40)
  type newManagerFunc (line 26) | type newManagerFunc
  function forEachBackend (line 28) | func forEachBackend(t *testing.T, f func(t *testing.T, newManager newMan...
  function TestManager_FullScenario_Default_DenyAll (line 49) | func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
  function TestManager_Access_Order_LengthWriteRead (line 164) | func TestManager_Access_Order_LengthWriteRead(t *testing.T) {
  function TestManager_AddUser_Invalid (line 181) | func TestManager_AddUser_Invalid(t *testing.T) {
  function TestManager_AddUser_Timing (line 189) | func TestManager_AddUser_Timing(t *testing.T) {
  function TestManager_AddUser_And_Query (line 196) | func TestManager_AddUser_And_Query(t *testing.T) {
  function TestManager_MarkUserRemoved_RemoveDeletedUsers (line 223) | func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) {
  function TestManager_CreateToken_Only_Lower (line 276) | func TestManager_CreateToken_Only_Lower(t *testing.T) {
  function TestManager_UserManagement (line 291) | func TestManager_UserManagement(t *testing.T) {
  function TestManager_ChangePassword (line 384) | func TestManager_ChangePassword(t *testing.T) {
  function TestManager_ChangeRole (line 410) | func TestManager_ChangeRole(t *testing.T) {
  function TestManager_Reservations (line 437) | func TestManager_Reservations(t *testing.T) {
  function TestManager_ChangeRoleFromTierUserToAdmin (line 509) | func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
  function TestManager_Token_Valid (line 570) | func TestManager_Token_Valid(t *testing.T) {
  function TestManager_Token_Invalid (line 616) | func TestManager_Token_Invalid(t *testing.T) {
  function TestManager_Token_NotFound (line 631) | func TestManager_Token_NotFound(t *testing.T) {
  function TestManager_Token_Expire (line 639) | func TestManager_Token_Expire(t *testing.T) {
  function TestManager_Token_Extend (line 689) | func TestManager_Token_Extend(t *testing.T) {
  function TestManager_Token_MaxCount_AutoDelete (line 718) | func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
  function TestManager_EnqueueStats_ResetStats (line 790) | func TestManager_EnqueueStats_ResetStats(t *testing.T) {
  function TestManager_EnqueueTokenUpdate (line 838) | func TestManager_EnqueueTokenUpdate(t *testing.T) {
  function TestManager_ChangeSettings (line 877) | func TestManager_ChangeSettings(t *testing.T) {
  function TestManager_Tier_Create_Update_List_Delete (line 924) | func TestManager_Tier_Create_Update_List_Delete(t *testing.T) {
  function TestAccount_Tier_Create_With_ID (line 1044) | func TestAccount_Tier_Create_With_ID(t *testing.T) {
  function TestManager_Tier_Change_And_Reset (line 1059) | func TestManager_Tier_Change_And_Reset(t *testing.T) {
  function TestUser_PhoneNumberAddListRemove (line 1098) | func TestUser_PhoneNumberAddListRemove(t *testing.T) {
  function TestUser_PhoneNumberAdd_Multiple_Users_Same_Number (line 1125) | func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) {
  function TestManager_Topic_Wildcard_With_Asterisk_Underscore (line 1140) | func TestManager_Topic_Wildcard_With_Asterisk_Underscore(t *testing.T) {
  function TestManager_Topic_Wildcard_With_Underscore (line 1154) | func TestManager_Topic_Wildcard_With_Underscore(t *testing.T) {
  function TestManager_WithProvisionedUsers (line 1165) | func TestManager_WithProvisionedUsers(t *testing.T) {
  function TestManager_WithProvisionedUsers_RemoveToken (line 1318) | func TestManager_WithProvisionedUsers_RemoveToken(t *testing.T) {
  function TestManager_UpdateNonProvisionedUsersToProvisionedUsers (line 1365) | func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing....
  function TestManager_RemoveProvisionedOnEmptyConfig (line 1445) | func TestManager_RemoveProvisionedOnEmptyConfig(t *testing.T) {
  function TestToFromSQLWildcard (line 1493) | func TestToFromSQLWildcard(t *testing.T) {
  function TestMigrationFrom1 (line 1507) | func TestMigrationFrom1(t *testing.T) {
  function TestMigrationFrom4 (line 1592) | func TestMigrationFrom4(t *testing.T) {
  function checkSchemaVersion (line 1738) | func checkSchemaVersion(t *testing.T, d *db.DB) {
  function newTestManager (line 1749) | func newTestManager(t *testing.T, newManager newManagerFunc, defaultAcce...
  function newTestManagerFromFile (line 1759) | func newTestManagerFromFile(t *testing.T, filename, startupQueries strin...
  function newTestManagerFromConfig (line 1769) | func newTestManagerFromConfig(t *testing.T, newManager newManagerFunc, c...
  function testDB (line 1775) | func testDB(a *Manager) *db.DB {
  function forEachStoreBackend (line 1779) | func forEachStoreBackend(t *testing.T, f func(t *testing.T, manager *Man...
  function TestStoreAddUser (line 1794) | func TestStoreAddUser(t *testing.T) {
  function TestStoreAddUserAlreadyExists (line 1807) | func TestStoreAddUserAlreadyExists(t *testing.T) {
  function TestStoreRemoveUser (line 1814) | func TestStoreRemoveUser(t *testing.T) {
  function TestStoreUserByID (line 1827) | func TestStoreUserByID(t *testing.T) {
  function TestStoreUserByToken (line 1840) | func TestStoreUserByToken(t *testing.T) {
  function TestStoreUserByStripeCustomer (line 1856) | func TestStoreUserByStripeCustomer(t *testing.T) {
  function TestStoreUsers (line 1871) | func TestStoreUsers(t *testing.T) {
  function TestStoreUsersCount (line 1882) | func TestStoreUsersCount(t *testing.T) {
  function TestStoreChangePassword (line 1895) | func TestStoreChangePassword(t *testing.T) {
  function TestStoreChangeRole (line 1909) | func TestStoreChangeRole(t *testing.T) {
  function TestStoreTokens (line 1923) | func TestStoreTokens(t *testing.T) {
  function TestStoreTokenChange (line 1951) | func TestStoreTokenChange(t *testing.T) {
  function TestStoreTokenRemove (line 1970) | func TestStoreTokenRemove(t *testing.T) {
  function TestStoreTokenRemoveExpired (line 1985) | func TestStoreTokenRemoveExpired(t *testing.T) {
  function TestStoreTokenUpdateLastAccess (line 2010) | func TestStoreTokenUpdateLastAccess(t *testing.T) {
  function TestStoreAllowAccess (line 2025) | func TestStoreAllowAccess(t *testing.T) {
  function TestStoreAllowAccessReadOnly (line 2038) | func TestStoreAllowAccessReadOnly(t *testing.T) {
  function TestStoreResetAccess (line 2051) | func TestStoreResetAccess(t *testing.T) {
  function TestStoreResetAccessAll (line 2069) | func TestStoreResetAccessAll(t *testing.T) {
  function TestStoreAuthorizeTopicAccess (line 2082) | func TestStoreAuthorizeTopicAccess(t *testing.T) {
  function TestStoreAuthorizeTopicAccessNotFound (line 2095) | func TestStoreAuthorizeTopicAccessNotFound(t *testing.T) {
  function TestStoreAuthorizeTopicAccessDenyAll (line 2105) | func TestStoreAuthorizeTopicAccessDenyAll(t *testing.T) {
  function TestStoreReservations (line 2118) | func TestStoreReservations(t *testing.T) {
  function TestStoreReservationsCount (line 2133) | func TestStoreReservationsCount(t *testing.T) {
  function TestStoreHasReservation (line 2145) | func TestStoreHasReservation(t *testing.T) {
  function TestStoreReservationOwner (line 2160) | func TestStoreReservationOwner(t *testing.T) {
  function TestStoreAddReservationWithLimit (line 2175) | func TestStoreAddReservationWithLimit(t *testing.T) {
  function TestStoreTiers (line 2195) | func TestStoreTiers(t *testing.T) {
  function TestStoreTierUpdate (line 2232) | func TestStoreTierUpdate(t *testing.T) {
  function TestStoreTierRemove (line 2252) | func TestStoreTierRemove(t *testing.T) {
  function TestStoreTierByStripePrice (line 2271) | func TestStoreTierByStripePrice(t *testing.T) {
  function TestStoreChangeTier (line 2292) | func TestStoreChangeTier(t *testing.T) {
  function TestStorePhoneNumbers (line 2310) | func TestStorePhoneNumbers(t *testing.T) {
  function TestStoreChangeSettings (line 2331) | func TestStoreChangeSettings(t *testing.T) {
  function TestStoreChangeBilling (line 2348) | func TestStoreChangeBilling(t *testing.T) {
  function TestStoreUpdateStats (line 2365) | func TestStoreUpdateStats(t *testing.T) {
  function TestStoreResetStats (line 2382) | func TestStoreResetStats(t *testing.T) {
  function TestStoreMarkUserRemoved (line 2400) | func TestStoreMarkUserRemoved(t *testing.T) {
  function TestStoreRemoveDeletedUsers (line 2414) | func TestStoreRemoveDeletedUsers(t *testing.T) {
  function TestStoreAllGrants (line 2431) | func TestStoreAllGrants(t *testing.T) {
  function TestStoreOtherAccessCount (line 2450) | func TestStoreOtherAccessCount(t *testing.T) {

FILE: user/types.go
  type User (line 14) | type User struct
    method TierID (line 31) | func (u *User) TierID() string {
    method IsAdmin (line 39) | func (u *User) IsAdmin() bool {
    method IsUser (line 44) | func (u *User) IsUser() bool {
  type Auther (line 49) | type Auther interface
  type Token (line 61) | type Token struct
  type TokenUpdate (line 71) | type TokenUpdate struct
  type Prefs (line 77) | type Prefs struct
  type Tier (line 84) | type Tier struct
    method Context (line 102) | func (t *Tier) Context() log.Context {
  type Subscription (line 112) | type Subscription struct
    method Context (line 119) | func (s *Subscription) Context() log.Context {
  type NotificationPrefs (line 127) | type NotificationPrefs struct
  type Stats (line 134) | type Stats struct
  type Billing (line 141) | type Billing struct
  type Grant (line 151) | type Grant struct
  type Reservation (line 158) | type Reservation struct
  type Permission (line 165) | type Permission
    method IsRead (line 204) | func (p Permission) IsRead() bool {
    method IsWrite (line 209) | func (p Permission) IsWrite() bool {
    method IsReadWrite (line 214) | func (p Permission) IsReadWrite() bool {
    method String (line 219) | func (p Permission) String() string {
  constant PermissionDenyAll (line 169) | PermissionDenyAll Permission = iota
  constant PermissionRead (line 170) | PermissionRead
  constant PermissionWrite (line 171) | PermissionWrite
  constant PermissionReadWrite (line 172) | PermissionReadWrite
  function NewPermission (line 176) | func NewPermission(read, write bool) Permission {
  function ParsePermission (line 188) | func ParsePermission(s string) (Permission, error) {
  type Role (line 231) | type Role
  constant RoleAdmin (line 235) | RoleAdmin     = Role("admin")
  constant RoleUser (line 236) | RoleUser      = Role("user")
  constant RoleAnonymous (line 237) | RoleAnonymous = Role("anonymous")
  constant Everyone (line 242) | Everyone   = "*"
  constant everyoneID (line 243) | everyoneID = "u_everyone"
  type Config (line 247) | type Config struct
  type queries (line 279) | type queries struct

FILE: user/types_test.go
  function TestPermission (line 8) | func TestPermission(t *testing.T) {
  function TestParsePermission (line 20) | func TestParsePermission(t *testing.T) {
  function TestAllowedTier (line 45) | func TestAllowedTier(t *testing.T) {
  function TestTierContext (line 50) | func TestTierContext(t *testing.T) {
  function TestUsernameRegex (line 65) | func TestUsernameRegex(t *testing.T) {

FILE: user/util.go
  function AllowedRole (line 21) | func AllowedRole(role Role) bool {
  function AllowedUsername (line 26) | func AllowedUsername(username string) bool {
  function AllowedTopic (line 31) | func AllowedTopic(topic string) bool {
  function AllowedTopicPattern (line 36) | func AllowedTopicPattern(topic string) bool {
  function AllowedTier (line 41) | func AllowedTier(tier string) bool {
  function ValidPasswordHash (line 46) | func ValidPasswordHash(hash string, minCost int) error {
  function ValidToken (line 60) | func ValidToken(token string) bool {
  function GenerateToken (line 66) | func GenerateToken() string {
  function HashPassword (line 71) | func HashPassword(password string) (string, error) {
  function hashPassword (line 75) | func hashPassword(password string, cost int) (string, error) {
  function nullString (line 83) | func nullString(s string) sql.NullString {
  function nullInt64 (line 90) | func nullInt64(v int64) sql.NullInt64 {
  function toSQLWildcard (line 99) | func toSQLWildcard(s string) string {
  function fromSQLWildcard (line 105) | func fromSQLWildcard(s string) string {
  function escapeUnderscore (line 109) | func escapeUnderscore(s string) string {
  function unescapeUnderscore (line 113) | func unescapeUnderscore(s string) string {

FILE: user/util_test.go
  function TestAllowedRole (line 9) | func TestAllowedRole(t *testing.T) {
  function TestAllowedTopic (line 18) | func TestAllowedTopic(t *testing.T) {
  function TestAllowedTopicPattern (line 52) | func TestAllowedTopicPattern(t *testing.T) {
  function TestValidPasswordHash (line 90) | func TestValidPasswordHash(t *testing.T) {
  function TestValidToken (line 119) | func TestValidToken(t *testing.T) {
  function TestGenerateToken (line 150) | func TestGenerateToken(t *testing.T) {
  function TestHashPassword (line 175) | func TestHashPassword(t *testing.T) {
  function TestHashPassword_WithCost (line 201) | func TestHashPassword_WithCost(t *testing.T) {
  function TestUser_TierID (line 223) | func TestUser_TierID(t *testing.T) {
  function TestUser_IsAdmin (line 244) | func TestUser_IsAdmin(t *testing.T) {
  function TestUser_IsUser (line 260) | func TestUser_IsUser(t *testing.T) {
  function TestPermission_String (line 276) | func TestPermission_String(t *testing.T) {

FILE: util/batching_queue.go
  type BatchingQueue (line 26) | type BatchingQueue struct
  function NewBatchingQueue (line 35) | func NewBatchingQueue[T any](batchSize int, timeout time.Duration) *Batc...
  method Enqueue (line 48) | func (q *BatchingQueue[T]) Enqueue(element T) {
  method Dequeue (line 62) | func (q *BatchingQueue[T]) Dequeue() <-chan []T {
  method dequeueAll (line 66) | func (q *BatchingQueue[T]) dequeueAll() []T {
  method timeoutTicker (line 73) | func (q *BatchingQueue[T]) timeoutTicker() {

FILE: util/batching_queue_test.go
  function TestBatchingQueue_InfTimeout (line 12) | func TestBatchingQueue_InfTimeout(t *testing.T) {
  function TestBatchingQueue_WithTimeout (line 34) | func TestBatchingQueue_WithTimeout(t *testing.T) {

FILE: util/content_type_writer.go
  type ContentTypeWriter (line 13) | type ContentTypeWriter struct
    method Write (line 24) | func (w *ContentTypeWriter) Write(p []byte) (n int, err error) {
  function NewContentTypeWriter (line 20) | func NewContentTypeWriter(w http.ResponseWriter, filename string) *Conte...

FILE: util/content_type_writer_test.go
  function TestSniffWriter_WriteHTML (line 10) | func TestSniffWriter_WriteHTML(t *testing.T) {
  function TestSniffWriter_WriteTwoWriteCalls (line 17) | func TestSniffWriter_WriteTwoWriteCalls(t *testing.T) {
  function TestSniffWriter_NoSniffWriterWriteHTML (line 25) | func TestSniffWriter_NoSniffWriterWriteHTML(t *testing.T) {
  function TestSniffWriter_WriteHTMLSplitIntoTwoWrites (line 33) | func TestSniffWriter_WriteHTMLSplitIntoTwoWrites(t *testing.T) {
  function TestSniffWriter_WriteUnknownMimeType (line 43) | func TestSniffWriter_WriteUnknownMimeType(t *testing.T)
Condensed preview — 377 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,293K chars).
[
  {
    "path": ".dockerignore",
    "chars": 32,
    "preview": "dist\n*/node_modules\nDockerfile*\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "chars": 495,
    "preview": "# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 40,
    "preview": "github: [binwiederhier]\nliberapay: ntfy\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1_bug_report.md",
    "chars": 729,
    "preview": "---\nname: 🐛 Bug Report\nabout: Report any errors and problems\ntitle: ''\nlabels: '🪲 bug'\nassignees: ''\n\n---\n\n:lady_beetle:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2_enhancement_request.md",
    "chars": 641,
    "preview": "---\nname: 💡 Feature/Enhancement Request\nabout: Got a great idea? Let us know!\ntitle: ''\nlabels: 'enhancement'\nassignees:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/3_tech_support.md",
    "chars": 457,
    "preview": "---\nname: 🆘 I need help with ...\nabout: Installing ntfy, configuring the app, etc.\ntitle: ''\nlabels: 'tech-support'\nassi"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/4_question.md",
    "chars": 461,
    "preview": "---\nname: ❓ Question\nabout: Ask a question about ntfy\ntitle: ''\nlabels: 'question'\nassignees: ''\n\n---\n\n<!--\n\nBefore you "
  },
  {
    "path": ".github/workflows/build.yaml",
    "chars": 660,
    "preview": "name: build\non: [ push, pull_request ]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n"
  },
  {
    "path": ".github/workflows/docs.yaml",
    "chars": 1068,
    "preview": "name: docs\non:\n  push:\n    branches:\n      - main\njobs:\n  publish-docs:\n    runs-on: ubuntu-latest\n    steps:\n      -\n  "
  },
  {
    "path": ".github/workflows/release.yaml",
    "chars": 1398,
    "preview": "name: release\non:\n  push:\n    tags:\n      - 'v[0-9]+.[0-9]+.[0-9]+'\njobs:\n  release:\n    runs-on: ubuntu-latest\n    serv"
  },
  {
    "path": ".github/workflows/test.yaml",
    "chars": 1253,
    "preview": "name: test\non: [ push, pull_request ]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    services:\n      postgres:\n        ima"
  },
  {
    "path": ".gitignore",
    "chars": 240,
    "preview": "dist/\ndev-dist/\nbuild/\n.idea/\n.vscode/\n*.swp\nserver/docs/\nserver/site/\ntools/fbsend/fbsend\ntools/pgimport/pgimport\ntools"
  },
  {
    "path": ".gitpod.yml",
    "chars": 643,
    "preview": "tasks:\n  - name: docs\n    before: make docs-deps\n    command: mkdocs serve\n  - name: binary\n    before: |\n      npm inst"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 6066,
    "preview": "version: 2\nbefore:\n  hooks:\n    - go mod download\n    - go mod tidy\nbuilds:\n  - id: ntfy_linux_amd64\n    binary: ntfy\n  "
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5525,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "Dockerfile",
    "chars": 638,
    "preview": "FROM alpine\n\nLABEL org.opencontainers.image.authors=\"philipp.heckel@gmail.com\"\nLABEL org.opencontainers.image.url=\"https"
  },
  {
    "path": "Dockerfile-arm",
    "chars": 722,
    "preview": "FROM alpine\n\nLABEL org.opencontainers.image.authors=\"philipp.heckel@gmail.com\"\nLABEL org.opencontainers.image.url=\"https"
  },
  {
    "path": "Dockerfile-build",
    "chars": 1881,
    "preview": "FROM golang:1.24-bullseye as builder\n\nARG VERSION=dev\nARG COMMIT=unknown\nARG NODE_MAJOR=18\n\nRUN apt-get update && apt-ge"
  },
  {
    "path": "LICENSE",
    "chars": 11347,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "LICENSE.GPLv2",
    "chars": 18024,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
  },
  {
    "path": "Makefile",
    "chars": 13600,
    "preview": "MAKEFLAGS := --jobs=1\nNPM := npm\nPYTHON := python3\nPIP := pip3\nVERSION := $(shell git describe --tag)\nCOMMIT := $(shell "
  },
  {
    "path": "README.md",
    "chars": 24015,
    "preview": "<div align=\"center\" markdown=\"1\">\n<sup>Special thanks to:</sup>\n<br>\n<br>\n<a href=\"https://go.warp.dev/ntfy\">\n  <img alt"
  },
  {
    "path": "SECURITY.md",
    "chars": 436,
    "preview": "# Security Policy\n\n## Supported Versions\n\nAs of today, I only support the latest version of ntfy. Please make sure you s"
  },
  {
    "path": "client/client.go",
    "chars": 9139,
    "preview": "// Package client provides a ntfy client to publish and subscribe to topics\npackage client\n\nimport (\n\t\"bufio\"\n\t\"context\""
  },
  {
    "path": "client/client.yml",
    "chars": 2406,
    "preview": "# ntfy client config file\n\n# Base URL used to expand short topic names in the \"ntfy publish\" and \"ntfy subscribe\" comman"
  },
  {
    "path": "client/client_test.go",
    "chars": 3251,
    "preview": "package client_test\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel.io/ntfy/v2/client\"\n\t\"heckel.io/ntfy"
  },
  {
    "path": "client/config.go",
    "chars": 1627,
    "preview": "package client\n\nimport (\n\t\"gopkg.in/yaml.v2\"\n\t\"heckel.io/ntfy/v2/log\"\n\t\"os\"\n)\n\nconst (\n\t// DefaultBaseURL is the base UR"
  },
  {
    "path": "client/config_darwin.go",
    "chars": 335,
    "preview": "//go:build darwin\n\npackage client\n\nimport (\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n)\n\nfunc init() {\n\tu, err := user.Current()"
  },
  {
    "path": "client/config_test.go",
    "chars": 4731,
    "preview": "package client_test\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel.io/ntfy/v2/client\"\n\t\"os\"\n\t\"path/filepath\"\n"
  },
  {
    "path": "client/config_unix.go",
    "chars": 379,
    "preview": "//go:build linux || dragonfly || freebsd || netbsd || openbsd\n\npackage client\n\nimport (\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath"
  },
  {
    "path": "client/config_windows.go",
    "chars": 214,
    "preview": "//go:build windows\n\npackage client\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc init() {\n\tif configDir, err := os.UserConfig"
  },
  {
    "path": "client/ntfy-client.service",
    "chars": 219,
    "preview": "[Unit]\nDescription=ntfy client\nAfter=network.target\n\n[Service]\nUser=ntfy\nGroup=ntfy\nExecStart=/usr/bin/ntfy subscribe --"
  },
  {
    "path": "client/options.go",
    "chars": 7579,
    "preview": "package client\n\nimport (\n\t\"fmt\"\n\t\"heckel.io/ntfy/v2/util\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\n// RequestOption is a generi"
  },
  {
    "path": "client/user/ntfy-client.service",
    "chars": 203,
    "preview": "[Unit]\nDescription=ntfy client\nAfter=network.target\n\n[Service]\nExecStart=/usr/bin/ntfy subscribe --config \"%h/.config/nt"
  },
  {
    "path": "cmd/access.go",
    "chars": 8365,
    "preview": "//go:build !noserver\n\npackage cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/user\"\n\t\"hec"
  },
  {
    "path": "cmd/access_test.go",
    "chars": 2700,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/serv"
  },
  {
    "path": "cmd/app.go",
    "chars": 3412,
    "preview": "// Package cmd provides the ntfy CLI application\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\n\t\"github.com/urfave/cli/v"
  },
  {
    "path": "cmd/app_test.go",
    "chars": 702,
    "preview": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/client\"\n\t\"heckel.io/ntfy"
  },
  {
    "path": "cmd/config_loader.go",
    "chars": 1892,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"github.com/urfave/cli/v2\"\n\t\"github.com/urfave/cli/v2/altsrc\"\n\t\"gopkg.in/yaml.v2\"\n\t\"heckel"
  },
  {
    "path": "cmd/config_loader_test.go",
    "chars": 854,
    "preview": "package cmd\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestNewYamlSource"
  },
  {
    "path": "cmd/publish.go",
    "chars": 13362,
    "preview": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/client\"\n\t\"heckel.io/ntfy/v2/log\"\n"
  },
  {
    "path": "cmd/publish_test.go",
    "chars": 11611,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\""
  },
  {
    "path": "cmd/publish_unix.go",
    "chars": 205,
    "preview": "//go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd\n\npackage cmd\n\nimport \"syscall\"\n\nfunc processExis"
  },
  {
    "path": "cmd/publish_windows.go",
    "chars": 118,
    "preview": "package cmd\n\nimport (\n\t\"os\"\n)\n\nfunc processExists(pid int) bool {\n\t_, err := os.FindProcess(pid)\n\treturn err == nil\n}\n"
  },
  {
    "path": "cmd/serve.go",
    "chars": 45914,
    "preview": "//go:build !noserver\n\npackage cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"math\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"runtime\"\n"
  },
  {
    "path": "cmd/serve_test.go",
    "chars": 14613,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorilla/web"
  },
  {
    "path": "cmd/serve_unix.go",
    "chars": 1322,
    "preview": "//go:build linux || dragonfly || freebsd || netbsd || openbsd\n\npackage cmd\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"gi"
  },
  {
    "path": "cmd/serve_windows.go",
    "chars": 2673,
    "preview": "//go:build windows && !noserver\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"golang.org/x/sys/windows/svc\"\n\t\"heckel.io/ntfy/"
  },
  {
    "path": "cmd/subscribe.go",
    "chars": 11142,
    "preview": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy"
  },
  {
    "path": "cmd/subscribe_darwin.go",
    "chars": 373,
    "preview": "//go:build darwin\n\npackage cmd\n\nconst (\n\tscriptExt                      = \"sh\"\n\tscriptHeader                   = \"#!/bin"
  },
  {
    "path": "cmd/subscribe_test.go",
    "chars": 14963,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/require\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepa"
  },
  {
    "path": "cmd/subscribe_unix.go",
    "chars": 395,
    "preview": "//go:build linux || dragonfly || freebsd || netbsd || openbsd\n\npackage cmd\n\nconst (\n\tscriptExt                      = \"s"
  },
  {
    "path": "cmd/subscribe_windows.go",
    "chars": 293,
    "preview": "//go:build windows\n\npackage cmd\n\nconst (\n\tscriptExt                      = \"bat\"\n\tscriptHeader                   = \"\"\n\tc"
  },
  {
    "path": "cmd/tier.go",
    "chars": 14331,
    "preview": "//go:build !noserver\n\npackage cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/user\"\n\t\"hec"
  },
  {
    "path": "cmd/tier_test.go",
    "chars": 2442,
    "preview": "package cmd\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/server\"\n\t\"h"
  },
  {
    "path": "cmd/token.go",
    "chars": 7332,
    "preview": "//go:build !noserver\n\npackage cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/user\"\n\t\"hec"
  },
  {
    "path": "cmd/token_test.go",
    "chars": 1616,
    "preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/serv"
  },
  {
    "path": "cmd/user.go",
    "chars": 14600,
    "preview": "//go:build !noserver\n\npackage cmd\n\nimport (\n\t\"crypto/subtle\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/urfave/cli/"
  },
  {
    "path": "cmd/user_test.go",
    "chars": 4843,
    "preview": "package cmd\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/server\"\n\t\"h"
  },
  {
    "path": "cmd/webpush.go",
    "chars": 1728,
    "preview": "//go:build !noserver && !nowebpush\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/SherClockHolmes/webpush-go\"\n\t\"githu"
  },
  {
    "path": "cmd/webpush_test.go",
    "chars": 952,
    "preview": "package cmd\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n\t\""
  },
  {
    "path": "db/db.go",
    "chars": 3701,
    "preview": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"heckel.io/ntfy/v2/log\"\n)\n\nconst (\n\ttag        "
  },
  {
    "path": "db/pg/pg.go",
    "chars": 3235,
    "preview": "package pg\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t_ \"github.com/jackc/pgx/v5/stdlib"
  },
  {
    "path": "db/pg/pg_test.go",
    "chars": 1256,
    "preview": "package pg\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOpen_InvalidScheme(t *tes"
  },
  {
    "path": "db/test/test.go",
    "chars": 1778,
    "preview": "package dbtest\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel.io/ntfy/v2/"
  },
  {
    "path": "db/types.go",
    "chars": 580,
    "preview": "package db\n\nimport (\n\t\"database/sql\"\n\t\"sync/atomic\"\n)\n\n// Beginner is an interface for types that can begin a database t"
  },
  {
    "path": "db/util.go",
    "chars": 906,
    "preview": "package db\n\nimport \"database/sql\"\n\n// ExecTx executes a function within a database transaction. If the function returns "
  },
  {
    "path": "docker-compose.yml",
    "chars": 376,
    "preview": "services:\n  ntfy:\n    image: binwiederhier/ntfy\n    container_name: ntfy\n    command:\n      - serve\n    environment:\n   "
  },
  {
    "path": "docs/_overrides/main.html",
    "chars": 2209,
    "preview": "{% extends \"base.html\" %}\n\n{% block announce %}\n<style>\n    div[data-md-component=\"announce\"] {\n        z-index: 10;\n   "
  },
  {
    "path": "docs/config.md",
    "chars": 157750,
    "preview": "# Configuring the ntfy server\nThe ntfy server can be configured in three ways: using a config file (typically at `/etc/n"
  },
  {
    "path": "docs/contact.md",
    "chars": 3109,
    "preview": "# Contact\n\nThis service is run by [Philipp C. Heckel](https://heckel.io). There are several ways to get in touch with me"
  },
  {
    "path": "docs/contributing.md",
    "chars": 1568,
    "preview": "# Contributing\n\nThank you for your interest in contributing to ntfy! There are many ways to help, whether you're a devel"
  },
  {
    "path": "docs/deprecations.md",
    "chars": 2384,
    "preview": "# Deprecations and breaking changes\nThis page is used to list deprecation notices for ntfy. Deprecated commands and opti"
  },
  {
    "path": "docs/develop.md",
    "chars": 19736,
    "preview": "# Development\nHurray 🥳 🎉, you are interested in writing code for ntfy! **That's awesome.** 😎\n\nI tried my very best to wr"
  },
  {
    "path": "docs/emojis.md",
    "chars": 125611,
    "preview": "# Emoji reference\n\n<!-- This file was generated by scripts/emoji-convert.sh -->\n\nYou can [tag messages](publish.md#tags-"
  },
  {
    "path": "docs/examples.md",
    "chars": 22655,
    "preview": "# Examples\n\nThere are a million ways to use ntfy, but here are some inspirations. I try to collect\n<a href=\"https://gith"
  },
  {
    "path": "docs/faq.md",
    "chars": 6960,
    "preview": "# Frequently asked questions (FAQ)\n\n## Isn't this like ...?\nWho knows. I didn't do a lot of research before making this."
  },
  {
    "path": "docs/hooks.py",
    "chars": 169,
    "preview": "import os\nimport shutil\n\n\ndef on_post_build(config, **kwargs):\n    site_dir = config[\"site_dir\"]\n    shutil.copytree(\"do"
  },
  {
    "path": "docs/index.md",
    "chars": 3380,
    "preview": "# Getting started\nntfy lets you **send push notifications to your phone or desktop via scripts from any computer**, usin"
  },
  {
    "path": "docs/install.md",
    "chars": 23237,
    "preview": "# Installing ntfy\nThe `ntfy` CLI allows you to [publish messages](publish.md), [subscribe to topics](subscribe/cli.md) a"
  },
  {
    "path": "docs/integrations.md",
    "chars": 37106,
    "preview": "# Integrations + community projects\n\nThere are quite a few projects that work with ntfy, integrate ntfy, or have been bu"
  },
  {
    "path": "docs/known-issues.md",
    "chars": 2834,
    "preview": "# Known issues\nThis is an incomplete list of known issues with the ntfy server, web app, Android app, and iOS app. You c"
  },
  {
    "path": "docs/privacy.md",
    "chars": 8241,
    "preview": "# Privacy policy\n\n**Last updated:** January 2, 2026\n\nThis privacy policy describes how ntfy (\"we\", \"us\", or \"our\") colle"
  },
  {
    "path": "docs/publish/template-functions.md",
    "chars": 33266,
    "preview": "# Template Functions\n\nThese template functions may be used in the **[message template](../publish.md#message-templating)"
  },
  {
    "path": "docs/publish.md",
    "chars": 224384,
    "preview": "# Publishing\nPublishing messages can be done via HTTP PUT/POST or via the [ntfy CLI](subscribe/cli.md#publish-messages) "
  },
  {
    "path": "docs/releases.md",
    "chars": 123879,
    "preview": "# Release notes\nBinaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github"
  },
  {
    "path": "docs/static/css/config-generator.css",
    "chars": 16002,
    "preview": "/* Config Generator */\n\n/* Hidden utility */\n.cg-hidden {\n    display: none !important;\n}\n\n/* Open button */\n.cg-open-bt"
  },
  {
    "path": "docs/static/css/extra.css",
    "chars": 5076,
    "preview": ":root > * {\n    --md-primary-fg-color: #338574;\n    --md-primary-fg-color--light: #338574;\n    --md-primary-fg-color--da"
  },
  {
    "path": "docs/static/js/bcrypt.js",
    "chars": 47507,
    "preview": "// GENERATED FILE. DO NOT EDIT.\n(function (global, factory) {\n  function preferDefault(exports) {\n    return exports.def"
  },
  {
    "path": "docs/static/js/config-generator.js",
    "chars": 52643,
    "preview": "// Config Generator for ntfy\n//\n// Warning, AI code\n// ----------------\n// This code is entirely AI generated, but this "
  },
  {
    "path": "docs/static/js/extra.js",
    "chars": 3168,
    "preview": "// Link tabs, as per https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs\n\nconst savedCodeTa"
  },
  {
    "path": "docs/subscribe/api.md",
    "chars": 21910,
    "preview": "# Subscribe via API\nYou can create and subscribe to a topic in the [web UI](web.md), via the [phone app](phone.md), via "
  },
  {
    "path": "docs/subscribe/cli.md",
    "chars": 13181,
    "preview": "# Subscribe via ntfy CLI\nIn addition to subscribing via the [web UI](web.md), the [phone app](phone.md), or the [API](ap"
  },
  {
    "path": "docs/subscribe/phone.md",
    "chars": 26943,
    "preview": "# Subscribe from your phone\nYou can use the ntfy [Android App](https://play.google.com/store/apps/details?id=io.heckel.n"
  },
  {
    "path": "docs/subscribe/pwa.md",
    "chars": 4235,
    "preview": "# Using the progressive web app (PWA)\nWhile ntfy doesn't have a native desktop app, it is built as a [progressive web ap"
  },
  {
    "path": "docs/subscribe/web.md",
    "chars": 5047,
    "preview": "# Subscribe from the web app\nThe web app lets you subscribe and publish messages to ntfy topics. For ntfy.sh, the web ap"
  },
  {
    "path": "docs/terms.md",
    "chars": 13014,
    "preview": "# Terms of Service\n\n**Last updated:** January 26, 2026\n\nPlease read these Terms of Service (\"Terms\") carefully before us"
  },
  {
    "path": "docs/troubleshooting.md",
    "chars": 13084,
    "preview": "# Troubleshooting\nThis page lists a few suggestions of what to do when things don't work as expected. This is not a comp"
  },
  {
    "path": "examples/grafana-dashboard/ntfy-grafana.json",
    "chars": 57493,
    "preview": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"Prometheus\",\n      \"description\": \"\",\n      \"type"
  },
  {
    "path": "examples/linux-desktop-notifications/notify-desktop.sh",
    "chars": 321,
    "preview": "#!/bin/bash\n# This is an example shell script showing how to consume a ntfy.sh topic using\n# a simple script. The notify"
  },
  {
    "path": "examples/publish-go/main.go",
    "chars": 843,
    "preview": "package main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc main() {\n\t// Without additional headers (priority, tags, ti"
  },
  {
    "path": "examples/publish-php/publish.php",
    "chars": 509,
    "preview": "<?php\n\n// Check out https://ntfy.sh/phil_alerts in your browser after running this.\nfile_get_contents('https://ntfy.sh/p"
  },
  {
    "path": "examples/publish-python/publish.py",
    "chars": 290,
    "preview": "#!/usr/bin/env python3\n\nimport requests\n\nresp = requests.get(\"https://ntfy.sh/mytopic/trigger\",\n    data=\"Backup success"
  },
  {
    "path": "examples/ssh-login-alert/ntfy-ssh-login.sh",
    "chars": 354,
    "preview": "#!/bin/bash\n# This is a PAM script hook that shows how to notify you when\n# somebody logs into your server. Place at /us"
  },
  {
    "path": "examples/ssh-login-alert/pam_sshd",
    "chars": 260,
    "preview": "# PAM config file snippet\n#\n# Put this snippet AT THE END of the file /etc/pam.d/sshd\n# See https://geekthis.net/post/ru"
  },
  {
    "path": "examples/subscribe-go/main.go",
    "chars": 282,
    "preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc main() {\n\tresp, err := http.Get(\"https://ntfy.sh/phil_alerts/"
  },
  {
    "path": "examples/subscribe-php/subscribe.php",
    "chars": 205,
    "preview": "<?php\n\n$fp = fopen('https://ntfy.sh/phil_alerts/json', 'r');\nif (!$fp) {\n    die('cannot open stream');\n}\nwhile (!feof($"
  },
  {
    "path": "examples/subscribe-python/subscribe.py",
    "chars": 170,
    "preview": "#!/usr/bin/env python3\n\nimport requests\n\nresp = requests.get(\"https://ntfy.sh/mytopic/json\", stream=True)\nfor line in re"
  },
  {
    "path": "examples/web-example-eventsource/example-sse.html",
    "chars": 1906,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>ntfy.sh: EventSource Example</title>\n    <"
  },
  {
    "path": "examples/web-example-websocket/example-ws.html",
    "chars": 1881,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>ntfy.sh: WebSocket Example</title>\n    <me"
  },
  {
    "path": "go.mod",
    "chars": 4959,
    "preview": "module heckel.io/ntfy/v2\n\ngo 1.25.0\n\nrequire (\n\tcloud.google.com/go/firestore v1.21.0 // indirect\n\tcloud.google.com/go/s"
  },
  {
    "path": "go.sum",
    "chars": 27618,
    "preview": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ"
  },
  {
    "path": "log/event.go",
    "chars": 6235,
    "preview": "package log\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"heckel.io/ntfy/v2/util\"\n\t\"log\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst"
  },
  {
    "path": "log/log.go",
    "chars": 4934,
    "preview": "package log\n\nimport (\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Defaults for package level variables\nvar (\n\tDe"
  },
  {
    "path": "log/log_test.go",
    "chars": 9415,
    "preview": "package log\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"github.com/stretchr/testify/require\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepat"
  },
  {
    "path": "log/types.go",
    "chars": 2029,
    "preview": "package log\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n)\n\n// Level is a well-known log level, as defined below\ntype Level int"
  },
  {
    "path": "main.go",
    "chars": 1117,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/urfave/cli/v2\"\n\t\"heckel.io/ntfy/v2/cmd\"\n)\n\n// These variabl"
  },
  {
    "path": "message/cache.go",
    "chars": 15520,
    "preview": "package message\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"heckel.i"
  },
  {
    "path": "message/cache_postgres.go",
    "chars": 6910,
    "preview": "package message\n\nimport (\n\t\"time\"\n\n\t\"heckel.io/ntfy/v2/db\"\n)\n\n// PostgreSQL runtime query constants\nconst (\n\tpostgresIns"
  },
  {
    "path": "message/cache_postgres_schema.go",
    "chars": 2775,
    "preview": "package message\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"heckel.io/ntfy/v2/db\"\n)\n\n// Initial PostgreSQL schema\nconst (\n\tpostg"
  },
  {
    "path": "message/cache_sqlite.go",
    "chars": 8132,
    "preview": "package message\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t_ \"github.com/mattn/go-sqlite3\" // S"
  },
  {
    "path": "message/cache_sqlite_schema.go",
    "chars": 14392,
    "preview": "package message\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"heckel.io/ntfy/v2/db\"\n\t\"heckel.io/ntfy/v2/log\"\n)\n\n// Initial"
  },
  {
    "path": "message/cache_sqlite_test.go",
    "chars": 9016,
    "preview": "package message_test\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t_ \"github.com/mattn/go-sqlit"
  },
  {
    "path": "message/cache_test.go",
    "chars": 35183,
    "preview": "package message_test\n\nimport (\n\t\"net/netip\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/t"
  },
  {
    "path": "mkdocs.yml",
    "chars": 2888,
    "preview": "site_dir: server/docs\nsite_name: ntfy\nsite_url: https://ntfy.sh\nsite_description: Send push notifications to your phone "
  },
  {
    "path": "model/model.go",
    "chars": 7973,
    "preview": "package model\n\nimport (\n\t\"errors\"\n\t\"net/netip\"\n\t\"time\"\n\n\t\"heckel.io/ntfy/v2/log\"\n\t\"heckel.io/ntfy/v2/util\"\n)\n\n// List of"
  },
  {
    "path": "payments/payments.go",
    "chars": 656,
    "preview": "//go:build !nopayments\n\npackage payments\n\nimport \"github.com/stripe/stripe-go/v74\"\n\n// Available is a constant used to i"
  },
  {
    "path": "payments/payments_dummy.go",
    "chars": 433,
    "preview": "//go:build nopayments\n\npackage payments\n\n// Available is a constant used to indicate that Stripe support is available.\n/"
  },
  {
    "path": "requirements.txt",
    "chars": 99,
    "preview": "# The documentation uses 'mkdocs', which is written in Python\nmkdocs-material\nmkdocs-minify-plugin\n"
  },
  {
    "path": "scripts/emoji-convert.sh",
    "chars": 2196,
    "preview": "#!/bin/bash\n\n# This script reduces the size and converts the emoji.json file from https://github.com/github/gemoji/blob/"
  },
  {
    "path": "scripts/emoji.json",
    "chars": 392169,
    "preview": "[\n  {\n    \"emoji\": \"😀\"\n  , \"description\": \"grinning face\"\n  , \"category\": \"Smileys & Emotion\"\n  , \"aliases\": [\n      \"gr"
  },
  {
    "path": "scripts/postinst.sh",
    "chars": 1671,
    "preview": "#!/bin/sh\nset -e\n\n# Restart systemd service if it was already running. Note that \"deb-systemd-invoke try-restart\" will\n#"
  },
  {
    "path": "scripts/postrm.sh",
    "chars": 227,
    "preview": "#!/bin/sh\nset -e\n\n# Delete the config if package is purged\nif [ \"$1\" = \"purge\" ] || [ \"$1\" = \"0\" ]; then\n  id ntfy >/dev"
  },
  {
    "path": "scripts/preinst.sh",
    "chars": 325,
    "preview": "#!/bin/sh\nset -e\n\nif [ \"$1\" = \"install\" ] || [ \"$1\" = \"upgrade\" ] || [ \"$1\" -ge 1 ]; then\n  # Migration of old to new co"
  },
  {
    "path": "scripts/prerm.sh",
    "chars": 349,
    "preview": "#!/bin/sh\nset -e\n\n# Stop systemd service\nif [ -d /run/systemd/system ]; then\n  if [ \"$1\" = \"remove\" ] || [ \"$1\" = \"0\" ];"
  },
  {
    "path": "server/actions.go",
    "chars": 9612,
    "preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"heckel.io/ntfy/v2/mod"
  },
  {
    "path": "server/actions_test.go",
    "chars": 9439,
    "preview": "package server\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseActions(t *testing.T) {\n\tac"
  },
  {
    "path": "server/config.go",
    "chars": 16461,
    "preview": "package server\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/netip\"\n\t\"reflect\"\n\t\"text/template\"\n\t\"ti"
  },
  {
    "path": "server/config_test.go",
    "chars": 282,
    "preview": "package server_test\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"heckel.io/ntfy/v2/server\"\n\t\"testing\"\n)\n\nfunc TestC"
  },
  {
    "path": "server/config_unix.go",
    "chars": 141,
    "preview": "//go:build !windows\n\npackage server\n\nfunc init() {\n\tDefaultConfigFile = \"/etc/ntfy/server.yml\"\n\tDefaultTemplateDir = \"/e"
  },
  {
    "path": "server/config_windows.go",
    "chars": 328,
    "preview": "//go:build windows\n\npackage server\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc init() {\n\tprogramData := os.Getenv(\"ProgramD"
  },
  {
    "path": "server/errors.go",
    "chars": 15403,
    "preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"heckel.io/ntfy/v2/log\"\n)\n\n// errHTTP is a generic HTTP e"
  },
  {
    "path": "server/file_cache.go",
    "chars": 2820,
    "preview": "package server\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"heckel.io/ntfy/v2/log\"\n\t\"heckel.io/ntfy/v2/model\"\n\t\"heckel.io/ntfy/v2/util\"\n"
  },
  {
    "path": "server/file_cache_test.go",
    "chars": 2334,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel.io/ntfy/v2/util\"\n\t\"os\"\n\t\"string"
  },
  {
    "path": "server/log.go",
    "chars": 3632,
    "preview": "package server\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/emersion/go-smtp\"\n\t\"githu"
  },
  {
    "path": "server/mailer_emoji_map.json",
    "chars": 44115,
    "preview": "{\n    \"+1\": \"👍\",\n    \"-1\": \"👎\",\n    \"100\": \"💯\",\n    \"1234\": \"🔢\",\n    \"1st_place_medal\": \"🥇\",\n    \"2nd_place_medal\": \"🥈\","
  },
  {
    "path": "server/ntfy.service",
    "chars": 288,
    "preview": "[Unit]\nDescription=ntfy server\nAfter=network.target\n\n[Service]\nUser=ntfy\nGroup=ntfy\nExecStart=/usr/bin/ntfy serve --no-l"
  },
  {
    "path": "server/server.go",
    "chars": 89331,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"embed\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"f"
  },
  {
    "path": "server/server.yml",
    "chars": 22819,
    "preview": "# ntfy server config file\n#\n# Please refer to the documentation at https://ntfy.sh/docs/config/ for details.\n# All optio"
  },
  {
    "path": "server/server_account.go",
    "chars": 21229,
    "preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"time\"\n\n\t\"heckel.io/ntfy/v2/log"
  },
  {
    "path": "server/server_account_test.go",
    "chars": 31564,
    "preview": "package server\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel.io/ntfy/v2/log\"\n\t\"heckel.io/ntfy/v2/mode"
  },
  {
    "path": "server/server_admin.go",
    "chars": 5917,
    "preview": "package server\n\nimport (\n\t\"errors\"\n\t\"heckel.io/ntfy/v2/user\"\n\t\"net/http\"\n)\n\nfunc (s *Server) handleVersion(w http.Respon"
  },
  {
    "path": "server/server_admin_test.go",
    "chars": 15855,
    "preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel.io/ntfy/v2/user\"\n\t\"heckel.io/n"
  },
  {
    "path": "server/server_firebase.go",
    "chars": 10725,
    "preview": "//go:build !nofirebase\n\npackage server\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\tfirebase \"firebase.google.com/go"
  },
  {
    "path": "server/server_firebase_dummy.go",
    "chars": 871,
    "preview": "//go:build nofirebase\n\npackage server\n\nimport (\n\t\"errors\"\n\t\"heckel.io/ntfy/v2/model\"\n\t\"heckel.io/ntfy/v2/user\"\n)\n\nconst "
  },
  {
    "path": "server/server_firebase_test.go",
    "chars": 10964,
    "preview": "//go:build !nofirebase\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"heckel.io/ntfy/v2/model\"\n\t\"heckel.i"
  },
  {
    "path": "server/server_manager.go",
    "chars": 5341,
    "preview": "package server\n\nimport (\n\t\"heckel.io/ntfy/v2/log\"\n\t\"heckel.io/ntfy/v2/util\"\n\t\"strings\"\n)\n\nfunc (s *Server) execManager()"
  },
  {
    "path": "server/server_manager_test.go",
    "chars": 810,
    "preview": "package server\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel.io/ntfy/v2/model\"\n\t\"testing\"\n)\n\nfunc TestServer"
  },
  {
    "path": "server/server_matrix.go",
    "chars": 7062,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"heckel.io/ntfy/v2/util\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\""
  },
  {
    "path": "server/server_matrix_test.go",
    "chars": 4891,
    "preview": "package server\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n"
  },
  {
    "path": "server/server_metrics.go",
    "chars": 4671,
    "preview": "package server\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar (\n\tmetricMessagesPublishedSuccess     "
  },
  {
    "path": "server/server_middleware.go",
    "chars": 3490,
    "preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"heckel.io/ntfy/v2/util\"\n)\n\ntype contextKey int\n\nconst (\n\tcontextRateVisitor cont"
  },
  {
    "path": "server/server_payments.go",
    "chars": 22305,
    "preview": "//go:build !nopayments\n\npackage server\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/stripe/stripe-go/v74\"\n\tportalses"
  },
  {
    "path": "server/server_payments_dummy.go",
    "chars": 1264,
    "preview": "//go:build nopayments\n\npackage server\n\nimport (\n\t\"net/http\"\n)\n\ntype stripeAPI interface {\n\tCancelSubscription(id string)"
  },
  {
    "path": "server/server_payments_test.go",
    "chars": 31108,
    "preview": "//go:build !nopayments\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stret"
  },
  {
    "path": "server/server_race_off_test.go",
    "chars": 60,
    "preview": "//go:build !race\n\npackage server\n\nconst raceEnabled = false\n"
  },
  {
    "path": "server/server_race_on_test.go",
    "chars": 58,
    "preview": "//go:build race\n\npackage server\n\nconst raceEnabled = true\n"
  },
  {
    "path": "server/server_test.go",
    "chars": 218546,
    "preview": "package server\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/rand\"\n\t_ \"embed\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"f"
  },
  {
    "path": "server/server_twilio.go",
    "chars": 7410,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"hec"
  },
  {
    "path": "server/server_twilio_test.go",
    "chars": 14396,
    "preview": "package server\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"text/template\"\n\n\t\"github.com"
  },
  {
    "path": "server/server_webpush.go",
    "chars": 6258,
    "preview": "//go:build !nowebpush\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/S"
  },
  {
    "path": "server/server_webpush_dummy.go",
    "chars": 701,
    "preview": "//go:build nowebpush\n\npackage server\n\nimport (\n\t\"net/http\"\n\n\t\"heckel.io/ntfy/v2/model\"\n)\n\nconst (\n\t// WebPushAvailable i"
  },
  {
    "path": "server/server_webpush_test.go",
    "chars": 11027,
    "preview": "//go:build !nowebpush\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/net"
  },
  {
    "path": "server/smtp_sender.go",
    "chars": 4001,
    "preview": "package server\n\nimport (\n\t_ \"embed\" // required by go:embed\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"mime\"\n\t\"net\"\n\t\"net/smtp\"\n\t\"strings"
  },
  {
    "path": "server/smtp_sender_test.go",
    "chars": 4404,
    "preview": "package server\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel.io/ntfy/v2/model\"\n)\n\nfunc TestForma"
  },
  {
    "path": "server/smtp_server.go",
    "chars": 10044,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"mime/quotedprint"
  },
  {
    "path": "server/smtp_server_test.go",
    "chars": 70003,
    "preview": "package server\n\nimport (\n\t\"bufio\"\n\t\"github.com/emersion/go-smtp\"\n\t\"github.com/stretchr/testify/require\"\n\t\"io\"\n\t\"net\"\n\t\"n"
  },
  {
    "path": "server/templates/alertmanager.yml",
    "chars": 763,
    "preview": "title: |\n  {{- if eq .status \"firing\" }}\n  🚨 Alert: {{ (first .alerts).labels.alertname }}\n  {{- else if eq .status \"res"
  },
  {
    "path": "server/templates/github.yml",
    "chars": 1943,
    "preview": "title: |\n  {{- if and .starred_at (eq .action \"created\")}}\n  ⭐ {{ .sender.login }} starred {{ .repository.name }}\n  \n  {"
  },
  {
    "path": "server/templates/grafana.yml",
    "chars": 285,
    "preview": "title: |\n  {{- if eq .status \"firing\" }}\n  🚨 {{ .title | default \"Alert firing\" }}\n  {{- else if eq .status \"resolved\" }"
  },
  {
    "path": "server/testdata/webhook_alertmanager_firing.json",
    "chars": 747,
    "preview": "{\n  \"version\": \"4\",\n  \"groupKey\": \"...\",\n  \"status\": \"firing\",\n  \"receiver\": \"webhook-receiver\",\n  \"groupLabels\": {\n    "
  },
  {
    "path": "server/testdata/webhook_github_comment_created.json",
    "chars": 13309,
    "preview": "{\n  \"action\": \"created\",\n  \"issue\": {\n    \"url\": \"https://api.github.com/repos/binwiederhier/ntfy/issues/1389\",\n    \"rep"
  },
  {
    "path": "server/testdata/webhook_github_issue_opened.json",
    "chars": 10926,
    "preview": "{\n  \"action\": \"opened\",\n  \"issue\": {\n    \"url\": \"https://api.github.com/repos/binwiederhier/ntfy/issues/1391\",\n    \"repo"
  },
  {
    "path": "server/testdata/webhook_github_pr_opened.json",
    "chars": 27631,
    "preview": "{\n  \"action\": \"opened\",\n  \"number\": 1390,\n  \"pull_request\": {\n    \"url\": \"https://api.github.com/repos/binwiederhier/ntf"
  },
  {
    "path": "server/testdata/webhook_github_star_created.json",
    "chars": 7115,
    "preview": "{\n  \"action\": \"created\",\n  \"starred_at\": \"2025-07-16T12:57:43Z\",\n  \"repository\": {\n    \"id\": 420503947,\n    \"node_id\": \""
  },
  {
    "path": "server/testdata/webhook_github_watch_created.json",
    "chars": 7074,
    "preview": "{\n  \"action\": \"started\",\n  \"repository\": {\n    \"id\": 420503947,\n    \"node_id\": \"R_kgDOGRBhiw\",\n    \"name\": \"ntfy\",\n    \""
  },
  {
    "path": "server/testdata/webhook_grafana_resolved.json",
    "chars": 1997,
    "preview": "{\n  \"receiver\": \"ntfy\\\\.example\\\\.com/alerts\",\n  \"status\": \"resolved\",\n  \"alerts\": [\n    {\n      \"status\": \"resolved\",\n "
  },
  {
    "path": "server/topic.go",
    "chars": 5391,
    "preview": "package server\n\nimport (\n\t\"math/rand\"\n\t\"sync\"\n\t\"time\"\n\n\t\"heckel.io/ntfy/v2/log\"\n\t\"heckel.io/ntfy/v2/model\"\n\t\"heckel.io/n"
  },
  {
    "path": "server/topic_test.go",
    "chars": 2047,
    "preview": "package server\n\nimport (\n\t\"math/rand\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"heckel"
  },
  {
    "path": "server/types.go",
    "chars": 13712,
    "preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"heckel.io/ntfy/v2/model\"\n\t\"heckel.io/ntfy/v2/user\"\n\t\"heckel.io/ntfy/v2/util\"\n)\n\n"
  },
  {
    "path": "server/util.go",
    "chars": 6767,
    "preview": "package server\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"heck"
  },
  {
    "path": "server/util_test.go",
    "chars": 6835,
    "preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"heckel.io/ntfy/v2/user\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"strings\"\n\t\""
  },
  {
    "path": "server/visitor.go",
    "chars": 18552,
    "preview": "package server\n\nimport (\n\t\"fmt\"\n\t\"net/netip\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/time/rate\"\n\t\"heckel.io/ntfy/v2/log\"\n\t\"hecke"
  },
  {
    "path": "test/server.go",
    "chars": 1350,
    "preview": "package test\n\nimport (\n\t\"fmt\"\n\t\"heckel.io/ntfy/v2/server\"\n\t\"net\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\n// StartServ"
  },
  {
    "path": "test/test.go",
    "chars": 138,
    "preview": "// Package test provides test helpers for unit and integration tests.\n// This code is not meant to be used outside of te"
  },
  {
    "path": "test/util.go",
    "chars": 1050,
    "preview": "package test\n\nimport (\n\t\"net\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n)\n\n// WaitForPortUp waits up to 7s for a port to come up and"
  },
  {
    "path": "tools/fbsend/README.md",
    "chars": 94,
    "preview": "# fbsend\nfbsend is a tiny tool to send data messages to Firebase. It's only used for testing.\n"
  },
  {
    "path": "tools/fbsend/main.go",
    "chars": 1117,
    "preview": "//go:build !nofirebase\n\npackage main\n\nimport (\n\t\"context\"\n\tfirebase \"firebase.google.com/go/v4\"\n\t\"firebase.google.com/go"
  },
  {
    "path": "tools/loadgen/main.go",
    "chars": 2080,
    "preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\tbaseURL := \"https://stagin"
  },
  {
    "path": "tools/loadtest/go.mod",
    "chars": 72,
    "preview": "module loadtest\n\ngo 1.25.2\n\nrequire github.com/gorilla/websocket v1.5.3\n"
  }
]

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

About this extraction

This page contains the full source code of the binwiederhier/ntfy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 377 files (4.7 MB), approximately 1.2M tokens, and a symbol index with 2715 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!