Compare commits

..

1 Commits

Author SHA1 Message Date
Philipp Heckel
3baa1a9fd0 Electron WIP, nothing works... 2022-05-05 20:49:48 -04:00
234 changed files with 23461 additions and 35190 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
github: [binwiederhier]
liberapay: ntfy

View File

@@ -1,26 +0,0 @@
---
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. -->

View File

@@ -1,26 +0,0 @@
---
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 -->

View File

@@ -1,21 +0,0 @@
---
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
-->

View File

@@ -1,21 +0,0 @@
---
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 :) -->

View File

@@ -1,39 +0,0 @@
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Install Go
uses: actions/setup-go@v2
with:
go-version: '1.19.x'
-
name: Install node
uses: actions/setup-node@v2
with:
node-version: '18'
-
name: Checkout code
uses: actions/checkout@v2
-
name: Cache Go and npm modules
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
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

View File

@@ -1,36 +0,0 @@
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 "<>"
git add docs/
git commit -m "Updated docs"
git push origin main

View File

@@ -1,50 +0,0 @@
name: release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
release:
runs-on: ubuntu-latest
steps:
-
name: Install Go
uses: actions/setup-go@v2
with:
go-version: '1.19.x'
-
name: Install node
uses: actions/setup-node@v2
with:
node-version: '18'
-
name: Checkout code
uses: actions/checkout@v2
-
name: Cache Go and npm modules
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
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

View File

@@ -4,45 +4,25 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
-
name: Install Go
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: '1.19.x'
-
name: Install node
go-version: '1.17.x'
- name: Install node
uses: actions/setup-node@v2
with:
node-version: '18'
-
name: Checkout code
node-version: '16'
- name: Checkout code
uses: actions/checkout@v2
-
name: Cache Go and npm modules
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
name: Install dependencies
run: make build-deps-ubuntu
-
name: Build docs (required for tests)
- name: Install dependencies
run: sudo apt update && sudo apt install -y python3-pip curl
- name: Build docs (required for tests)
run: make docs
-
name: Build web app (required for tests)
- name: Build web app (required for tests)
run: make web
-
name: Run tests, formatting, vetting and linting
- name: Run tests, formatting, vetting and linting
run: make check
-
name: Run coverage
- name: Run coverage
run: make coverage
-
name: Upload coverage to codecov.io
- name: Upload coverage to codecov.io
run: make coverage-upload

5
.gitignore vendored
View File

@@ -1,14 +1,9 @@
dist/
build/
.idea/
.vscode/
*.swp
server/docs/
server/site/
tools/fbsend/fbsend
playground/
secrets/
*.iml
node_modules/
.DS_Store
__pycache__

View File

@@ -1,28 +0,0 @@
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

View File

@@ -4,7 +4,7 @@ before:
- go mod tidy
builds:
-
id: ntfy_linux_amd64
id: ntfy_amd64
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
@@ -13,8 +13,11 @@ builds:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [linux]
goarch: [amd64]
hooks:
post:
- upx "{{ .Path }}" # apt install upx
-
id: ntfy_linux_armv6
id: ntfy_armv6
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
@@ -25,8 +28,10 @@ builds:
goos: [linux]
goarch: [arm]
goarm: [6]
# No "upx", since it causes random core dumps, see
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
-
id: ntfy_linux_armv7
id: ntfy_armv7
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
@@ -37,8 +42,10 @@ builds:
goos: [linux]
goarch: [arm]
goarm: [7]
# No "upx", since it causes random core dumps, see
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
-
id: ntfy_linux_arm64
id: ntfy_arm64
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
@@ -48,26 +55,8 @@ builds:
- "-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=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: [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)
# No "upx", since it causes random core dumps, see
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
nfpms:
-
package_name: ntfy
@@ -105,12 +94,6 @@ nfpms:
postremove: "scripts/postrm.sh"
archives:
-
id: ntfy_linux
builds:
- ntfy_linux_amd64
- ntfy_linux_armv6
- ntfy_linux_armv7
- ntfy_linux_arm64
wrap_in_directory: true
files:
- LICENSE
@@ -120,35 +103,8 @@ archives:
- client/client.yml
- client/ntfy-client.service
replacements:
386: i386
amd64: x86_64
-
id: ntfy_windows
builds:
- ntfy_windows_amd64
format: zip
wrap_in_directory: true
files:
- LICENSE
- README.md
- client/client.yml
replacements:
amd64: x86_64
-
id: ntfy_darwin
builds:
- ntfy_darwin_all
wrap_in_directory: true
files:
- LICENSE
- README.md
- client/client.yml
replacements:
darwin: macOS
universal_binaries:
-
id: ntfy_darwin_all
replace: true
name_template: ntfy
checksum:
name_template: 'checksums.txt'
snapshot:

View File

@@ -1,133 +0,0 @@
# 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

View File

@@ -1,13 +1,5 @@
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"
MAINTAINER Philipp C. Heckel <philipp.heckel@gmail.com>
COPY ntfy /usr/bin

263
Makefile
View File

@@ -1,73 +1,64 @@
MAKEFLAGS := --jobs=1
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 " make build - Build web app, documentation and server/client (sloowwww)"
@echo " make server-amd64 - Build server/client binary (amd64, no web app or docs)"
@echo " make install-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 " 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-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
@echo "Build server & client (not release version):"
@echo " make server - Build server & client (all architectures)"
@echo " make server-amd64 - Build server & client (amd64 only)"
@echo " make server-armv6 - Build server & client (armv6 only)"
@echo " make server-armv7 - Build server & client (armv7 only)"
@echo " make server-arm64 - Build server & client (arm64 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 - 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
@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 " 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 " 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 " 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 " 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)"
@echo " make install-amd64 - Copy amd64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-armv6 - Copy armv6 binary from dist/ to /usr/bin/ntfy"
@echo " make install-armv7 - Copy armv7 binary from dist/ to /usr/bin/ntfy"
@echo " make install-arm64 - Copy arm64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-deb-amd64 - Install .deb from dist/ (amd64 only)"
@echo " make install-deb-armv6 - Install .deb from dist/ (armv6 only)"
@echo " make install-deb-armv7 - Install .deb from dist/ (armv7 only)"
@echo " make install-deb-arm64 - Install .deb from dist/ (arm64 only)"
# Building everything
@@ -75,51 +66,28 @@ help:
clean: .PHONY
rm -rf dist build server/docs server/site
build: web docs cli
build: web docs server
update: web-deps-update cli-deps-update docs-deps-update
docker pull alpine
# Ubuntu-specific
build-deps-ubuntu:
sudo apt update
sudo apt install -y \
curl \
gcc-aarch64-linux-gnu \
gcc-arm-linux-gnueabi \
jq
which pip3 || sudo apt install -y python3-pip
# Documentation
docs: docs-deps docs-build
docs-build: .PHONY
@if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
if which python3.8; then \
echo "python3.8 $(shell which mkdocs) build"; \
python3.8 $(shell which mkdocs) build; \
else \
echo "ERROR: Python version too low. mkdocs-material needs >= 3.8"; \
exit 1; \
fi; \
else \
echo "mkdocs build"; \
mkdocs build; \
fi
docs-deps: .PHONY
pip3 install -r requirements.txt
docs-deps-update: .PHONY
pip3 install -r requirements.txt --upgrade
docs-build: .PHONY
mkdocs build
# Web app
web: web-deps web-build
web-deps:
cd web && npm install
# If this fails for .svg files, optimizes them with svgo
web-build:
cd web \
&& npm run build \
@@ -130,116 +98,55 @@ web-build:
../server/site/config.js \
../server/site/asset-manifest.json
web-deps:
cd web && npm install
# If this fails for .svg files, optimize them with svgo
web-deps-update:
cd web && npm update
# Main server/client build
cli: cli-deps
goreleaser build --snapshot --clean
server: server-deps
goreleaser build --snapshot --rm-dist --debug
cli-linux-amd64: cli-deps-static-sites
goreleaser build --snapshot --clean --id ntfy_linux_amd64
server-amd64: server-deps-static-sites
goreleaser build --snapshot --rm-dist --debug --id ntfy_amd64
cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
goreleaser build --snapshot --clean --id ntfy_linux_armv6
server-armv6: server-deps-static-sites server-deps-gcc-armv6-armv7
goreleaser build --snapshot --rm-dist --debug --id ntfy_armv6
cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
goreleaser build --snapshot --clean --id ntfy_linux_armv7
server-armv7: server-deps-static-sites server-deps-gcc-armv6-armv7
goreleaser build --snapshot --rm-dist --debug --id ntfy_armv7
cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
goreleaser build --snapshot --clean --id ntfy_linux_arm64
server-arm64: server-deps-static-sites server-deps-gcc-arm64
goreleaser build --snapshot --rm-dist --debug --id ntfy_arm64
cli-windows-amd64: cli-deps-static-sites
goreleaser build --snapshot --clean --id ntfy_windows_amd64
server-deps: server-deps-static-sites server-deps-all server-deps-gcc
cli-darwin-all: cli-deps-static-sites
goreleaser build --snapshot --clean --id ntfy_darwin_all
server-deps-gcc: server-deps-gcc-armv6-armv7 server-deps-gcc-arm64
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-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-static-sites:
server-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@latest
server-deps-all:
which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; }
cli-deps-gcc-armv6-armv7:
server-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:
server-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-update:
go get -u
go install honnef.co/go/tools/cmd/staticcheck@latest
go install golang.org/x/lint/golint@latest
go install github.com/goreleaser/goreleaser@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 fmt-check vet lint staticcheck
test: .PHONY
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
testv: .PHONY
go test -v $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
race: .PHONY
go test -v -race $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
go test -race $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
coverage:
mkdir -p build/coverage
go test -v -race -coverprofile=build/coverage/coverage.txt -covermode=atomic $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
go test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
go tool cover -func build/coverage/coverage.txt
coverage-html:
@@ -277,13 +184,13 @@ staticcheck: .PHONY
# Releasing targets
release: clean cli-deps release-checks docs web check
goreleaser release --clean
release: clean server-deps release-check-tags docs web check
goreleaser release --rm-dist --debug
release-snapshot: clean cli-deps docs web check
goreleaser release --snapshot --skip-publish --clean
release-snapshot: clean server-deps docs web check
goreleaser release --snapshot --skip-publish --rm-dist --debug
release-checks:
release-check-tags:
$(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.";\
@@ -293,39 +200,35 @@ release-checks:
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-amd64: remove-binary
sudo cp -a dist/ntfy_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-armv6: remove-binary
sudo cp -a dist/ntfy_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-armv7: remove-binary
sudo cp -a dist/ntfy_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
install-arm64: remove-binary
sudo cp -a dist/ntfy_arm64_linux_arm64/ntfy /usr/bin/ntfy
remove-binary:
sudo rm -f /usr/bin/ntfy
install-linux-amd64-deb: purge-package
install-amd64-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_amd64.deb
install-linux-armv6-deb: purge-package
install-armv6-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_armv6.deb
install-linux-armv7-deb: purge-package
install-armv7-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_armv7.deb
install-linux-arm64-deb: purge-package
install-arm64-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_arm64.deb
purge-package:

121
README.md
View File

@@ -8,19 +8,14 @@
[![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)
[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/ntfy?color=%23317f6f&label=-%20r%2Fntfy&style=social)](https://www.reddit.com/r/ntfy/)
[![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.
**ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service.
It allows you to **send notifications to your phone or desktop via scripts** from any computer, entirely **without signup or cost**.
It's also open source (as you can plainly see) if you want to run your own.
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).
I run a free version of it at **[ntfy.sh](https://ntfy.sh)**, and there's an [open source](https://github.com/binwiederhier/ntfy-android) [Android app](https://play.google.com/store/apps/details?id=io.heckel.ntfy)
too.
<p>
<img src="web/public/static/img/screenshot-curl.png" height="180">
@@ -38,117 +33,27 @@ as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) a
[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
* [Reddit r/ntfy](https://www.reddit.com/r/ntfy/) - asynchronous forum (_new as of October 2022_)
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
* [Email](https://heckel.io/about) - reach me directly (_I usually prefer the other methods_)
## 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).
## Contributing
I welcome any and all 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
I welcome any and all contributions. Just create a PR or an issue. 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>
## Sponsors
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks already 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>
I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free,
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
<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>
## 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)._
## Contact me
You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org)
(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information
[on my website](https://heckel.io/about).
## 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
* [github.com/urfave/cli/v2](https://github.com/urfave/cli/v2) (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

View File

@@ -1,10 +0,0 @@
# 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 severe security issues privately via ntfy@heckel.io, [Discord](https://discord.gg/cT7ECsZj9w),
or [Matrix](https://matrix.to/#/#ntfy:matrix.org) (my username is `binwiederhier`).

122
auth/auth.go Normal file
View File

@@ -0,0 +1,122 @@
// Package auth deals with authentication and authorization against topics
package auth
import (
"errors"
"regexp"
)
// Auther is a generic interface to implement password-based authentication and authorization
type Auther interface {
// Authenticate checks username and password and returns a user if correct. The method
// returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect.
Authenticate(username, password string) (*User, error)
// Authorize returns nil if the given user has access to the given topic using the desired
// permission. The user param may be nil to signal an anonymous user.
Authorize(user *User, topic string, perm Permission) error
}
// Manager is an interface representing user and access management
type Manager interface {
// AddUser adds a user with the given username, password and role. The password should be hashed
// before it is stored in a persistence layer.
AddUser(username, password string, role Role) error
// RemoveUser deletes the user with the given username. The function returns nil on success, even
// if the user did not exist in the first place.
RemoveUser(username string) error
// Users returns a list of users. It always also returns the Everyone user ("*").
Users() ([]*User, error)
// User returns the user with the given username if it exists, or ErrNotFound otherwise.
// You may also pass Everyone to retrieve the anonymous user and its Grant list.
User(username string) (*User, error)
// ChangePassword changes a user's password
ChangePassword(username, password string) error
// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
// all existing access control entries (Grant) are removed, since they are no longer needed.
ChangeRole(username string, role Role) error
// AllowAccess adds or updates an entry in th access control list for a specific user. It controls
// read/write access to a topic. The parameter topicPattern may include wildcards (*).
AllowAccess(username string, topicPattern string, read bool, write bool) error
// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
// empty) for an entire user. The parameter topicPattern may include wildcards (*).
ResetAccess(username string, topicPattern string) error
// DefaultAccess returns the default read/write access if no access control entry matches
DefaultAccess() (read bool, write bool)
}
// User is a struct that represents a user
type User struct {
Name string
Hash string // password hash (bcrypt)
Role Role
Grants []Grant
}
// Grant is a struct that represents an access control entry to a topic
type Grant struct {
TopicPattern string // May include wildcard (*)
AllowRead bool
AllowWrite bool
}
// Permission represents a read or write permission to a topic
type Permission int
// Permissions to a topic
const (
PermissionRead = Permission(1)
PermissionWrite = Permission(2)
)
// Role represents a user's role, either admin or regular user
type Role string
// User roles
const (
RoleAdmin = Role("admin")
RoleUser = Role("user")
RoleAnonymous = Role("anonymous")
)
// Everyone is a special username representing anonymous users
const (
Everyone = "*"
)
var (
allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`) // Does not include Everyone (*)
allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
)
// AllowedRole returns true if the given role can be used for new users
func AllowedRole(role Role) bool {
return role == RoleUser || role == RoleAdmin
}
// AllowedUsername returns true if the given username is valid
func AllowedUsername(username string) bool {
return allowedUsernameRegex.MatchString(username)
}
// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
func AllowedTopicPattern(username string) bool {
return allowedTopicPatternRegex.MatchString(username)
}
// Error constants used by the package
var (
ErrUnauthenticated = errors.New("unauthenticated")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidArgument = errors.New("invalid argument")
ErrNotFound = errors.New("not found")
)

399
auth/auth_sqlite.go Normal file
View File

@@ -0,0 +1,399 @@
package auth
import (
"database/sql"
"errors"
"fmt"
_ "github.com/mattn/go-sqlite3" // SQLite driver
"golang.org/x/crypto/bcrypt"
"strings"
)
const (
bcryptCost = 10
intentionalSlowDownHash = "$2a$10$YFCQvqQDwIIwnJM1xkAYOeih0dg17UVGanaTStnrSzC8NCWxcLDwy" // Cost should match bcryptCost
)
// Auther-related queries
const (
createAuthTablesQueries = `
BEGIN;
CREATE TABLE IF NOT EXISTS user (
user TEXT NOT NULL PRIMARY KEY,
pass TEXT NOT NULL,
role TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS access (
user TEXT NOT NULL,
topic TEXT NOT NULL,
read INT NOT NULL,
write INT NOT NULL,
PRIMARY KEY (topic, user)
);
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
version INT NOT NULL
);
COMMIT;
`
selectUserQuery = `SELECT pass, role FROM user WHERE user = ?`
selectTopicPermsQuery = `
SELECT read, write
FROM access
WHERE user IN ('*', ?) AND ? LIKE topic
ORDER BY user DESC
`
)
// Manager-related queries
const (
insertUserQuery = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
selectUsernamesQuery = `SELECT user FROM user ORDER BY role, user`
updateUserPassQuery = `UPDATE user SET pass = ? WHERE user = ?`
updateUserRoleQuery = `UPDATE user SET role = ? WHERE user = ?`
deleteUserQuery = `DELETE FROM user WHERE user = ?`
upsertUserAccessQuery = `
INSERT INTO access (user, topic, read, write)
VALUES (?, ?, ?, ?)
ON CONFLICT (user, topic) DO UPDATE SET read=excluded.read, write=excluded.write
`
selectUserAccessQuery = `SELECT topic, read, write FROM access WHERE user = ?`
deleteAllAccessQuery = `DELETE FROM access`
deleteUserAccessQuery = `DELETE FROM access WHERE user = ?`
deleteTopicAccessQuery = `DELETE FROM access WHERE user = ? AND topic = ?`
)
// Schema management queries
const (
currentSchemaVersion = 1
insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)`
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
)
// SQLiteAuth is an implementation of Auther and Manager. It stores users and access control list
// in a SQLite database.
type SQLiteAuth struct {
db *sql.DB
defaultRead bool
defaultWrite bool
}
var _ Auther = (*SQLiteAuth)(nil)
var _ Manager = (*SQLiteAuth)(nil)
// NewSQLiteAuth creates a new SQLiteAuth instance
func NewSQLiteAuth(filename string, defaultRead, defaultWrite bool) (*SQLiteAuth, error) {
db, err := sql.Open("sqlite3", filename)
if err != nil {
return nil, err
}
if err := setupAuthDB(db); err != nil {
return nil, err
}
return &SQLiteAuth{
db: db,
defaultRead: defaultRead,
defaultWrite: defaultWrite,
}, nil
}
// Authenticate checks username and password and returns a user if correct. The method
// returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect.
func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) {
if username == Everyone {
return nil, ErrUnauthenticated
}
user, err := a.User(username)
if err != nil {
bcrypt.CompareHashAndPassword([]byte(intentionalSlowDownHash),
[]byte("intentional slow-down to avoid timing attacks"))
return nil, ErrUnauthenticated
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Hash), []byte(password)); err != nil {
return nil, ErrUnauthenticated
}
return user, nil
}
// Authorize returns nil if the given user has access to the given topic using the desired
// permission. The user param may be nil to signal an anonymous user.
func (a *SQLiteAuth) Authorize(user *User, topic string, perm Permission) error {
if user != nil && user.Role == RoleAdmin {
return nil // Admin can do everything
}
username := Everyone
if user != nil {
username = user.Name
}
// Select the read/write permissions for this user/topic combo. The query may return two
// rows (one for everyone, and one for the user), but prioritizes the user. The value for
// user.Name may be empty (= everyone).
rows, err := a.db.Query(selectTopicPermsQuery, username, topic)
if err != nil {
return err
}
defer rows.Close()
if !rows.Next() {
return a.resolvePerms(a.defaultRead, a.defaultWrite, perm)
}
var read, write bool
if err := rows.Scan(&read, &write); err != nil {
return err
} else if err := rows.Err(); err != nil {
return err
}
return a.resolvePerms(read, write, perm)
}
func (a *SQLiteAuth) resolvePerms(read, write bool, perm Permission) error {
if perm == PermissionRead && read {
return nil
} else if perm == PermissionWrite && write {
return nil
}
return ErrUnauthorized
}
// AddUser adds a user with the given username, password and role. The password should be hashed
// before it is stored in a persistence layer.
func (a *SQLiteAuth) AddUser(username, password string, role Role) error {
if !AllowedUsername(username) || !AllowedRole(role) {
return ErrInvalidArgument
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return err
}
if _, err = a.db.Exec(insertUserQuery, username, hash, role); err != nil {
return err
}
return nil
}
// RemoveUser deletes the user with the given username. The function returns nil on success, even
// if the user did not exist in the first place.
func (a *SQLiteAuth) RemoveUser(username string) error {
if !AllowedUsername(username) {
return ErrInvalidArgument
}
if _, err := a.db.Exec(deleteUserQuery, username); err != nil {
return err
}
if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil {
return err
}
return nil
}
// Users returns a list of users. It always also returns the Everyone user ("*").
func (a *SQLiteAuth) Users() ([]*User, error) {
rows, err := a.db.Query(selectUsernamesQuery)
if err != nil {
return nil, err
}
defer rows.Close()
usernames := make([]string, 0)
for rows.Next() {
var username string
if err := rows.Scan(&username); err != nil {
return nil, err
} else if err := rows.Err(); err != nil {
return nil, err
}
usernames = append(usernames, username)
}
rows.Close()
users := make([]*User, 0)
for _, username := range usernames {
user, err := a.User(username)
if err != nil {
return nil, err
}
users = append(users, user)
}
everyone, err := a.everyoneUser()
if err != nil {
return nil, err
}
users = append(users, everyone)
return users, nil
}
// User returns the user with the given username if it exists, or ErrNotFound otherwise.
// You may also pass Everyone to retrieve the anonymous user and its Grant list.
func (a *SQLiteAuth) User(username string) (*User, error) {
if username == Everyone {
return a.everyoneUser()
}
rows, err := a.db.Query(selectUserQuery, username)
if err != nil {
return nil, err
}
defer rows.Close()
var hash, role string
if !rows.Next() {
return nil, ErrNotFound
}
if err := rows.Scan(&hash, &role); err != nil {
return nil, err
} else if err := rows.Err(); err != nil {
return nil, err
}
grants, err := a.readGrants(username)
if err != nil {
return nil, err
}
return &User{
Name: username,
Hash: hash,
Role: Role(role),
Grants: grants,
}, nil
}
func (a *SQLiteAuth) everyoneUser() (*User, error) {
grants, err := a.readGrants(Everyone)
if err != nil {
return nil, err
}
return &User{
Name: Everyone,
Hash: "",
Role: RoleAnonymous,
Grants: grants,
}, nil
}
func (a *SQLiteAuth) readGrants(username string) ([]Grant, error) {
rows, err := a.db.Query(selectUserAccessQuery, username)
if err != nil {
return nil, err
}
defer rows.Close()
grants := make([]Grant, 0)
for rows.Next() {
var topic string
var read, write bool
if err := rows.Scan(&topic, &read, &write); err != nil {
return nil, err
} else if err := rows.Err(); err != nil {
return nil, err
}
grants = append(grants, Grant{
TopicPattern: fromSQLWildcard(topic),
AllowRead: read,
AllowWrite: write,
})
}
return grants, nil
}
// ChangePassword changes a user's password
func (a *SQLiteAuth) ChangePassword(username, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return err
}
if _, err := a.db.Exec(updateUserPassQuery, hash, username); err != nil {
return err
}
return nil
}
// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
// all existing access control entries (Grant) are removed, since they are no longer needed.
func (a *SQLiteAuth) ChangeRole(username string, role Role) error {
if !AllowedUsername(username) || !AllowedRole(role) {
return ErrInvalidArgument
}
if _, err := a.db.Exec(updateUserRoleQuery, string(role), username); err != nil {
return err
}
if role == RoleAdmin {
if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil {
return err
}
}
return nil
}
// AllowAccess adds or updates an entry in th access control list for a specific user. It controls
// read/write access to a topic. The parameter topicPattern may include wildcards (*).
func (a *SQLiteAuth) AllowAccess(username string, topicPattern string, read bool, write bool) error {
if (!AllowedUsername(username) && username != Everyone) || !AllowedTopicPattern(topicPattern) {
return ErrInvalidArgument
}
if _, err := a.db.Exec(upsertUserAccessQuery, username, toSQLWildcard(topicPattern), read, write); err != nil {
return err
}
return nil
}
// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
// empty) for an entire user. The parameter topicPattern may include wildcards (*).
func (a *SQLiteAuth) ResetAccess(username string, topicPattern string) error {
if !AllowedUsername(username) && username != Everyone && username != "" {
return ErrInvalidArgument
} else if !AllowedTopicPattern(topicPattern) && topicPattern != "" {
return ErrInvalidArgument
}
if username == "" && topicPattern == "" {
_, err := a.db.Exec(deleteAllAccessQuery, username)
return err
} else if topicPattern == "" {
_, err := a.db.Exec(deleteUserAccessQuery, username)
return err
}
_, err := a.db.Exec(deleteTopicAccessQuery, username, toSQLWildcard(topicPattern))
return err
}
// DefaultAccess returns the default read/write access if no access control entry matches
func (a *SQLiteAuth) DefaultAccess() (read bool, write bool) {
return a.defaultRead, a.defaultWrite
}
func toSQLWildcard(s string) string {
return strings.ReplaceAll(s, "*", "%")
}
func fromSQLWildcard(s string) string {
return strings.ReplaceAll(s, "%", "*")
}
func setupAuthDB(db *sql.DB) error {
// If 'schemaVersion' table does not exist, this must be a new database
rowsSV, err := db.Query(selectSchemaVersionQuery)
if err != nil {
return setupNewAuthDB(db)
}
defer rowsSV.Close()
// If 'schemaVersion' table exists, read version and potentially upgrade
schemaVersion := 0
if !rowsSV.Next() {
return errors.New("cannot determine schema version: database file may be corrupt")
}
if err := rowsSV.Scan(&schemaVersion); err != nil {
return err
}
rowsSV.Close()
// Do migrations
if schemaVersion == currentSchemaVersion {
return nil
}
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
}
func setupNewAuthDB(db *sql.DB) error {
if _, err := db.Exec(createAuthTablesQueries); err != nil {
return err
}
if _, err := db.Exec(insertSchemaVersion, currentSchemaVersion); err != nil {
return err
}
return nil
}

243
auth/auth_sqlite_test.go Normal file
View File

@@ -0,0 +1,243 @@
package auth_test
import (
"github.com/stretchr/testify/require"
"heckel.io/ntfy/auth"
"path/filepath"
"strings"
"testing"
"time"
)
const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources
func TestSQLiteAuth_FullScenario_Default_DenyAll(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin))
require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser))
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair!
require.Nil(t, a.AllowAccess(auth.Everyone, "announcements", true, false))
require.Nil(t, a.AllowAccess(auth.Everyone, "everyonewrite", true, true))
require.Nil(t, a.AllowAccess(auth.Everyone, "up*", false, true)) // Everyone can write to /up*
phil, err := a.Authenticate("phil", "phil")
require.Nil(t, err)
require.Equal(t, "phil", phil.Name)
require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$"))
require.Equal(t, auth.RoleAdmin, phil.Role)
require.Equal(t, []auth.Grant{}, phil.Grants)
ben, err := a.Authenticate("ben", "ben")
require.Nil(t, err)
require.Equal(t, "ben", ben.Name)
require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$"))
require.Equal(t, auth.RoleUser, ben.Role)
require.Equal(t, []auth.Grant{
{"mytopic", true, true},
{"readme", true, false},
{"writeme", false, true},
{"everyonewrite", false, false},
}, ben.Grants)
notben, err := a.Authenticate("ben", "this is wrong")
require.Nil(t, notben)
require.Equal(t, auth.ErrUnauthenticated, err)
// Admin can do everything
require.Nil(t, a.Authorize(phil, "sometopic", auth.PermissionWrite))
require.Nil(t, a.Authorize(phil, "mytopic", auth.PermissionRead))
require.Nil(t, a.Authorize(phil, "readme", auth.PermissionWrite))
require.Nil(t, a.Authorize(phil, "writeme", auth.PermissionWrite))
require.Nil(t, a.Authorize(phil, "announcements", auth.PermissionWrite))
require.Nil(t, a.Authorize(phil, "everyonewrite", auth.PermissionWrite))
// User cannot do everything
require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionWrite))
require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionRead))
require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "readme", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "writeme", auth.PermissionRead))
require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite))
require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "everyonewrite", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "everyonewrite", auth.PermissionWrite))
require.Nil(t, a.Authorize(ben, "announcements", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "announcements", auth.PermissionWrite))
// Everyone else can do barely anything
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "sometopicnotinthelist", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "sometopicnotinthelist", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "mytopic", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "mytopic", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "readme", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "readme", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "writeme", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "writeme", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "announcements", auth.PermissionWrite))
require.Nil(t, a.Authorize(nil, "announcements", auth.PermissionRead))
require.Nil(t, a.Authorize(nil, "everyonewrite", auth.PermissionRead))
require.Nil(t, a.Authorize(nil, "everyonewrite", auth.PermissionWrite))
require.Nil(t, a.Authorize(nil, "up1234", auth.PermissionWrite)) // Wildcard permission
require.Nil(t, a.Authorize(nil, "up5678", auth.PermissionWrite))
}
func TestSQLiteAuth_AddUser_Invalid(t *testing.T) {
a := newTestAuth(t, false, false)
require.Equal(t, auth.ErrInvalidArgument, a.AddUser(" invalid ", "pass", auth.RoleAdmin))
require.Equal(t, auth.ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role"))
}
func TestSQLiteAuth_AddUser_Timing(t *testing.T) {
a := newTestAuth(t, false, false)
start := time.Now().UnixMilli()
require.Nil(t, a.AddUser("user", "pass", auth.RoleAdmin))
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
}
func TestSQLiteAuth_Authenticate_Timing(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("user", "pass", auth.RoleAdmin))
// Timing a correct attempt
start := time.Now().UnixMilli()
_, err := a.Authenticate("user", "pass")
require.Nil(t, err)
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
// Timing an incorrect attempt
start = time.Now().UnixMilli()
_, err = a.Authenticate("user", "INCORRECT")
require.Equal(t, auth.ErrUnauthenticated, err)
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
// Timing a non-existing user attempt
start = time.Now().UnixMilli()
_, err = a.Authenticate("DOES-NOT-EXIST", "hithere")
require.Equal(t, auth.ErrUnauthenticated, err)
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
}
func TestSQLiteAuth_UserManagement(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin))
require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser))
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair!
require.Nil(t, a.AllowAccess(auth.Everyone, "announcements", true, false))
require.Nil(t, a.AllowAccess(auth.Everyone, "everyonewrite", true, true))
// Query user details
phil, err := a.User("phil")
require.Nil(t, err)
require.Equal(t, "phil", phil.Name)
require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$"))
require.Equal(t, auth.RoleAdmin, phil.Role)
require.Equal(t, []auth.Grant{}, phil.Grants)
ben, err := a.User("ben")
require.Nil(t, err)
require.Equal(t, "ben", ben.Name)
require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$"))
require.Equal(t, auth.RoleUser, ben.Role)
require.Equal(t, []auth.Grant{
{"mytopic", true, true},
{"readme", true, false},
{"writeme", false, true},
{"everyonewrite", false, false},
}, ben.Grants)
everyone, err := a.User(auth.Everyone)
require.Nil(t, err)
require.Equal(t, "*", everyone.Name)
require.Equal(t, "", everyone.Hash)
require.Equal(t, auth.RoleAnonymous, everyone.Role)
require.Equal(t, []auth.Grant{
{"announcements", true, false},
{"everyonewrite", true, true},
}, everyone.Grants)
// Ben: Before revoking
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionRead))
require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionWrite))
require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead))
require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite))
// Revoke access for "ben" to "mytopic", then check again
require.Nil(t, a.ResetAccess("ben", "mytopic"))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "mytopic", auth.PermissionWrite)) // Revoked
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "mytopic", auth.PermissionRead)) // Revoked
require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead)) // Unchanged
require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite)) // Unchanged
// Revoke rest of the access
require.Nil(t, a.ResetAccess("ben", ""))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "readme", auth.PermissionRead)) // Revoked
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "wrtiteme", auth.PermissionWrite)) // Revoked
// User list
users, err := a.Users()
require.Nil(t, err)
require.Equal(t, 3, len(users))
require.Equal(t, "phil", users[0].Name)
require.Equal(t, "ben", users[1].Name)
require.Equal(t, "*", users[2].Name)
// Remove user
require.Nil(t, a.RemoveUser("ben"))
_, err = a.User("ben")
require.Equal(t, auth.ErrNotFound, err)
users, err = a.Users()
require.Nil(t, err)
require.Equal(t, 2, len(users))
require.Equal(t, "phil", users[0].Name)
require.Equal(t, "*", users[1].Name)
}
func TestSQLiteAuth_ChangePassword(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin))
_, err := a.Authenticate("phil", "phil")
require.Nil(t, err)
require.Nil(t, a.ChangePassword("phil", "newpass"))
_, err = a.Authenticate("phil", "phil")
require.Equal(t, auth.ErrUnauthenticated, err)
_, err = a.Authenticate("phil", "newpass")
require.Nil(t, err)
}
func TestSQLiteAuth_ChangeRole(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser))
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
ben, err := a.User("ben")
require.Nil(t, err)
require.Equal(t, auth.RoleUser, ben.Role)
require.Equal(t, 2, len(ben.Grants))
require.Nil(t, a.ChangeRole("ben", auth.RoleAdmin))
ben, err = a.User("ben")
require.Nil(t, err)
require.Equal(t, auth.RoleAdmin, ben.Role)
require.Equal(t, 0, len(ben.Grants))
}
func newTestAuth(t *testing.T, defaultRead, defaultWrite bool) *auth.SQLiteAuth {
filename := filepath.Join(t.TempDir(), "user.db")
a, err := auth.NewSQLiteAuth(filename, defaultRead, defaultWrite)
require.Nil(t, err)
return a
}

View File

@@ -7,9 +7,9 @@ import (
"encoding/json"
"errors"
"fmt"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"io"
"log"
"net/http"
"strings"
"sync"
@@ -47,7 +47,6 @@ type Message struct { // TODO combine with server.message
Priority int
Tags []string
Click string
Icon string
Attachment *Attachment
// Additional fields
@@ -103,7 +102,6 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO
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
@@ -138,7 +136,6 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
msgChan := make(chan *Message)
errChan := make(chan error)
topicURL := c.expandTopicURL(topic)
log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL))
options = append(options, WithPoll())
go func() {
err := performSubscribeRequest(ctx, msgChan, topicURL, "", options...)
@@ -164,18 +161,16 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
// 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)
// }
// 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 {
c.mu.Lock()
defer c.mu.Unlock()
subscriptionID := util.RandomString(10)
topicURL := c.expandTopicURL(topic)
log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL))
ctx, cancel := context.WithCancel(context.Background())
c.subscriptions[subscriptionID] = &subscription{
ID: subscriptionID,
@@ -231,11 +226,11 @@ func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicUR
// 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())
log.Printf("Connection to %s failed: %s", topicURL, err.Error())
}
select {
case <-ctx.Done():
log.Info("%s Connection exited", util.ShortTopicURL(topicURL))
log.Printf("Connection to %s exited", topicURL)
return
case <-time.After(10 * time.Second): // TODO Add incremental backoff
}
@@ -243,9 +238,7 @@ func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicUR
}
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)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/json", topicURL), nil)
if err != nil {
return err
}
@@ -268,12 +261,10 @@ func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicUR
}
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
messageJSON := scanner.Text()
m, err := toMessage(messageJSON, topicURL, subscriptionID)
m, err := toMessage(scanner.Text(), topicURL, subscriptionID)
if err != nil {
return err
}
log.Trace("%s Message received: %s", util.ShortTopicURL(topicURL), messageJSON)
if m.Event == MessageEvent {
msgChan <- m
}

View File

@@ -5,18 +5,6 @@
#
# 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 ("")
# 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 cann "ntfy subscribe --from-config" directly.
#
@@ -32,8 +20,6 @@
# command: 'notify-send "$m"'
# user: phill
# password: mypass
# - topic: token_topic
# token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
#
# Variables:
# Variable Aliases Description

View File

@@ -4,18 +4,11 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/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)

View File

@@ -12,33 +12,21 @@ const (
// 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"`
DefaultHost string `yaml:"default-host"`
Subscribe []struct {
Topic string `yaml:"topic"`
User string `yaml:"user"`
Password string `yaml:"password"`
Command string `yaml:"command"`
If map[string]string `yaml:"if"`
} `yaml:"subscribe"`
}
// NewConfig creates a new Config struct for a Client
func NewConfig() *Config {
return &Config{
DefaultHost: DefaultBaseURL,
DefaultUser: "",
DefaultPassword: nil,
DefaultToken: "",
DefaultCommand: "",
Subscribe: nil,
DefaultHost: DefaultBaseURL,
Subscribe: nil,
}
}

View File

@@ -12,9 +12,6 @@ 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
@@ -25,116 +22,19 @@ subscribe:
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, 3, 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, "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.Equal(t, "", conf.Subscribe[0].User)
require.Nil(t, conf.Subscribe[0].Password)
require.Equal(t, "", conf.Subscribe[0].Token)
}

View File

@@ -56,11 +56,6 @@ 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 {
@@ -87,11 +82,6 @@ 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))
}
// WithNoCache instructs the server not to cache the message server-side
func WithNoCache() PublishOption {
return WithHeader("X-Cache", "no")

View File

@@ -1,25 +1,19 @@
//go:build !noserver
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/user"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/util"
)
func init() {
commands = append(commands, cmdAccess)
}
const (
userEveryone = "everyone"
)
var flagsAccess = append(
append([]cli.Flag{}, flagsUser...),
userCommandFlags(),
&cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"},
)
@@ -28,7 +22,7 @@ var cmdAccess = &cli.Command{
Usage: "Grant/revoke access to a topic, or show access",
UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]",
Flags: flagsAccess,
Before: initConfigFileInputSourceFunc("config", flagsAccess, initLogFunc),
Before: initConfigFileInputSource("config", flagsAccess),
Action: execUserAccess,
Category: categoryServer,
Description: `Manage the access control list for the ntfy server.
@@ -71,13 +65,13 @@ 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)
manager, err := createAuthManager(c)
if err != nil {
return err
}
username := c.Args().Get(0)
if username == userEveryone {
username = user.Everyone
username = auth.Everyone
}
topic := c.Args().Get(1)
perms := c.Args().Get(2)
@@ -96,28 +90,26 @@ func execUserAccess(c *cli.Context) error {
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) {
func changeAccess(c *cli.Context, manager auth.Manager, username string, topic string, perms string) error {
if !util.InStringList([]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 err == user.ErrUserNotFound {
read := util.InStringList([]string{"read-write", "rw", "read-only", "read", "ro"}, perms)
write := util.InStringList([]string{"read-write", "rw", "write-only", "write", "wo"}, perms)
user, err := manager.User(username)
if err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if u.Role == user.RoleAdmin {
} else if user.Role == auth.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 {
if err := manager.AllowAccess(username, topic, read, write); err != nil {
return err
}
if permission.IsReadWrite() {
if read && write {
fmt.Fprintf(c.App.ErrWriter, "granted read-write access to topic %s\n\n", topic)
} else if permission.IsRead() {
} else if read {
fmt.Fprintf(c.App.ErrWriter, "granted read-only access to topic %s\n\n", topic)
} else if permission.IsWrite() {
} else if write {
fmt.Fprintf(c.App.ErrWriter, "granted write-only access to topic %s\n\n", topic)
} else {
fmt.Fprintf(c.App.ErrWriter, "revoked all access to topic %s\n\n", topic)
@@ -125,7 +117,7 @@ func changeAccess(c *cli.Context, manager *user.Manager, username string, topic
return showUserAccess(c, manager, username)
}
func resetAccess(c *cli.Context, manager *user.Manager, username, topic string) error {
func resetAccess(c *cli.Context, manager auth.Manager, username, topic string) error {
if username == "" {
return resetAllAccess(c, manager)
} else if topic == "" {
@@ -134,7 +126,7 @@ func resetAccess(c *cli.Context, manager *user.Manager, username, topic string)
return resetUserTopicAccess(c, manager, username, topic)
}
func resetAllAccess(c *cli.Context, manager *user.Manager) error {
func resetAllAccess(c *cli.Context, manager auth.Manager) error {
if err := manager.ResetAccess("", ""); err != nil {
return err
}
@@ -142,7 +134,7 @@ func resetAllAccess(c *cli.Context, manager *user.Manager) error {
return nil
}
func resetUserAccess(c *cli.Context, manager *user.Manager, username string) error {
func resetUserAccess(c *cli.Context, manager auth.Manager, username string) error {
if err := manager.ResetAccess(username, ""); err != nil {
return err
}
@@ -150,7 +142,7 @@ func resetUserAccess(c *cli.Context, manager *user.Manager, username string) err
return showUserAccess(c, manager, username)
}
func resetUserTopicAccess(c *cli.Context, manager *user.Manager, username string, topic string) error {
func resetUserTopicAccess(c *cli.Context, manager auth.Manager, username string, topic string) error {
if err := manager.ResetAccess(username, topic); err != nil {
return err
}
@@ -158,14 +150,14 @@ func resetUserTopicAccess(c *cli.Context, manager *user.Manager, username string
return showUserAccess(c, manager, username)
}
func showAccess(c *cli.Context, manager *user.Manager, username string) error {
func showAccess(c *cli.Context, manager auth.Manager, username string) error {
if username == "" {
return showAllAccess(c, manager)
}
return showUserAccess(c, manager, username)
}
func showAllAccess(c *cli.Context, manager *user.Manager) error {
func showAllAccess(c *cli.Context, manager auth.Manager) error {
users, err := manager.Users()
if err != nil {
return err
@@ -173,36 +165,28 @@ func showAllAccess(c *cli.Context, manager *user.Manager) error {
return showUsers(c, manager, users)
}
func showUserAccess(c *cli.Context, manager *user.Manager, username string) error {
func showUserAccess(c *cli.Context, manager auth.Manager, username string) error {
users, err := manager.User(username)
if err == user.ErrUserNotFound {
if err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
}
return showUsers(c, manager, []*user.User{users})
return showUsers(c, manager, []*auth.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
}
fmt.Fprintf(c.App.ErrWriter, "user %s (role: %s, tier: %s)\n", u.Name, u.Role, tier)
if u.Role == user.RoleAdmin {
func showUsers(c *cli.Context, manager auth.Manager, users []*auth.User) error {
for _, user := range users {
fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", user.Name, user.Role)
if user.Role == auth.RoleAdmin {
fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
} else if len(grants) > 0 {
for _, grant := range grants {
if grant.Allow.IsReadWrite() {
} else if len(user.Grants) > 0 {
for _, grant := range user.Grants {
if grant.AllowRead && grant.AllowWrite {
fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern)
} else if grant.Allow.IsRead() {
} else if grant.AllowRead {
fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.TopicPattern)
} else if grant.Allow.IsWrite() {
} else if grant.AllowWrite {
fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.TopicPattern)
} else {
fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.TopicPattern)
@@ -211,13 +195,13 @@ func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error
} else {
fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n")
}
if u.Name == user.Everyone {
access := manager.DefaultAccess()
if access.IsReadWrite() {
if user.Name == auth.Everyone {
defaultRead, defaultWrite := manager.DefaultAccess()
if defaultRead && defaultWrite {
fmt.Fprintln(c.App.ErrWriter, "- read-write access to all (other) topics (server config)")
} else if access.IsRead() {
} else if defaultRead {
fmt.Fprintln(c.App.ErrWriter, "- read-only access to all (other) topics (server config)")
} else if access.IsWrite() {
} else if defaultWrite {
fmt.Fprintln(c.App.ErrWriter, "- write-only access to all (other) topics (server config)")
} else {
fmt.Fprintln(c.App.ErrWriter, "- no access to any (other) topics (server config)")

View File

@@ -15,7 +15,7 @@ func TestCLI_Access_Show(t *testing.T) {
app, _, _, stderr := newTestApp()
require.Nil(t, runAccessCommand(app, conf))
require.Contains(t, stderr.String(), "user * (role: anonymous, tier: none)\n- no topic-specific permissions\n- no access to any (other) topics (server config)")
require.Contains(t, stderr.String(), "user * (anonymous)\n- no topic-specific permissions\n- no access to any (other) topics (server config)")
}
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
@@ -32,12 +32,12 @@ func TestCLI_Access_Grant_And_Publish(t *testing.T) {
app, _, _, stderr := newTestApp()
require.Nil(t, runAccessCommand(app, conf))
expected := `user phil (role: admin, tier: none)
expected := `user phil (admin)
- read-write access to all topics (admin role)
user ben (role: user, tier: none)
user ben (user)
- read-write access to topic announcements
- read-only access to topic sometopic
user * (role: anonymous, tier: none)
user * (anonymous)
- read-only access to topic announcements
- no access to any (other) topics (server config)
`
@@ -79,11 +79,9 @@ user * (role: anonymous, tier: none)
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(),
"--auth-default-access=" + confToDefaultAccess(conf),
}
return app.Run(append(userArgs, args...))
}

View File

@@ -5,9 +5,13 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"os"
"regexp"
)
var (
defaultClientRootConfigFile = "/etc/ntfy/client.yml"
defaultClientUserConfigFile = "~/.config/ntfy/client.yml"
)
const (
@@ -15,22 +19,6 @@ const (
categoryServer = "Server commands"
)
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{
@@ -42,49 +30,33 @@ func New() *cli.App {
Reader: os.Stdin,
Writer: os.Stdout,
ErrWriter: os.Stderr,
Commands: commands,
Flags: flagsDefault,
Before: initLogFunc,
Commands: []*cli.Command{
// Server commands
cmdServe,
cmdUser,
cmdAccess,
// Client commands
cmdPublish,
cmdSubscribe,
},
}
}
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)
// initConfigFileInputSource 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 initConfigFileInputSource(configFlag string, flags []cli.Flag) 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 := altsrc.NewYamlSourceFromFile(configFile)
if err != nil {
return err
}
log.SetOutput(w)
return altsrc.ApplyInputSourceValues(context, inputSource, flags)
}
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
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"os"
"strings"
"testing"
@@ -14,7 +13,7 @@ import (
// This only contains helpers so far
func TestMain(m *testing.M) {
log.SetLevel(log.ErrorLevel)
// log.SetOutput(io.Discard)
os.Exit(m.Run())
}

View File

@@ -1,60 +0,0 @@
package cmd
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"gopkg.in/yaml.v2"
"heckel.io/ntfy/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
}

View File

@@ -1,38 +0,0 @@
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)
}

View File

@@ -5,55 +5,38 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/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.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
&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,
Name: "publish",
Aliases: []string{"pub", "send", "trigger"},
Usage: "Send message via a ntfy server",
UsageText: "ntfy send [OPTIONS..] TOPIC [MESSAGE]\n NTFY_TOPIC=.. ntfy send [OPTIONS..] -P [MESSAGE]",
Action: execPublish,
Category: categoryClient,
Flags: []cli.Flag{
&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: "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: "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.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
&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.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do print message"},
},
Description: `Publish a message to a ntfy server.
Examples:
@@ -65,21 +48,19 @@ Examples:
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 -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
NTFY_TOPIC=mytopic ntfy pub -P "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,
The default config file for all client commands is /etc/ntfy/client.yml (if root user),
or ~/.config/ntfy/client.yml for all other users.`,
}
func execPublish(c *cli.Context) error {
@@ -92,28 +73,30 @@ func execPublish(c *cli.Context) error {
tags := c.String("tags")
delay := c.String("delay")
click := c.String("click")
icon := c.String("icon")
actions := c.String("actions")
attach := c.String("attach")
filename := c.String("filename")
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")
envTopic := c.Bool("env-topic")
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 topic, message string
if envTopic {
topic = os.Getenv("NTFY_TOPIC")
if c.NArg() > 0 {
message = strings.Join(c.Args().Slice(), " ")
}
} else {
if c.NArg() < 1 {
return errors.New("must specify topic, type 'ntfy publish --help' for help")
}
topic = c.Args().Get(0)
if c.NArg() > 1 {
message = strings.Join(c.Args().Slice()[1:], " ")
}
}
var options []client.PublishOption
if title != "" {
@@ -131,9 +114,6 @@ func execPublish(c *cli.Context) error {
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", " ")))
}
@@ -152,9 +132,7 @@ func execPublish(c *cli.Context) error {
if noFirebase {
options = append(options, client.WithNoFirebase())
}
if token != "" {
options = append(options, client.WithBearerAuth(token))
} else if user != "" {
if user != "" {
var pass string
parts := strings.SplitN(user, ":", 2)
if len(parts) == 2 {
@@ -170,25 +148,6 @@ func execPublish(c *cli.Context) error {
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 == "" {
@@ -222,88 +181,3 @@ func execPublish(c *cli.Context) error {
}
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")
}
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
}

View File

@@ -5,33 +5,18 @@ import (
"github.com/stretchr/testify/require"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
)
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
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)
app2, _, stdout, _ := newTestApp()
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
require.Contains(t, stdout.String(), testMessage)
}
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
@@ -63,7 +48,6 @@ func TestCLI_Publish_All_The_Things(t *testing.T) {
"--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",
@@ -85,216 +69,4 @@ func TestCLI_Publish_All_The_Things(t *testing.T) {
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())
}

View File

@@ -1,11 +0,0 @@
//go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd
// +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
}

View File

@@ -1,10 +0,0 @@
package cmd
import (
"os"
)
func processExists(pid int) bool {
_, err := os.FindProcess(pid)
return err == nil
}

View File

@@ -1,96 +1,58 @@
//go:build !noserver
package cmd
import (
"errors"
"fmt"
"github.com/stripe/stripe-go/v74"
"heckel.io/ntfy/user"
"io/fs"
"math"
"net"
"net/netip"
"os"
"os/signal"
"strings"
"syscall"
"time"
"heckel.io/ntfy/log"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/server"
"heckel.io/ntfy/util"
"log"
"math"
"net"
"strings"
"time"
)
func init() {
commands = append(commands, cmdServe)
var flagsServe = []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"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{"l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "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{"A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home) or web app (app)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "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", EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "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", EnvVars: []string{"NTFY_SMTP_SENDER_FROM"}, Usage: "SMTP sender address (if e-mail sending is enabled)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "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", 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", EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "100M", Usage: "total storage limit used for attachments per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "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", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "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-email-limit-burst", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
}
const (
defaultServerConfigFile = "/etc/ntfy/server.yml"
)
var flagsServe = append(
append([]cli.Flag{}, flagsDefault...),
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, 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: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: 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.NewDurationFlag(&cli.DurationFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, 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.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"}, DefaultText: "5G", 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"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: 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: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (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.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: "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.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.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: "100M", 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.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: 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.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
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.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
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)"}),
)
var cmdServe = &cli.Command{
Name: "serve",
Usage: "Run the ntfy server",
@@ -98,7 +60,7 @@ var cmdServe = &cli.Command{
Action: execServe,
Category: categoryServer,
Flags: flagsServe,
Before: initConfigFileInputSourceFunc("config", flagsServe, initLogFunc),
Before: initConfigFileInputSource("config", flagsServe),
Description: `Run the ntfy server and listen for incoming requests
The command will load the configuration from /etc/ntfy/server.yml. Config options can
@@ -115,22 +77,16 @@ func execServe(c *cli.Context) error {
}
// Read all the options
config := c.String("config")
baseURL := 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")
cacheFile := c.String("cache-file")
cacheDuration := c.Duration("cache-duration")
cacheStartupQueries := c.String("cache-startup-queries")
cacheBatchSize := c.Int("cache-batch-size")
cacheBatchTimeout := c.Duration("cache-batch-timeout")
authFile := c.String("auth-file")
authStartupQueries := c.String("auth-startup-queries")
authDefaultAccess := c.String("auth-default-access")
attachmentCacheDir := c.String("attachment-cache-dir")
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
@@ -138,12 +94,7 @@ func execServe(c *cli.Context) error {
attachmentExpiryDuration := c.Duration("attachment-expiry-duration")
keepaliveInterval := c.Duration("keepalive-interval")
managerInterval := c.Duration("manager-interval")
disallowedTopics := c.StringSlice("disallowed-topics")
webRoot := c.String("web-root")
enableSignup := c.Bool("enable-signup")
enableLogin := c.Bool("enable-login")
enableReservations := c.Bool("enable-reservations")
upstreamBaseURL := c.String("upstream-base-url")
smtpSenderAddr := c.String("smtp-sender-addr")
smtpSenderUser := c.String("smtp-sender-user")
smtpSenderPass := c.String("smtp-sender-pass")
@@ -153,22 +104,14 @@ func execServe(c *cli.Context) error {
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
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")
visitorRequestLimitReplenish := c.Duration("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")
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
behindProxy := c.Bool("behind-proxy")
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")
// Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
@@ -185,42 +128,24 @@ func execServe(c *cli.Context) error {
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 smtpSenderAddr != "" && (baseURL == "" || smtpSenderUser == "" || smtpSenderPass == "" || smtpSenderFrom == "") {
return errors.New("if smtp-sender-addr is set, base-url, smtp-sender-user, smtp-sender-pass 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 != "" && baseURL == "" {
return errors.New("if attachment-cache-dir is set, base-url must also be set")
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
return errors.New("if set, base-url must start with http:// or https://")
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
return errors.New("if set, base-url must not end with a slash (/)")
} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) {
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
} else if !util.InStringList([]string{"app", "home"}, webRoot) {
return errors.New("if set, web-root must be 'home' or 'app'")
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
return errors.New("if set, upstream-base-url must start with http:// or https://")
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
return errors.New("if set, upstream-base-url must not end with a slash (/)")
} else if upstreamBaseURL != "" && baseURL == "" {
return errors.New("if upstream-base-url is set, base-url must also be set")
} else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL {
return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications")
} else if authFile == "" && (enableSignup || enableLogin || enableReservations || stripeSecretKey != "") {
return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
} else if enableSignup && !enableLogin {
return errors.New("cannot set enable-signup without also setting enable-login")
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
}
webRootIsApp := webRoot == "app"
enableWeb := webRoot != "disable"
// Default auth permissions
authDefault, err := user.ParsePermission(authDefaultAccess)
if err != nil {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
}
webRootIsApp := webRoot == "app"
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
// Special case: Unset default
if listenHTTP == "-" {
@@ -248,53 +173,39 @@ func execServe(c *cli.Context) error {
}
// Resolve hosts
visitorRequestLimitExemptIPs := make([]netip.Prefix, 0)
visitorRequestLimitExemptIPs := make([]string, 0)
for _, host := range visitorRequestLimitExemptHosts {
ips, err := parseIPHostPrefix(host)
ips, err := net.LookupIP(host)
if err != nil {
log.Warn("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error())
log.Printf("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error())
continue
}
visitorRequestLimitExemptIPs = append(visitorRequestLimitExemptIPs, ips...)
for _, ip := range ips {
visitorRequestLimitExemptIPs = append(visitorRequestLimitExemptIPs, ip.String())
}
}
// Stripe things
if stripeSecretKey != "" {
stripe.EnableTelemetry = false // Whoa!
stripe.Key = stripeSecretKey
}
// Add default forbidden topics
disallowedTopics = append(disallowedTopics, server.DefaultDisallowedTopics...)
// Run server
conf := server.NewConfig()
conf.File = config
conf.BaseURL = baseURL
conf.ListenHTTP = listenHTTP
conf.ListenHTTPS = listenHTTPS
conf.ListenUnix = listenUnix
conf.ListenUnixMode = fs.FileMode(listenUnixMode)
conf.KeyFile = keyFile
conf.CertFile = certFile
conf.FirebaseKeyFile = firebaseKeyFile
conf.CacheFile = cacheFile
conf.CacheDuration = cacheDuration
conf.CacheStartupQueries = cacheStartupQueries
conf.CacheBatchSize = cacheBatchSize
conf.CacheBatchTimeout = cacheBatchTimeout
conf.AuthFile = authFile
conf.AuthStartupQueries = authStartupQueries
conf.AuthDefault = authDefault
conf.AuthDefaultRead = authDefaultRead
conf.AuthDefaultWrite = authDefaultWrite
conf.AttachmentCacheDir = attachmentCacheDir
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit
conf.AttachmentExpiryDuration = attachmentExpiryDuration
conf.KeepaliveInterval = keepaliveInterval
conf.ManagerInterval = managerInterval
conf.DisallowedTopics = disallowedTopics
conf.WebRootIsApp = webRootIsApp
conf.UpstreamBaseURL = upstreamBaseURL
conf.SMTPSenderAddr = smtpSenderAddr
conf.SMTPSenderUser = smtpSenderUser
conf.SMTPSenderPass = smtpSenderPass
@@ -305,38 +216,21 @@ func execServe(c *cli.Context) error {
conf.TotalTopicLimit = totalTopicLimit
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
conf.VisitorAttachmentDailyBandwidthLimit = visitorAttachmentDailyBandwidthLimit
conf.VisitorAttachmentDailyBandwidthLimit = int(visitorAttachmentDailyBandwidthLimit)
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
conf.VisitorRequestExemptIPAddrs = visitorRequestLimitExemptIPs
conf.VisitorMessageDailyLimit = visitorMessageDailyLimit
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting
conf.BehindProxy = behindProxy
conf.StripeSecretKey = stripeSecretKey
conf.StripeWebhookKey = stripeWebhookKey
conf.BillingContact = billingContact
conf.EnableWeb = enableWeb
conf.EnableSignup = enableSignup
conf.EnableLogin = enableLogin
conf.EnableReservations = enableReservations
conf.EnableMetrics = enableMetrics
conf.MetricsListenHTTP = metricsListenHTTP
conf.ProfileListenHTTP = profileListenHTTP
conf.Version = c.App.Version
// Set up hot-reloading of config
go sigHandlerConfigReload(config)
// Run server
s, err := server.New(conf)
if err != nil {
log.Fatal(err.Error())
} else if err := s.Run(); err != nil {
log.Fatal(err.Error())
log.Fatalln(err)
}
log.Info("Exiting.")
if err := s.Run(); err != nil {
log.Fatalln(err)
}
log.Printf("Exiting.")
return nil
}
@@ -350,66 +244,3 @@ func parseSize(s string, defaultValue int64) (v int64, err error) {
}
return v, nil
}
func sigHandlerConfigReload(config string) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP)
for range sigs {
log.Info("Partially hot reloading configuration ...")
inputSource, err := newYamlSourceFromFile(config, flagsServe)
if err != nil {
log.Warn("Hot reload failed: %s", err.Error())
continue
}
if err := reloadLogLevel(inputSource); err != nil {
log.Warn("Reloading log level failed: %s", err.Error())
}
}
}
func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
// Try parsing as prefix, e.g. 10.0.1.0/24
prefix, err := netip.ParsePrefix(host)
if err == nil {
prefixes = append(prefixes, prefix.Masked())
return prefixes, nil
}
// Not a prefix, parse as host or IP (LookupHost passes through an IP as is)
ips, err := net.LookupHost(host)
if err != nil {
return nil, err
}
for _, ipStr := range ips {
ip, err := netip.ParseAddr(ipStr)
if err == nil {
prefix, err := ip.Prefix(ip.BitLen())
if err != nil {
return nil, fmt.Errorf("%s successfully parsed but unable to make prefix: %s", ip.String(), err.Error())
}
prefixes = append(prefixes, prefix.Masked())
}
}
return
}
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
newLevelStr, err := inputSource.String("log-level")
if err != nil {
return fmt.Errorf("cannot load log level: %s", err.Error())
}
overrides, err := inputSource.StringSlice("log-level-overrides")
if err != nil {
return fmt.Errorf("cannot load log level overrides (1): %s", err.Error())
}
log.ResetLevelOverrides()
if err := applyLogLevelOverrides(overrides); err != nil {
return fmt.Errorf("cannot load log level overrides (2): %s", err.Error())
}
log.SetLevel(log.ToLevel(newLevelStr))
if len(overrides) > 0 {
log.Info("Log level is %v, %d override(s) in place", strings.ToUpper(newLevelStr), len(overrides))
} else {
log.Info("Log level is %v", strings.ToUpper(newLevelStr))
}
return nil
}

View File

@@ -2,19 +2,17 @@ package cmd
import (
"fmt"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/client"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
"math/rand"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/client"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
)
func init() {
@@ -72,22 +70,6 @@ func TestCLI_Serve_WebSocket(t *testing.T) {
require.Equal(t, "mytopic", m.Topic)
}
func TestIP_Host_Parsing(t *testing.T) {
cases := map[string]string{
"1.1.1.1": "1.1.1.1/32",
"fd00::1234": "fd00::1234/128",
"192.168.0.3/24": "192.168.0.0/24",
"10.1.2.3/8": "10.0.0.0/8",
"201:be93::4a6/21": "201:b800::/21",
}
for q, expectedAnswer := range cases {
ips, err := parseIPHostPrefix(q)
require.Nil(t, err)
assert.Equal(t, 1, len(ips))
assert.Equal(t, expectedAnswer, ips[0].String())
}
}
func newEmptyFile(t *testing.T) string {
filename := filepath.Join(t.TempDir(), "empty")
require.Nil(t, os.WriteFile(filename, []byte{}, 0600))

View File

@@ -5,37 +5,14 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"sort"
"strings"
)
func init() {
commands = append(commands, cmdSubscribe)
}
const (
clientRootConfigFileUnixAbsolute = "/etc/ntfy/client.yml"
clientUserConfigFileUnixRelative = "ntfy/client.yml"
clientUserConfigFileWindowsRelative = "ntfy\\client.yml"
)
var flagsSubscribe = append(
append([]cli.Flag{}, flagsDefault...),
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
&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.BoolFlag{Name: "from-config", Aliases: []string{"from_config", "C"}, Usage: "read subscriptions from config file (service mode)"},
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
)
var cmdSubscribe = &cli.Command{
Name: "subscribe",
Aliases: []string{"sub"},
@@ -43,8 +20,15 @@ var cmdSubscribe = &cli.Command{
UsageText: "ntfy subscribe [OPTIONS..] [TOPIC]",
Action: execSubscribe,
Category: categoryClient,
Flags: flagsSubscribe,
Before: initLogFunc,
Flags: []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"},
&cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"},
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
&cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "print verbose output"},
},
Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for
every arriving message. There are 3 modes in which the command can be run:
@@ -76,17 +60,19 @@ ntfy subscribe TOPIC COMMAND
Examples:
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
ntfy sub topic1 myscript.sh # Execute script for incoming messages
ntfy sub topic1 /my/script.sh # Execute script for incoming messages
ntfy subscribe --from-config
Service mode (used in ntfy-client.service). This reads the config file and sets up
subscriptions for every topic in the "subscribe:" block (see config file).
Service mode (used in ntfy-client.service). This reads the config file (/etc/ntfy/client.yml
or ~/.config/ntfy/client.yml) and sets up subscriptions for every topic in the "subscribe:"
block (see config file).
Examples:
ntfy sub --from-config # Read topics from config file
ntfy sub --config=myclient.yml --from-config # Read topics from alternate config file
ntfy sub --config=/my/client.yml --from-config # Read topics from alternate config file
` + clientCommandDescriptionSuffix,
The default config file for all client commands is /etc/ntfy/client.yml (if root user),
or ~/.config/ntfy/client.yml for all other users.`,
}
func execSubscribe(c *cli.Context) error {
@@ -98,18 +84,11 @@ func execSubscribe(c *cli.Context) error {
cl := client.New(conf)
since := c.String("since")
user := c.String("user")
token := c.String("token")
poll := c.Bool("poll")
scheduled := c.Bool("scheduled")
fromConfig := c.Bool("from-config")
topic := c.Args().Get(0)
command := c.Args().Get(1)
// Checks
if user != "" && token != "" {
return errors.New("cannot set both --user and --token")
}
if !fromConfig {
conf.Subscribe = nil // wipe if --from-config not passed
}
@@ -117,9 +96,6 @@ func execSubscribe(c *cli.Context) error {
if since != "" {
options = append(options, client.WithSince(since))
}
if token != "" {
options = append(options, client.WithBearerAuth(token))
}
if user != "" {
var pass string
parts := strings.SplitN(user, ":", 2)
@@ -137,6 +113,9 @@ func execSubscribe(c *cli.Context) error {
}
options = append(options, client.WithBasicAuth(user, pass))
}
if poll {
options = append(options, client.WithPoll())
}
if scheduled {
options = append(options, client.WithScheduled())
}
@@ -153,9 +132,6 @@ func execSubscribe(c *cli.Context) error {
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
for _, s := range conf.Subscribe { // may be nil
if auth := maybeAddAuthHeader(s, conf); auth != nil {
options = append(options, auth)
}
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
return err
}
@@ -180,56 +156,28 @@ func doPollSingle(c *cli.Context, cl *client.Client, topic, command string, opti
}
func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
cmds := make(map[string]string) // Subscription ID -> command
for _, s := range conf.Subscribe { // May be nil
commands := make(map[string]string) // Subscription ID -> command
for _, s := range conf.Subscribe { // May be nil
topicOptions := append(make([]client.SubscribeOption, 0), options...)
for filter, value := range s.If {
topicOptions = append(topicOptions, client.WithFilter(filter, value))
}
if auth := maybeAddAuthHeader(s, conf); auth != nil {
topicOptions = append(topicOptions, auth)
if s.User != "" && s.Password != "" {
topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password))
}
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
if s.Command != "" {
cmds[subscriptionID] = s.Command
} else if conf.DefaultCommand != "" {
cmds[subscriptionID] = conf.DefaultCommand
} else {
cmds[subscriptionID] = ""
}
commands[subscriptionID] = s.Command
}
if topic != "" {
subscriptionID := cl.Subscribe(topic, options...)
cmds[subscriptionID] = command
commands[subscriptionID] = command
}
for m := range cl.Messages {
cmd, ok := cmds[m.SubscriptionID]
command, ok := commands[m.SubscriptionID]
if !ok {
continue
}
log.Debug("%s Dispatching received message: %s", logMessagePrefix(m), m.Raw)
printMessageOrRunCommand(c, m, cmd)
}
return nil
}
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
// check for subscription token then subscription user:pass
if s.Token != "" {
return client.WithBearerAuth(s.Token)
}
if s.User != "" && s.Password != nil {
return client.WithBasicAuth(s.User, *s.Password)
}
// if no subscription token nor subscription user:pass, check for default token then default user:pass
if conf.DefaultToken != "" {
return client.WithBearerAuth(conf.DefaultToken)
}
if conf.DefaultUser != "" && conf.DefaultPassword != nil {
return client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword)
printMessageOrRunCommand(c, m, command)
}
return nil
}
@@ -238,27 +186,27 @@ func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string)
if command != "" {
runCommand(c, command, m)
} else {
log.Debug("%s Printing raw message", logMessagePrefix(m))
fmt.Fprintln(c.App.Writer, m.Raw)
}
}
func runCommand(c *cli.Context, command string, m *client.Message) {
if err := runCommandInternal(c, command, m); err != nil {
log.Warn("%s Command failed: %s", logMessagePrefix(m), err.Error())
fmt.Fprintf(c.App.ErrWriter, "Command failed: %s\n", err.Error())
}
}
func runCommandInternal(c *cli.Context, script string, m *client.Message) error {
scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.%s", os.TempDir(), util.RandomString(10), scriptExt)
log.Debug("%s Running command '%s' via temporary script %s", logMessagePrefix(m), script, scriptFile)
script = scriptHeader + script
if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
scriptFile, err := createTmpScript(command)
if err != nil {
return err
}
defer os.Remove(scriptFile)
log.Debug("%s Executing script %s", logMessagePrefix(m), scriptFile)
cmd := exec.Command(scriptLauncher[0], append(scriptLauncher[1:], scriptFile)...)
verbose := c.Bool("verbose")
if verbose {
log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), command, m.Raw)
}
cmd := exec.Command("sh", "-c", scriptFile)
cmd.Stdin = c.App.Reader
cmd.Stdout = c.App.Writer
cmd.Stderr = c.App.ErrWriter
@@ -266,8 +214,17 @@ func runCommandInternal(c *cli.Context, script string, m *client.Message) error
return cmd.Run()
}
func createTmpScript(command string) (string, error) {
scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.sh.tmp", os.TempDir(), util.RandomString(10))
script := fmt.Sprintf("#!/bin/sh\n%s", command)
if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
return "", err
}
return scriptFile, nil
}
func envVars(m *client.Message) []string {
env := make([]string, 0)
env := os.Environ()
env = append(env, envVar(m.ID, "NTFY_ID", "id")...)
env = append(env, envVar(m.Topic, "NTFY_TOPIC", "topic")...)
env = append(env, envVar(fmt.Sprintf("%d", m.Time), "NTFY_TIME", "time")...)
@@ -276,11 +233,7 @@ func envVars(m *client.Message) []string {
env = append(env, envVar(fmt.Sprintf("%d", m.Priority), "NTFY_PRIORITY", "priority", "prio", "p")...)
env = append(env, envVar(strings.Join(m.Tags, ","), "NTFY_TAGS", "tags", "tag", "ta")...)
env = append(env, envVar(m.Raw, "NTFY_RAW", "raw")...)
sort.Strings(env)
if log.IsTrace() {
log.Trace("%s With environment:\n%s", logMessagePrefix(m), strings.Join(env, "\n"))
}
return append(os.Environ(), env...)
return env
}
func envVar(value string, vars ...string) []string {
@@ -296,30 +249,13 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
if filename != "" {
return client.LoadConfig(filename)
}
configFile := defaultClientConfigFile()
u, _ := user.Current()
configFile := defaultClientRootConfigFile
if u.Uid != "0" {
configFile = util.ExpandHome(defaultClientUserConfigFile)
}
if s, _ := os.Stat(configFile); s != nil {
return client.LoadConfig(configFile)
}
return client.NewConfig(), nil
}
//lint:ignore U1000 Conditionally used in different builds
func defaultClientConfigFileUnix() string {
u, _ := user.Current()
configFile := clientRootConfigFileUnixAbsolute
if u.Uid != "0" {
homeDir, _ := os.UserConfigDir()
return filepath.Join(homeDir, clientUserConfigFileUnixRelative)
}
return configFile
}
//lint:ignore U1000 Conditionally used in different builds
func defaultClientConfigFileWindows() string {
homeDir, _ := os.UserConfigDir()
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative)
}
func logMessagePrefix(m *client.Message) string {
return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID)
}

View File

@@ -1,16 +0,0 @@
package cmd
const (
scriptExt = "sh"
scriptHeader = "#!/bin/sh\n"
clientCommandDescriptionSuffix = `The default config file for all client commands is /etc/ntfy/client.yml (if root user),
or "~/Library/Application Support/ntfy/client.yml" for all other users.`
)
var (
scriptLauncher = []string{"sh", "-c"}
)
func defaultClientConfigFile() string {
return defaultClientConfigFileUnix()
}

View File

@@ -1,312 +0,0 @@
package cmd
import (
"fmt"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCLI_Subscribe_Default_UserPass_Subscription_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/json", 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
subscribe:
- topic: mytopic
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_Subscription_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/json", 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
subscribe:
- topic: mytopic
user: philipp
password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_Subscription_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/json", 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
subscribe:
- topic: mytopic
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_UserPass_Subscription_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/json", 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: fake
default-password: password
subscribe:
- topic: mytopic
user: philipp
password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_Subscription_Empty(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/json", 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
subscribe:
- topic: mytopic
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_UserPass_Subscription_Empty(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/json", 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
subscribe:
- topic: mytopic
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Empty_Subscription_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/json", 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
subscribe:
- topic: mytopic
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Empty_Subscription_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/json", 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
subscribe:
- topic: mytopic
user: philipp
password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_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/json", 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_FAKETOKEN0123456789FAKETOKEN
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_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/json", 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", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass", "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_Subscription_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/json", 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
subscribe:
- topic: mytopic
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
app, _, _, _ := newTestApp()
err := app.Run([]string{"ntfy", "subscribe", "--poll", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
require.Error(t, err)
require.Equal(t, "cannot set both --user and --token", err.Error())
}

View File

@@ -1,18 +0,0 @@
//go:build linux || dragonfly || freebsd || netbsd || openbsd
package cmd
const (
scriptExt = "sh"
scriptHeader = "#!/bin/sh\n"
clientCommandDescriptionSuffix = `The default config file for all client commands is /etc/ntfy/client.yml (if root user),
or ~/.config/ntfy/client.yml for all other users.`
)
var (
scriptLauncher = []string{"sh", "-c"}
)
func defaultClientConfigFile() string {
return defaultClientConfigFileUnix()
}

View File

@@ -1,15 +0,0 @@
package cmd
const (
scriptExt = "bat"
scriptHeader = ""
clientCommandDescriptionSuffix = `The default config file for all client commands is %AppData%\ntfy\client.yml.`
)
var (
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
)
func defaultClientConfigFile() string {
return defaultClientConfigFileWindows()
}

View File

@@ -1,366 +0,0 @@
//go:build !noserver
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
)
func init() {
commands = append(commands, cmdTier)
}
const (
defaultMessageLimit = 5000
defaultMessageExpiryDuration = "12h"
defaultEmailLimit = 20
defaultReservationLimit = 3
defaultAttachmentFileSizeLimit = "15M"
defaultAttachmentTotalSizeLimit = "100M"
defaultAttachmentExpiryDuration = "6h"
defaultAttachmentBandwidthLimit = "1G"
)
var (
flagsTier = append([]cli.Flag{}, flagsUser...)
)
var cmdTier = &cli.Command{
Name: "tier",
Usage: "Manage/show tiers",
UsageText: "ntfy tier [list|add|change|remove] ...",
Flags: flagsTier,
Before: initConfigFileInputSourceFunc("config", flagsUser, initLogFunc),
Category: categoryServer,
Subcommands: []*cli.Command{
{
Name: "add",
Aliases: []string{"a"},
Usage: "Adds a new tier",
UsageText: "ntfy tier add [OPTIONS] CODE",
Action: execTierAdd,
Flags: []cli.Flag{
&cli.StringFlag{Name: "name", Usage: "tier name"},
&cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"},
&cli.StringFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"},
&cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"},
&cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"},
&cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"},
&cli.StringFlag{Name: "attachment-total-size-limit", Value: defaultAttachmentTotalSizeLimit, Usage: "total size limit of attachments for the user"},
&cli.StringFlag{Name: "attachment-expiry-duration", Value: defaultAttachmentExpiryDuration, Usage: "duration after which attachments are deleted"},
&cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"},
&cli.StringFlag{Name: "stripe-monthly-price-id", Usage: "Monthly Stripe price ID for paid tiers (e.g. price_12345)"},
&cli.StringFlag{Name: "stripe-yearly-price-id", Usage: "Yearly Stripe price ID for paid tiers (e.g. price_12345)"},
&cli.BoolFlag{Name: "ignore-exists", Usage: "if the tier already exists, perform no action and exit"},
},
Description: `Add a new tier to the ntfy user database.
Tiers can be used to grant users higher limits, such as daily message limits, attachment size, or
make it possible for users to reserve topics.
This is a server-only command. It directly reads from user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
Examples:
ntfy tier add pro # Add tier with code "pro", using the defaults
ntfy tier add \ # Add a tier with custom limits
--name="Pro" \
--message-limit=10000 \
--message-expiry-duration=24h \
--email-limit=50 \
--reservation-limit=10 \
--attachment-file-size-limit=100M \
--attachment-total-size-limit=1G \
--attachment-expiry-duration=12h \
--attachment-bandwidth-limit=5G \
pro
`,
},
{
Name: "change",
Aliases: []string{"ch"},
Usage: "Change a tier",
UsageText: "ntfy tier change [OPTIONS] CODE",
Action: execTierChange,
Flags: []cli.Flag{
&cli.StringFlag{Name: "name", Usage: "tier name"},
&cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"},
&cli.StringFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"},
&cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"},
&cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"},
&cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"},
&cli.StringFlag{Name: "attachment-total-size-limit", Usage: "total size limit of attachments for the user"},
&cli.StringFlag{Name: "attachment-expiry-duration", Usage: "duration after which attachments are deleted"},
&cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"},
&cli.StringFlag{Name: "stripe-monthly-price-id", Usage: "Monthly Stripe price ID for paid tiers (e.g. price_12345)"},
&cli.StringFlag{Name: "stripe-yearly-price-id", Usage: "Yearly Stripe price ID for paid tiers (e.g. price_12345)"},
},
Description: `Updates a tier to change the limits.
After updating a tier, you may have to restart the ntfy server to apply them
to all visitors.
This is a server-only command. It directly reads from user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
Examples:
ntfy tier change --name="Pro" pro # Update the name of an existing tier
ntfy tier change \ # Update multiple limits and fields
--message-expiry-duration=24h \
--stripe-monthly-price-id=price_1234 \
--stripe-monthly-price-id=price_5678 \
pro
`,
},
{
Name: "remove",
Aliases: []string{"del", "rm"},
Usage: "Removes a tier",
UsageText: "ntfy tier remove CODE",
Action: execTierDel,
Description: `Remove a tier from the ntfy user database.
You cannot remove a tier if there are users associated with a tier. Use "ntfy user change-tier"
to remove or switch their tier first.
This is a server-only command. It directly reads from user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
Example:
ntfy tier del pro
`,
},
{
Name: "list",
Aliases: []string{"l"},
Usage: "Shows a list of tiers",
Action: execTierList,
Description: `Shows a list of all configured tiers.
This is a server-only command. It directly reads from user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
`,
},
},
Description: `Manage tiers of the ntfy server.
The command allows you to add/remove/change tiers in the ntfy user database. Tiers are used
to grant users higher limits, such as daily message limits, attachment size, or make it
possible for users to reserve topics.
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.
Examples:
ntfy tier add pro # Add tier with code "pro", using the defaults
ntfy tier change --name="Pro" pro # Update the name of an existing tier
ntfy tier del pro # Delete an existing tier
`,
}
func execTierAdd(c *cli.Context) error {
code := c.Args().Get(0)
if code == "" {
return errors.New("tier code expected, type 'ntfy tier add --help' for help")
} else if !user.AllowedTier(code) {
return errors.New("tier code must consist only of numbers and letters")
} else if c.String("stripe-monthly-price-id") != "" && c.String("stripe-yearly-price-id") == "" {
return errors.New("if stripe-monthly-price-id is set, stripe-yearly-price-id must also be set")
} else if c.String("stripe-monthly-price-id") == "" && c.String("stripe-yearly-price-id") != "" {
return errors.New("if stripe-yearly-price-id is set, stripe-monthly-price-id must also be set")
}
manager, err := createUserManager(c)
if err != nil {
return err
}
if tier, _ := manager.Tier(code); tier != nil {
if c.Bool("ignore-exists") {
fmt.Fprintf(c.App.ErrWriter, "tier %s already exists (exited successfully)\n", code)
return nil
}
return fmt.Errorf("tier %s already exists", code)
}
name := c.String("name")
if name == "" {
name = code
}
messageExpiryDuration, err := util.ParseDuration(c.String("message-expiry-duration"))
if err != nil {
return err
}
attachmentFileSizeLimit, err := util.ParseSize(c.String("attachment-file-size-limit"))
if err != nil {
return err
}
attachmentTotalSizeLimit, err := util.ParseSize(c.String("attachment-total-size-limit"))
if err != nil {
return err
}
attachmentBandwidthLimit, err := util.ParseSize(c.String("attachment-bandwidth-limit"))
if err != nil {
return err
}
attachmentExpiryDuration, err := util.ParseDuration(c.String("attachment-expiry-duration"))
if err != nil {
return err
}
tier := &user.Tier{
ID: "", // Generated
Code: code,
Name: name,
MessageLimit: c.Int64("message-limit"),
MessageExpiryDuration: messageExpiryDuration,
EmailLimit: c.Int64("email-limit"),
ReservationLimit: c.Int64("reservation-limit"),
AttachmentFileSizeLimit: attachmentFileSizeLimit,
AttachmentTotalSizeLimit: attachmentTotalSizeLimit,
AttachmentExpiryDuration: attachmentExpiryDuration,
AttachmentBandwidthLimit: attachmentBandwidthLimit,
StripeMonthlyPriceID: c.String("stripe-monthly-price-id"),
StripeYearlyPriceID: c.String("stripe-yearly-price-id"),
}
if err := manager.AddTier(tier); err != nil {
return err
}
tier, err = manager.Tier(code)
if err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "tier added\n\n")
printTier(c, tier)
return nil
}
func execTierChange(c *cli.Context) error {
code := c.Args().Get(0)
if code == "" {
return errors.New("tier code expected, type 'ntfy tier change --help' for help")
} else if !user.AllowedTier(code) {
return errors.New("tier code must consist only of numbers and letters")
}
manager, err := createUserManager(c)
if err != nil {
return err
}
tier, err := manager.Tier(code)
if err == user.ErrTierNotFound {
return fmt.Errorf("tier %s does not exist", code)
} else if err != nil {
return err
}
if c.IsSet("name") {
tier.Name = c.String("name")
}
if c.IsSet("message-limit") {
tier.MessageLimit = c.Int64("message-limit")
}
if c.IsSet("message-expiry-duration") {
tier.MessageExpiryDuration, err = util.ParseDuration(c.String("message-expiry-duration"))
if err != nil {
return err
}
}
if c.IsSet("email-limit") {
tier.EmailLimit = c.Int64("email-limit")
}
if c.IsSet("reservation-limit") {
tier.ReservationLimit = c.Int64("reservation-limit")
}
if c.IsSet("attachment-file-size-limit") {
tier.AttachmentFileSizeLimit, err = util.ParseSize(c.String("attachment-file-size-limit"))
if err != nil {
return err
}
}
if c.IsSet("attachment-total-size-limit") {
tier.AttachmentTotalSizeLimit, err = util.ParseSize(c.String("attachment-total-size-limit"))
if err != nil {
return err
}
}
if c.IsSet("attachment-expiry-duration") {
tier.AttachmentExpiryDuration, err = util.ParseDuration(c.String("attachment-expiry-duration"))
if err != nil {
return err
}
}
if c.IsSet("attachment-bandwidth-limit") {
tier.AttachmentBandwidthLimit, err = util.ParseSize(c.String("attachment-bandwidth-limit"))
if err != nil {
return err
}
}
if c.IsSet("stripe-monthly-price-id") {
tier.StripeMonthlyPriceID = c.String("stripe-monthly-price-id")
}
if c.IsSet("stripe-yearly-price-id") {
tier.StripeYearlyPriceID = c.String("stripe-yearly-price-id")
}
if tier.StripeMonthlyPriceID != "" && tier.StripeYearlyPriceID == "" {
return errors.New("if stripe-monthly-price-id is set, stripe-yearly-price-id must also be set")
} else if tier.StripeMonthlyPriceID == "" && tier.StripeYearlyPriceID != "" {
return errors.New("if stripe-yearly-price-id is set, stripe-monthly-price-id must also be set")
}
if err := manager.UpdateTier(tier); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "tier updated\n\n")
printTier(c, tier)
return nil
}
func execTierDel(c *cli.Context) error {
code := c.Args().Get(0)
if code == "" {
return errors.New("tier code expected, type 'ntfy tier del --help' for help")
}
manager, err := createUserManager(c)
if err != nil {
return err
}
if _, err := manager.Tier(code); err == user.ErrTierNotFound {
return fmt.Errorf("tier %s does not exist", code)
}
if err := manager.RemoveTier(code); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "tier %s removed\n", code)
return nil
}
func execTierList(c *cli.Context) error {
manager, err := createUserManager(c)
if err != nil {
return err
}
tiers, err := manager.Tiers()
if err != nil {
return err
}
for _, tier := range tiers {
printTier(c, tier)
}
return nil
}
func printTier(c *cli.Context, tier *user.Tier) {
prices := "(none)"
if tier.StripeMonthlyPriceID != "" && tier.StripeYearlyPriceID != "" {
prices = fmt.Sprintf("%s / %s", tier.StripeMonthlyPriceID, tier.StripeYearlyPriceID)
}
fmt.Fprintf(c.App.ErrWriter, "tier %s (id: %s)\n", tier.Code, tier.ID)
fmt.Fprintf(c.App.ErrWriter, "- Name: %s\n", tier.Name)
fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit)
fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds()))
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit))
fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit))
fmt.Fprintf(c.App.ErrWriter, "- Stripe prices (monthly/yearly): %s\n", prices)
}

View File

@@ -1,67 +0,0 @@
package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"testing"
)
func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, _, _, stderr := newTestApp()
require.Nil(t, runTierCommand(app, conf, "add", "--name", "Pro", "--message-limit", "1234", "pro"))
require.Contains(t, stderr.String(), "tier added\n\ntier pro (id: ti_")
err := runTierCommand(app, conf, "add", "pro")
require.NotNil(t, err)
require.Equal(t, "tier pro already exists", err.Error())
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "list"))
require.Contains(t, stderr.String(), "tier pro (id: ti_")
require.Contains(t, stderr.String(), "- Name: Pro")
require.Contains(t, stderr.String(), "- Message limit: 1234")
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "change",
"--message-limit=999",
"--message-expiry-duration=2d",
"--email-limit=91",
"--reservation-limit=98",
"--attachment-file-size-limit=100m",
"--attachment-expiry-duration=1d",
"--attachment-total-size-limit=10G",
"--attachment-bandwidth-limit=100G",
"--stripe-monthly-price-id=price_991",
"--stripe-yearly-price-id=price_992",
"pro",
))
require.Contains(t, stderr.String(), "- Message limit: 999")
require.Contains(t, stderr.String(), "- Message expiry duration: 48h")
require.Contains(t, stderr.String(), "- Email limit: 91")
require.Contains(t, stderr.String(), "- Reservation limit: 98")
require.Contains(t, stderr.String(), "- Attachment file size limit: 100.0 MB")
require.Contains(t, stderr.String(), "- Attachment expiry duration: 24h")
require.Contains(t, stderr.String(), "- Attachment total size limit: 10.0 GB")
require.Contains(t, stderr.String(), "- Stripe prices (monthly/yearly): price_991 / price_992")
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
require.Contains(t, stderr.String(), "tier pro removed")
}
func runTierCommand(app *cli.App, conf *server.Config, args ...string) error {
userArgs := []string{
"ntfy",
"--log-level=ERROR",
"tier",
"--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...))
}

View File

@@ -1,210 +0,0 @@
//go:build !noserver
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"net/netip"
"time"
)
func init() {
commands = append(commands, cmdToken)
}
var flagsToken = append([]cli.Flag{}, flagsUser...)
var cmdToken = &cli.Command{
Name: "token",
Usage: "Create, list or delete user tokens",
UsageText: "ntfy token [list|add|remove] ...",
Flags: flagsToken,
Before: initConfigFileInputSourceFunc("config", flagsToken, initLogFunc),
Category: categoryServer,
Subcommands: []*cli.Command{
{
Name: "add",
Aliases: []string{"a"},
Usage: "Create a new token",
UsageText: "ntfy token add [--expires=<duration>] [--label=..] USERNAME",
Action: execTokenAdd,
Flags: []cli.Flag{
&cli.StringFlag{Name: "expires", Aliases: []string{"e"}, Value: "", Usage: "token expires after"},
&cli.StringFlag{Name: "label", Aliases: []string{"l"}, Value: "", Usage: "token label"},
},
Description: `Create a new user access token.
User access tokens can be used to publish, subscribe, or perform any other user-specific tasks.
Tokens have full access, and can perform any task a user can do. They are meant to be used to
avoid spreading the password to various places.
This is a server-only command. It directly reads from user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
Examples:
ntfy token add phil # Create token for user phil which never expires
ntfy token add --expires=2d phil # Create token for user phil which expires in 2 days
ntfy token add -e "tuesday, 8pm" phil # Create token for user phil which expires next Tuesday
ntfy token add -l backups phil # Create token for user phil with label "backups"`,
},
{
Name: "remove",
Aliases: []string{"del", "rm"},
Usage: "Removes a token",
UsageText: "ntfy token remove USERNAME TOKEN",
Action: execTokenDel,
Description: `Remove a token from the ntfy user database.
Example:
ntfy token del phil tk_th2srHVlxrANQHAso5t0HuQ1J1TjN`,
},
{
Name: "list",
Aliases: []string{"l"},
Usage: "Shows a list of tokens",
Action: execTokenList,
Description: `Shows a list of all tokens.
This is a server-only command. It directly reads from user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.`,
},
},
Description: `Manage access tokens for individual users.
User access tokens can be used to publish, subscribe, or perform any other user-specific tasks.
Tokens have full access, and can perform any task a user can do. They are meant to be used to
avoid spreading the password to various places.
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.
Examples:
ntfy token list # Shows list of tokens for all users
ntfy token list phil # Shows list of tokens for user phil
ntfy token add phil # Create token for user phil which never expires
ntfy token add --expires=2d phil # Create token for user phil which expires in 2 days
ntfy token remove phil tk_th2srHVlxr... # Delete token`,
}
func execTokenAdd(c *cli.Context) error {
username := c.Args().Get(0)
expiresStr := c.String("expires")
label := c.String("label")
if username == "" {
return errors.New("username expected, type 'ntfy token add --help' for help")
} else if username == userEveryone || username == user.Everyone {
return errors.New("username not allowed")
}
expires := time.Unix(0, 0)
if expiresStr != "" {
var err error
expires, err = util.ParseFutureTime(expiresStr, time.Now())
if err != nil {
return err
}
}
manager, err := createUserManager(c)
if err != nil {
return err
}
u, err := manager.User(username)
if err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
}
token, err := manager.CreateToken(u.ID, label, expires, netip.IPv4Unspecified())
if err != nil {
return err
}
if expires.Unix() == 0 {
fmt.Fprintf(c.App.ErrWriter, "token %s created for user %s, never expires\n", token.Value, u.Name)
} else {
fmt.Fprintf(c.App.ErrWriter, "token %s created for user %s, expires %v\n", token.Value, u.Name, expires.Format(time.UnixDate))
}
return nil
}
func execTokenDel(c *cli.Context) error {
username, token := c.Args().Get(0), c.Args().Get(1)
if username == "" || token == "" {
return errors.New("username and token expected, type 'ntfy token remove --help' for help")
} else if username == userEveryone || username == user.Everyone {
return errors.New("username not allowed")
}
manager, err := createUserManager(c)
if err != nil {
return err
}
u, err := manager.User(username)
if err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
}
if err := manager.RemoveToken(u.ID, token); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "token %s for user %s removed\n", token, username)
return nil
}
func execTokenList(c *cli.Context) error {
username := c.Args().Get(0)
if username == userEveryone || username == user.Everyone {
return errors.New("username not allowed")
}
manager, err := createUserManager(c)
if err != nil {
return err
}
var users []*user.User
if username != "" {
u, err := manager.User(username)
if err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
}
users = append(users, u)
} else {
users, err = manager.Users()
if err != nil {
return err
}
}
usersWithTokens := 0
for _, u := range users {
tokens, err := manager.Tokens(u.ID)
if err != nil {
return err
} else if len(tokens) == 0 && username != "" {
fmt.Fprintf(c.App.ErrWriter, "user %s has no access tokens\n", username)
return nil
} else if len(tokens) == 0 {
continue
}
usersWithTokens++
fmt.Fprintf(c.App.ErrWriter, "user %s\n", u.Name)
for _, t := range tokens {
var label, expires string
if t.Label != "" {
label = fmt.Sprintf(" (%s)", t.Label)
}
if t.Expires.Unix() == 0 {
expires = "never expires"
} else {
expires = fmt.Sprintf("expires %s", t.Expires.Format(time.RFC822))
}
fmt.Fprintf(c.App.ErrWriter, "- %s%s, %s, accessed from %s at %s\n", t.Value, label, expires, t.LastOrigin.String(), t.LastAccess.Format(time.RFC822))
}
}
if usersWithTokens == 0 {
fmt.Fprintf(c.App.ErrWriter, "no users with tokens\n")
}
return nil
}

View File

@@ -1,50 +0,0 @@
package cmd
import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"regexp"
"testing"
)
func TestCLI_Token_AddListRemove(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user")
app, _, _, stderr = newTestApp()
require.Nil(t, runTokenCommand(app, conf, "add", "phil"))
require.Regexp(t, `token tk_.+ created for user phil, never expires`, stderr.String())
app, _, _, stderr = newTestApp()
require.Nil(t, runTokenCommand(app, conf, "list", "phil"))
require.Regexp(t, `user phil\n- tk_.+, never expires, accessed from 0.0.0.0 at .+`, stderr.String())
re := regexp.MustCompile(`tk_\w+`)
token := re.FindString(stderr.String())
app, _, _, stderr = newTestApp()
require.Nil(t, runTokenCommand(app, conf, "remove", "phil", token))
require.Regexp(t, fmt.Sprintf("token %s for user phil removed", token), stderr.String())
app, _, _, stderr = newTestApp()
require.Nil(t, runTokenCommand(app, conf, "list"))
require.Equal(t, "no users with tokens\n", stderr.String())
}
func runTokenCommand(app *cli.App, conf *server.Config, args ...string) error {
userArgs := []string{
"ntfy",
"--log-level=ERROR",
"token",
"--config=" + conf.File, // Dummy config file to avoid lookups of real file
"--auth-file=" + conf.AuthFile,
}
return app.Run(append(userArgs, args...))
}

View File

@@ -1,52 +1,33 @@
//go:build !noserver
package cmd
import (
"crypto/subtle"
"errors"
"fmt"
"heckel.io/ntfy/user"
"os"
"strings"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/util"
"strings"
)
const (
tierReset = "-"
)
func init() {
commands = append(commands, cmdUser)
}
var flagsUser = append(
append([]cli.Flag{}, flagsDefault...),
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
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-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"}),
)
var flagsUser = userCommandFlags()
var cmdUser = &cli.Command{
Name: "user",
Usage: "Manage/show users",
UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...",
Flags: flagsUser,
Before: initConfigFileInputSourceFunc("config", flagsUser, initLogFunc),
Before: initConfigFileInputSource("config", flagsUser),
Category: categoryServer,
Subcommands: []*cli.Command{
{
Name: "add",
Aliases: []string{"a"},
Usage: "Adds a new user",
UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME",
UsageText: "ntfy user add [--role=admin|user] USERNAME",
Action: execUserAdd,
Flags: []cli.Flag{
&cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(user.RoleUser), Usage: "user role"},
&cli.BoolFlag{Name: "ignore-exists", Usage: "if the user already exists, perform no action and exit"},
&cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(auth.RoleUser), Usage: "user role"},
},
Description: `Add a new user to the ntfy user database.
@@ -55,12 +36,8 @@ granted otherwise by the auth-default-access setting). An admin user has read an
topics.
Examples:
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts)
You may set the NTFY_PASSWORD environment variable to pass the password. This is useful if
you are creating users via scripts.
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
`,
},
{
@@ -79,7 +56,7 @@ Example:
Name: "change-pass",
Aliases: []string{"chp"},
Usage: "Changes a user's password",
UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME",
UsageText: "ntfy user change-pass USERNAME",
Action: execUserChangePass,
Description: `Change the password for the given user.
@@ -87,12 +64,7 @@ The new password will be read from STDIN, and it'll be confirmed by typing
it twice.
Example:
ntfy user change-pass phil
NTFY_PASSWORD=.. ntfy user change-pass phil
You may set the NTFY_PASSWORD environment variable to pass the new password. This is
useful if you are updating users via scripts.
ntfy user change-pass phil
`,
},
{
@@ -115,22 +87,6 @@ user are removed, since they are no longer necessary.
Example:
ntfy user change-role phil admin # Make user phil an admin
ntfy user change-role phil user # Remove admin role from user phil
`,
},
{
Name: "change-tier",
Aliases: []string{"cht"},
Usage: "Changes the tier of a user",
UsageText: "ntfy user change-tier USERNAME (TIER|-)",
Action: execUserChangeTier,
Description: `Change the tier for the given user.
This command can be used to change the tier of a user. Tiers define usage limits, such
as messages per day, attachment file sizes, etc.
Example:
ntfy user change-tier phil pro # Change tier to "pro" for user "phil"
ntfy user change-tier phil - # Remove tier from user "phil" entirely
`,
},
{
@@ -140,66 +96,52 @@ Example:
Action: execUserList,
Description: `Shows a list of all configured users, including the everyone ('*') user.
This command is an alias to calling 'ntfy access' (display access control list).
This is a server-only command. It directly reads from the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
This is a server-only command. It directly reads from user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
This command is an alias to calling 'ntfy access' (display access control list).
`,
},
},
Description: `Manage users of the ntfy server.
The command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles.
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 access'.
Examples:
ntfy user list # Shows list of users (alias: 'ntfy access')
ntfy user add phil # Add regular user phil
NTFY_PASSWORD=... ntfy user add phil # As above, using env variable to set password (for scripts)
ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil
ntfy user change-pass phil # Change password for user phil
NTFY_PASSWORD=.. ntfy user change-pass phil # As above, using env variable to set password (for scripts)
ntfy user change-role phil admin # Make user phil an admin
The command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles.
For the 'ntfy user add' and 'ntfy user change-pass' commands, you may set the NTFY_PASSWORD environment
variable to pass the new password. This is useful if you are creating/updating users via scripts.
Examples:
ntfy user list # Shows list of users (alias: 'ntfy access')
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil
ntfy user change-pass phil # Change password for user phil
ntfy user change-role phil admin # Make user phil an admin
`,
}
func execUserAdd(c *cli.Context) error {
username := c.Args().Get(0)
role := user.Role(c.String("role"))
password := os.Getenv("NTFY_PASSWORD")
role := auth.Role(c.String("role"))
if username == "" {
return errors.New("username expected, type 'ntfy user add --help' for help")
} else if username == userEveryone || username == user.Everyone {
} else if username == userEveryone {
return errors.New("username not allowed")
} else if !user.AllowedRole(role) {
} else if !auth.AllowedRole(role) {
return errors.New("role must be either 'user' or 'admin'")
}
manager, err := createUserManager(c)
manager, err := createAuthManager(c)
if err != nil {
return err
}
if user, _ := manager.User(username); user != nil {
if c.Bool("ignore-exists") {
fmt.Fprintf(c.App.ErrWriter, "user %s already exists (exited successfully)\n", username)
return nil
}
return fmt.Errorf("user %s already exists", username)
}
if password == "" {
p, err := readPasswordAndConfirm(c)
if err != nil {
return err
}
password = p
password, err := readPasswordAndConfirm(c)
if err != nil {
return err
}
if err := manager.AddUser(username, password, role); err != nil {
return err
@@ -212,14 +154,14 @@ func execUserDel(c *cli.Context) error {
username := c.Args().Get(0)
if username == "" {
return errors.New("username expected, type 'ntfy user del --help' for help")
} else if username == userEveryone || username == user.Everyone {
} else if username == userEveryone {
return errors.New("username not allowed")
}
manager, err := createUserManager(c)
manager, err := createAuthManager(c)
if err != nil {
return err
}
if _, err := manager.User(username); err == user.ErrUserNotFound {
if _, err := manager.User(username); err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if err := manager.RemoveUser(username); err != nil {
@@ -231,24 +173,21 @@ func execUserDel(c *cli.Context) error {
func execUserChangePass(c *cli.Context) error {
username := c.Args().Get(0)
password := os.Getenv("NTFY_PASSWORD")
if username == "" {
return errors.New("username expected, type 'ntfy user change-pass --help' for help")
} else if username == userEveryone || username == user.Everyone {
} else if username == userEveryone {
return errors.New("username not allowed")
}
manager, err := createUserManager(c)
manager, err := createAuthManager(c)
if err != nil {
return err
}
if _, err := manager.User(username); err == user.ErrUserNotFound {
if _, err := manager.User(username); err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if password == "" {
password, err = readPasswordAndConfirm(c)
if err != nil {
return err
}
password, err := readPasswordAndConfirm(c)
if err != nil {
return err
}
if err := manager.ChangePassword(username, password); err != nil {
return err
@@ -259,17 +198,17 @@ func execUserChangePass(c *cli.Context) error {
func execUserChangeRole(c *cli.Context) error {
username := c.Args().Get(0)
role := user.Role(c.Args().Get(1))
if username == "" || !user.AllowedRole(role) {
role := auth.Role(c.Args().Get(1))
if username == "" || !auth.AllowedRole(role) {
return errors.New("username and new role expected, type 'ntfy user change-role --help' for help")
} else if username == userEveryone || username == user.Everyone {
} else if username == userEveryone {
return errors.New("username not allowed")
}
manager, err := createUserManager(c)
manager, err := createAuthManager(c)
if err != nil {
return err
}
if _, err := manager.User(username); err == user.ErrUserNotFound {
if _, err := manager.User(username); err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if err := manager.ChangeRole(username, role); err != nil {
@@ -279,39 +218,8 @@ func execUserChangeRole(c *cli.Context) error {
return nil
}
func execUserChangeTier(c *cli.Context) error {
username := c.Args().Get(0)
tier := c.Args().Get(1)
if username == "" {
return errors.New("username and new tier expected, type 'ntfy user change-tier --help' for help")
} else if !user.AllowedTier(tier) && tier != tierReset {
return errors.New("invalid tier, must be tier code, or - to reset")
} else if username == userEveryone || username == user.Everyone {
return errors.New("username not allowed")
}
manager, err := createUserManager(c)
if err != nil {
return err
}
if _, err := manager.User(username); err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if tier == tierReset {
if err := manager.ResetTier(username); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "removed tier from user %s\n", username)
} else {
if err := manager.ChangeTier(username, tier); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "changed tier for user %s to %s\n", username, tier)
}
return nil
}
func execUserList(c *cli.Context) error {
manager, err := createUserManager(c)
manager, err := createAuthManager(c)
if err != nil {
return err
}
@@ -322,20 +230,19 @@ func execUserList(c *cli.Context) error {
return showUsers(c, manager, users)
}
func createUserManager(c *cli.Context) (*user.Manager, error) {
func createAuthManager(c *cli.Context) (auth.Manager, error) {
authFile := c.String("auth-file")
authStartupQueries := c.String("auth-startup-queries")
authDefaultAccess := c.String("auth-default-access")
if authFile == "" {
return nil, errors.New("option auth-file not set; auth is unconfigured for this server")
} else if !util.FileExists(authFile) {
return nil, errors.New("auth-file does not exist; please start the server at least once to create it")
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'")
}
authDefault, err := user.ParsePermission(authDefaultAccess)
if err != nil {
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
}
return user.NewManager(authFile, authStartupQueries, authDefault, user.DefaultUserPasswordBcryptCost, user.DefaultUserStatsQueueWriterInterval)
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
return auth.NewSQLiteAuth(authFile, authDefaultRead, authDefaultWrite)
}
func readPasswordAndConfirm(c *cli.Context) (string, error) {
@@ -355,3 +262,11 @@ func readPasswordAndConfirm(c *cli.Context) (string, error) {
}
return string(password), nil
}
func userCommandFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
}
}

View File

@@ -5,8 +5,6 @@ import (
"github.com/urfave/cli/v2"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"heckel.io/ntfy/user"
"os"
"path/filepath"
"testing"
)
@@ -114,12 +112,10 @@ func TestCLI_User_Delete(t *testing.T) {
}
func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config, port int) {
configFile := filepath.Join(t.TempDir(), "server-dummy.yml")
require.Nil(t, os.WriteFile(configFile, []byte(""), 0600)) // Dummy config file to avoid lookup of real server.yml
conf = server.NewConfig()
conf.File = configFile
conf.AuthFile = filepath.Join(t.TempDir(), "user.db")
conf.AuthDefault = user.PermissionDenyAll
conf.AuthDefaultRead = false
conf.AuthDefaultWrite = false
s, port = test.StartServerWithConfig(t, conf)
return
}
@@ -127,11 +123,23 @@ func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config,
func runUserCommand(app *cli.App, conf *server.Config, args ...string) error {
userArgs := []string{
"ntfy",
"--log-level=ERROR",
"user",
"--config=" + conf.File, // Dummy config file to avoid lookups of real file
"--auth-file=" + conf.AuthFile,
"--auth-default-access=" + conf.AuthDefault.String(),
"--auth-default-access=" + confToDefaultAccess(conf),
}
return app.Run(append(userArgs, args...))
}
func confToDefaultAccess(conf *server.Config) string {
var defaultAccess string
if conf.AuthDefaultRead && conf.AuthDefaultWrite {
defaultAccess = "read-write"
} else if conf.AuthDefaultRead && !conf.AuthDefaultWrite {
defaultAccess = "read-only"
} else if !conf.AuthDefaultRead && conf.AuthDefaultWrite {
defaultAccess = "write-only"
} else if !conf.AuthDefaultRead && !conf.AuthDefaultWrite {
defaultAccess = "deny-all"
}
return defaultAccess
}

View File

@@ -1,17 +0,0 @@
version: "2.1"
services:
ntfy:
image: binwiederhier/ntfy
container_name: ntfy
command:
- serve
environment:
- TZ=UTC # optional: Change to your desired timezone
user: UID:GID # optional: Set custom user/group or uid/gid
volumes:
- /var/cache/ntfy:/var/cache/ntfy
- /etc/ntfy:/etc/ntfy
ports:
- 80:80
restart: unless-stopped

View File

@@ -1,50 +0,0 @@
{% extends "base.html" %}
{% block announce %}
<style>
div[data-md-component="announce"] {
z-index: 10;
}
div[data-md-component="announce"] a {
color: white;
}
div[data-md-component="announce"] a:hover, div[data-md-component="announce"] a:focus {
transition: ease-in 150ms;
color: #ccc;
}
div[data-md-component="announce"] .md-banner__button {
color: #ccc;
}
div[data-md-component="announce"] .md-banner.hidden {
display: none;
}
div[data-md-component="announce"] .twemoji {
margin-top: 2px;
}
</style>
<button id="announce-bar-close" class="md-banner__button md-icon" aria-label="Don't show this again">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"></path>
</svg>
</button>
If you like ntfy, please consider sponsoring it via <a target="_blank" href="https://github.com/sponsors/binwiederhier"><strong>GitHub Sponsors</strong></a>
or <a target="_blank" href="https://en.liberapay.com/ntfy/"><strong>Liberapay</strong></a>
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 36 36" class="twemoji md-footer-custom-text">
<path fill="#DD2E44" d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z"/>
</svg>
<script>
announceBarKey = 'announce-bar-closed-sponsor';
document.getElementById('announce-bar-close').addEventListener('click', (e) => {
localStorage.setItem(announceBarKey, 'true');
document.querySelector('div[data-md-component="announce"] .md-banner').style.display = 'none';
});
if (localStorage.getItem(announceBarKey) === 'true') {
document.querySelector('div[data-md-component="announce"] .md-banner').style.display = 'none';
}
</script>
{% endblock %}

View File

@@ -161,7 +161,6 @@ ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil
ntfy user change-pass phil # Change password for user phil
ntfy user change-role phil admin # Make user phil an admin
ntfy user change-tier phil pro # Change phil's tier to "pro"
```
### Access control list (ACL)
@@ -223,45 +222,12 @@ User `ben` has three topic-specific entries. He can read, but not write to topic
to topic `garagedoor` and all topics starting with the word `alerts` (wildcards). Clients that are not authenticated
(called `*`/`everyone`) only have read access to the `announcements` and `server-stats` topics.
### Access tokens
In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful
to avoid having to configure your password across multiple publishing/subscribing applications. For instance, you may
want to use a dedicated token to publish from your backup host, and one from your home automation system.
!!! info
As of today, access tokens grant users **full access to the user account**. Aside from changing the password,
and deleting the account, every action can be performed with a token. Granular access tokens are on the roadmap,
but not yet implemented.
The `ntfy token` command can be used to manage access tokens for users. Tokens can have labels, and they can expire
automatically (or never expire). Each user can have up to 20 tokens (hardcoded).
**Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details):
```
ntfy token list # Shows list of tokens for all users
ntfy token list phil # Shows list of tokens for user phil
ntfy token add phil # Create token for user phil which never expires
ntfy token add --expires=2d phil # Create token for user phil which expires in 2 days
ntfy token remove phil tk_th2sxr... # Delete token
```
**Creating an access token:**
```
$ ntfy token add --expires=30d --label="backups" phil
$ ntfy token list
user phil
- tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 (backups), expires 15 Mar 23 14:33 EDT, accessed from 0.0.0.0 at 13 Feb 23 13:33 EST
```
Once an access token is created, you can **use it to authenticate against the ntfy server, e.g. when you publish or
subscribe to topics**. To learn how, check out [authenticate via access tokens](publish.md#access-tokens).
### Example: Private instance
The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`:
=== "/etc/ntfy/server.yml"
``` yaml
auth-file: "/var/lib/ntfy/user.db"
auth-file "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
```
@@ -343,25 +309,6 @@ with the given username/password. Be sure to use HTTPS to avoid eavesdropping an
]));
```
### Example: UnifiedPush
[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages.
The topic names used by UnifiedPush all start with the `up*` prefix. Please refer to the
**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users)** for more details.
To enable support for UnifiedPush for private servers (i.e. `auth-default-access: "deny-all"`), you should either
allow anonymous write access for the entire prefix or explicitly per topic:
=== "Prefix"
```
$ ntfy access '*' 'up*' write-only
```
=== "Explicitly"
```
$ ntfy access '*' upYzMtZGZiYTY5 write-only
```
## E-mail notifications
To allow forwarding messages via e-mail, you can configure an **SMTP server for outgoing messages**. Once configured,
you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g.
@@ -494,16 +441,8 @@ by forwarding the `Connection` and `Upgrade` headers accordingly.
In this example, ntfy runs on `:2586` and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic
or the root domain:
=== "nginx (convenient)"
=== "nginx (/etc/nginx/sites-*/ntfy)"
```
# /etc/nginx/sites-*/ntfy
#
# This config allows insecure HTTP POST/PUT requests against topics to allow a short curl syntax (without -L
# and "https://" prefix). It also disables output buffering, which has worked well for the ntfy.sh server.
#
# This is pretty much how ntfy.sh is configured. To see the exact configuration,
# see https://github.com/binwiederhier/ntfy-ansible/
server {
listen 80;
server_name ntfy.sh;
@@ -538,22 +477,19 @@ or the root domain:
proxy_send_timeout 3m;
proxy_read_timeout 3m;
client_max_body_size 0; # Stream request body to backend
client_max_body_size 20m; # Must be >= attachment-file-size-limit in /etc/ntfy/server.yml
}
}
server {
listen 443 ssl http2;
listen 443 ssl;
server_name ntfy.sh;
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
@@ -574,78 +510,13 @@ or the root domain:
proxy_send_timeout 3m;
proxy_read_timeout 3m;
client_max_body_size 0; # Stream request body to backend
client_max_body_size 20m; # Must be >= attachment-file-size-limit in /etc/ntfy/server.yml
}
}
```
=== "nginx (more secure)"
=== "Apache2 (/etc/apache2/sites-*/ntfy.conf)"
```
# /etc/nginx/sites-*/ntfy
#
# This config requires the use of the -L flag in curl to redirect to HTTPS, and it keeps nginx output buffering
# enabled. While recommended, I have had issues with that in the past.
server {
listen 80;
server_name ntfy.sh;
location / {
return 302 https://$http_host$request_uri$is_args$query_string;
proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 3m;
proxy_send_timeout 3m;
proxy_read_timeout 3m;
client_max_body_size 0; # Stream request body to backend
}
}
server {
listen 443 ssl http2;
server_name ntfy.sh;
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
location / {
proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 3m;
proxy_send_timeout 3m;
proxy_read_timeout 3m;
client_max_body_size 0; # Stream request body to backend
}
}
```
=== "Apache2"
```
# /etc/apache2/sites-*/ntfy.conf
<VirtualHost *:80>
ServerName ntfy.sh
@@ -747,113 +618,6 @@ Example:
firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
```
## iOS instant notifications
Unlike Android, iOS heavily restricts background processing, which sadly makes it impossible to implement instant
push notifications without a central server.
To still support instant notifications on iOS through your self-hosted ntfy server, you have to forward so called `poll_request`
messages to the main ntfy.sh server (or any upstream server that's APNS/Firebase connected, if you build your own iOS app),
which will then forward it to Firebase/APNS.
To configure it, simply set `upstream-base-url` like so:
``` yaml
upstream-base-url: "https://ntfy.sh"
```
If set, all incoming messages will publish a poll request to the configured upstream server, containing
the message ID of the original message, instructing the iOS app to poll this server for the actual message contents.
If `upstream-base-url` is not set, notifications will still eventually get to your device, but delivery can take hours,
depending on the state of the phone. If you are using your phone, it shouldn't take more than 20-30 minutes though.
In case you're curious, here's an example of the entire flow:
- In the iOS app, you subscribe to `https://ntfy.example.com/mytopic`
- The app subscribes to the Firebase topic `6de73be8dfb7d69e...` (the SHA256 of the topic URL)
- When you publish a message to `https://ntfy.example.com/mytopic`, your ntfy server will publish a
poll request to `https://ntfy.sh/6de73be8dfb7d69e...`. The request from your server to the upstream server
contains only the message ID (in the `X-Poll-ID` header), and the SHA256 checksum of the topic URL (as upstream topic).
- The ntfy.sh server publishes the poll request message to Firebase, which forwards it to APNS, which forwards it to your iOS device
- Your iOS device receives the poll request, and fetches the actual message from your server, and then displays it
Here's an example of what the self-hosted server forwards to the upstream server. The request is equivalent to this curl:
```
curl -X POST -H "X-Poll-ID: s4PdJozxM8na" https://ntfy.sh/6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b
{"id":"4HsClFEuCIcs","time":1654087955,"event":"poll_request","topic":"6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b","message":"New message","poll_id":"s4PdJozxM8na"}
```
Note that the self-hosted server literally sends the message `New message` for every message, even if your message
may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all),
it'll show `New message` as a popup.
## Tiers
ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as
daily message limits, attachment size, or make it possible for users to reserve topics. If [payments are enabled](#payments),
tiers can be paid or unpaid, and users can upgrade/downgrade between them. If payments are disabled, then the only way
to switch between tiers is with the `ntfy user change-tier` command (see [users and roles](#users-and-roles)).
By default, **newly created users have no tier**, and all usage limits are read from the `server.yml` config file.
Once a user is associated with a tier, some limits are overridden based on the tier.
The `ntfy tier` command can be used to manage all available tiers. By default, there are no pre-defined tiers.
**Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details):
```
ntfy tier add pro # Add tier with code "pro", using the defaults
ntfy tier change --name="Pro" pro # Update the name of an existing tier
ntfy tier del starter # Delete an existing tier
ntfy user change-tier phil pro # Switch user "phil" to tier "pro"
```
**Creating a tier (full example):**
```
ntfy tier add \
--name="Pro" \
--message-limit=10000 \
--message-expiry-duration=24h \
--email-limit=50 \
--reservation-limit=10 \
--attachment-file-size-limit=100M \
--attachment-total-size-limit=1G \
--attachment-expiry-duration=12h \
--attachment-bandwidth-limit=5G \
--stripe-price-id=price_123456 \
pro
```
## Payments
ntfy supports paid [tiers](#tiers) via [Stripe](https://stripe.com/) as a payment provider. If payments are enabled,
users can register, login and switch plans in the web app. The web app will behave slightly differently if payments
are enabled (e.g. showing an upgrade banner, or "ntfy Pro" tags).
!!! info
The ntfy payments integration is very tailored to ntfy.sh and Stripe. I do not intend to support arbitrary use
cases.
To enable payments, sign up with [Stripe](https://stripe.com/), set the `stripe-secret-key` and `stripe-webhook-key`
config options:
* `stripe-secret-key` is the key used for the Stripe API communication. Setting this values
enables payments in the ntfy web app (e.g. Upgrade dialog). See [API keys](https://dashboard.stripe.com/apikeys).
* `stripe-webhook-key` is the key required to validate the authenticity of incoming webhooks from Stripe.
Webhooks are essential to keep the local database in sync with the payment provider. See [Webhooks](https://dashboard.stripe.com/webhooks).
* `billing-contact` is an email address or website displayed in the "Upgrade tier" dialog to let people reach
out with billing questions. If unset, nothing will be displayed.
In addition to setting these two options, you also need to define a [Stripe webhook](https://dashboard.stripe.com/webhooks)
for the `customer.subscription.updated` and `customer.subscription.deleted` event, which points
to `https://ntfy.example.com/v1/account/billing/webhook`.
Here's an example:
``` yaml
stripe-secret-key: "sk_test_ZmhzZGtmbGhkc2tqZmhzYcO2a2hmbGtnaHNkbGtnaGRsc2hnbG"
stripe-webhook-key: "whsec_ZnNkZnNIRExBSFNES0hBRFNmaHNka2ZsaGR"
billing-contact: "phil@example.com"
```
## Rate limiting
!!! info
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
@@ -888,15 +652,7 @@ request every 5s (defined by `visitor-request-limit-replenish`)
* `visitor-request-limit-replenish` is the rate at which the bucket is refilled (one request per x). Defaults to 5s.
* `visitor-request-limit-exempt-hosts` is a comma-separated list of hostnames and IPs to be exempt from request rate
limiting; hostnames are resolved at the time the server is started. Defaults to an empty list.
### Message limits
By default, the number of messages a visitor can send is governed entirely by the [request limit](#request-limits).
For instance, if the request limit allows for 15,000 requests per day, and all of those requests are POST/PUT requests
to publish messages, then that is the daily message limit.
To limit the number of daily messages per visitor, you can set `visitor-message-daily-limit`. This defines the number
of messages a visitor can send in a day. This counter is reset every day at midnight (UTC).
### Attachment limits
Aside from the global file size and total attachment cache limits (see [above](#attachments)), there are two relevant
per-visitor limits:
@@ -915,42 +671,6 @@ are enabled):
* `visitor-email-limit-burst` is the initial bucket of emails each visitor has. This defaults to 16.
* `visitor-email-limit-replenish` is the rate at which the bucket is refilled (one email per x). Defaults to 1h.
### Firebase limits
If [Firebase is configured](#firebase-fcm), all messages are also published to a Firebase topic (unless `Firebase: no`
is set). Firebase enforces [its own limits](https://firebase.google.com/docs/cloud-messaging/concept-options#topics_throttling)
on how many messages can be published. Unfortunately these limits are a little vague and can change depending on the time
of day. In practice, I have only ever observed `429 Quota exceeded` responses from Firebase if **too many messages are published to
the same topic**.
In ntfy, if Firebase responds with a 429 after publishing to a topic, the visitor (= IP address) who published the message
is **banned from publishing to Firebase for 10 minutes** (not configurable). Because publishing to Firebase happens asynchronously,
there is no indication of the user that this has happened. Non-Firebase subscribers (WebSocket or HTTP stream) are not affected.
After the 10 minutes are up, messages forwarding to Firebase is resumed for this visitor.
If this ever happens, there will be a log message that looks something like this:
```
WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor
```
### Subscriber-based rate limiting
By default, ntfy puts almost all rate limits on the message publisher, e.g. number of messages, requests, and attachment
size are all based on the visitor who publishes a message. **Subscriber-based rate limiting is a way to use the rate limits
of a topic's subscriber, instead of the limits of the publisher.**
If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed
to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume
publishers (e.g. Matrix/Mastodon servers) are allowed to send.
Once enabled, a client may send a `Rate-Topics: <topic1>,<topic2>,...` header when subscribing to topics via
HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits
to use when publishing on this topic. Note that setting the rate visitor requires **read-write permission** on the topic.
UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
response if no "rate visitor" has been previously registered. This is to avoid burning the publisher's
`visitor-message-daily-limit`.
To enable subscriber-based rate limiting, set `visitor-subscriber-rate-limiting: true`.
## Tuning for scale
If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**.
@@ -959,29 +679,6 @@ out [this discussion on Reddit](https://www.reddit.com/r/golang/comments/r9u4ee/
Depending on *how you run it*, here are a few limits that are relevant:
### Message cache
By default, the [message cache](#message-cache) (defined by `cache-file`) uses the SQLite default settings, which means it
syncs to disk on every write. For personal servers, this is perfectly adequate. For larger installations, such as ntfy.sh,
the [write-ahead log (WAL)](https://sqlite.org/wal.html) should be enabled, and the sync mode should be adjusted.
See [this article](https://phiresky.github.io/blog/2020/sqlite-performance-tuning/) for details.
In addition to that, for very high load servers (such as ntfy.sh), it may be beneficial to write messages to the cache
in batches, and asynchronously. This can be enabled with the `cache-batch-size` and `cache-batch-timeout`. If you start
seeing `database locked` messages in the logs, you should probably enable that.
Here's how ntfy.sh has been tuned in the `server.yml` file:
``` yaml
cache-batch-size: 25
cache-batch-timeout: "1s"
cache-startup-queries: |
pragma journal_mode = WAL;
pragma synchronous = normal;
pragma temp_store = memory;
pragma busy_timeout = 15000;
vacuum;
```
### For systemd services
If you're running ntfy in a systemd service (e.g. for .deb/.rpm packages), the main limiting factor is the
`LimitNOFILE` setting in the systemd unit. The default open files limit for `ntfy.service` is 10,000. You can override it
@@ -1039,24 +736,8 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
=== "/etc/nginx/nginx.conf"
```
# Rate limit all IP addresses
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=45r/m;
}
# Alternatively, whitelist certain IP addresses
http {
geo $limited {
default 1;
116.203.112.46/32 0;
132.226.42.65/32 0;
...
}
map $limited $limitkey {
1 $binary_remote_addr;
0 "";
}
limit_req_zone $limitkey zone=one:10m rate=45r/m;
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
}
```
@@ -1085,183 +766,52 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
logpath = /var/log/nginx/error.log
findtime = 600
bantime = 14400
bantime = 7200
maxretry = 10
```
## Health checks
A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below.
If a non-200 HTTP status code is returned or if the returned `health` field is `false` the ntfy service should be considered as unhealthy.
```json
{"health":true}
```
See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment.
## Monitoring
If configured, ntfy can expose a `/metrics` endpoint for [Prometheus](https://prometheus.io/), which can then be used to
create dashboards and alerts (e.g. via [Grafana](https://grafana.com/)).
To configure the metrics endpoint, either set `enable-metrics` and/or set the `listen-metrics-http` option to a dedicated
listen address. Metrics may be considered sensitive information, so before you enable them, be sure you know what you are
doing, and/or secure access to the endpoint in your reverse proxy.
- `enable-metrics` enables the /metrics endpoint for the default ntfy server (i.e. HTTP, HTTPS and/or Unix socket)
- `metrics-listen-http` exposes the metrics endpoint via a dedicated `[IP]:port`. If set, this option implicitly
enables metrics as well, e.g. "10.0.1.1:9090" or ":9090"
=== "server.yml (Using default port)"
```yaml
enable-metrics: true
```
=== "server.yml (Using dedicated IP/port)"
```yaml
metrics-listen-http: "10.0.1.1:9090"
```
In Prometheus, an example scrape config would look like this:
=== "prometheus.yml"
```yaml
scrape_configs:
- job_name: "ntfy"
static_configs:
- targets: ["10.0.1.1:9090"]
```
Here's an example Grafana dashboard built from the metrics (see [Grafana JSON on GitHub](https://raw.githubusercontent.com/binwiederhier/ntfy/main/examples/grafana-dashboard/ntfy-grafana.json)):
<figure markdown style="padding-left: 50px; padding-right: 50px">
<a href="../../static/img/grafana-dashboard.png" target="_blank"><img src="../../static/img/grafana-dashboard.png"/></a>
<figcaption>ntfy Grafana dashboard</figcaption>
</figure>
## Profiling
ntfy can expose Go's [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoints to support profiling of the ntfy server.
If enabled, ntfy will listen on a dedicated listen IP/port, which can be accessed via the web browser on `http://<ip>:<port>/debug/pprof/`.
This can be helpful to expose bottlenecks, and visualize call flows. To enable, simply set the `profile-listen-http` config option.
## Logging & debugging
By default, ntfy logs to the console (stderr), with an `info` log level, and in a human-readable text format.
ntfy supports five different log levels, can also write to a file, log as JSON, and even supports granular
log level overrides for easier debugging. Some options (`log-level` and `log-level-overrides`) can be hot reloaded
by calling `kill -HUP $pid` or `systemctl reload ntfy`.
The following config options define the logging behavior:
* `log-format` defines the output format, can be `text` (default) or `json`
* `log-file` is a filename to write logs to. If this is not set, ntfy logs to stderr.
* `log-level` defines the default log level, can be one of `trace`, `debug`, `info` (default), `warn` or `error`.
Be aware that `debug` (and particularly `trace`) can be **very verbose**. Only turn them on briefly for debugging purposes.
* `log-level-overrides` lets you override the log level if certain fields match. This is incredibly powerful
for debugging certain parts of the system (e.g. only the account management, or only a certain visitor).
This is an array of strings in the format:
- `field=value -> level` to match a value exactly, e.g. `tag=manager -> trace`
- `field -> level` to match any value, e.g. `time_taken_ms -> debug`
**Logging config (good for production use):**
``` yaml
log-level: info
log-format: json
log-file: /var/log/ntfy.log
```
**Temporary debugging:**
If something's not working right, you can debug/trace through what the ntfy server is doing by setting the `log-level`
to `debug` or `trace`. The `debug` setting will output information about each published message, but not the message
contents. The `trace` setting will also print the message contents.
Alternatively, you can set `log-level-overrides` for only certain fields, such as a visitor's IP address (`visitor_ip`),
a username (`user_name`), or a tag (`tag`). There are dozens of fields you can use to override log levels. To learn what
they are, either turn the log-level to `trace` and observe, or reference the [source code](https://github.com/binwiederhier/ntfy).
Here's an example that will output only `info` log events, except when they match either of the defined overrides:
``` yaml
log-level: info
log-level-overrides:
- "tag=manager -> trace"
- "visitor_ip=1.2.3.4 -> debug"
- "time_taken_ms -> debug"
```
!!! warning
The `debug` and `trace` log levels are very verbose, and using `log-level-overrides` has a
performance penalty. Only use it for temporary debugging.
You can also hot-reload the `log-level` and `log-level-overrides` by sending the `SIGHUP` signal to the process after
editing the `server.yml` file. You can do so by calling `systemctl reload ntfy` (if ntfy is running inside systemd),
or by calling `kill -HUP $(pidof ntfy)`. If successful, you'll see something like this:
```
$ ntfy serve
2022/06/02 10:29:28 INFO Listening on :2586[http] :1025[smtp], log level is INFO
2022/06/02 10:29:34 INFO Partially hot reloading configuration ...
2022/06/02 10:29:34 INFO Log level is TRACE
```
## Config options
Each config option can be set in the config file `/etc/ntfy/server.yml` (e.g. `listen-http: :80`) or as a
CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment
variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
!!! info
All config options can also be defined in the `server.yml` file using underscores instead of dashes, e.g.
`cache_duration` and `cache-duration` are both supported. This is to support stricter YAML parsers that do
not support dashes.
| Config option | Env variable | Format | Default | Description |
|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) |
| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on |
| `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 |
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). |
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) |
| `cache-batch-size` | `NTFY_CACHE_BATCH_SIZE` | *int* | 0 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous) |
| `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) |
| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). |
| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. |
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. |
| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. |
| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. |
| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. |
| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending |
| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled |
| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled |
| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled |
| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` |
| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` |
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | *string* | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
| `manager-interval` | `NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
| `upstream-base-url` | `NTFY_UPSTREAM_BASE_URL` | *URL* | `https://ntfy.sh` | Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers |
| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. |
| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. |
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor |
| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled |
| `visitor-message-daily-limit` | `NTFY_VISITOR_MESSAGE_DAILY_LIMIT` | *number* | - | Rate limiting: Allowed number of messages per day per visitor, reset every day at midnight (UTC). By default, this value is unset. |
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
| `visitor-subscriber-rate-limiting` | `NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING` | *bool* | `false` | Rate limiting: Enables subscriber-based rate limiting |
| `web-root` | `NTFY_WEB_ROOT` | `app`, `home` or `disable` | `app` | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable) |
| `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
| `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
| `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |
| Config option | Env variable | Format | Default | Description |
|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) |
| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on |
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). |
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). |
| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. |
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. |
| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. |
| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. |
| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. |
| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending |
| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled |
| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled |
| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled |
| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` |
| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` |
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
| `web-root` | `NTFY_WEB_ROOT` | `app` or `home` | `app` | Sets web root to landing page (home) or web app (app) |
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. |
| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. |
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor |
| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled |
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
@@ -1280,72 +830,51 @@ CATEGORY:
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
OPTIONS:
--debug, -d enable debug logging (default: false) [$NTFY_DEBUG]
--trace enable tracing (very verbose, be careful) (default: false) [$NTFY_TRACE]
--no-log-dates, --no_log_dates disable the date/time prefix (default: false) [$NTFY_NO_LOG_DATES]
--log-level value, --log_level value set log level (default: "INFO") [$NTFY_LOG_LEVEL]
--log-level-overrides value, --log_level_overrides value [ --log-level-overrides value, --log_level_overrides value ] set log level overrides [$NTFY_LOG_LEVEL_OVERRIDES]
--log-format value, --log_format value set log format (default: "text") [$NTFY_LOG_FORMAT]
--log-file value, --log_file value set log file, default is STDOUT [$NTFY_LOG_FILE]
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
--base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
--listen-http value, --listen_http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
--listen-https value, --listen_https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
--listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
--listen-unix-mode value, --listen_unix_mode value file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE]
--key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
--cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
--firebase-key-file value, --firebase_key_file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
--cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
--cache-batch-size value, --cache_batch_size value max size of messages to batch together when writing to message cache (if zero, writes are synchronous) (default: 0) [$NTFY_BATCH_SIZE]
--cache-batch-timeout value, --cache_batch_timeout value timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: 0s) [$NTFY_CACHE_BATCH_TIMEOUT]
--cache-startup-queries value, --cache_startup_queries value queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES]
--auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE]
--auth-startup-queries value, --auth_startup_queries value queries run when the auth database is initialized [$NTFY_AUTH_STARTUP_QUERIES]
--auth-default-access value, --auth_default_access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
--attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR]
--attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
--attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
--attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION]
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
--disallowed-topics value, --disallowed_topics value [ --disallowed-topics value, --disallowed_topics value ] topics that are not allowed to be used [$NTFY_DISALLOWED_TOPICS]
--web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT]
--enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
--enable-login, --enable_login allows users to log in via the web app, or API (default: false) [$NTFY_ENABLE_LOGIN]
--enable-reservations, --enable_reservations allows users to reserve topics (if their tier allows it) (default: false) [$NTFY_ENABLE_RESERVATIONS]
--upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL]
--smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
--smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
--smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
--smtp-sender-from value, --smtp_sender_from value SMTP sender address (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_FROM]
--smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
--smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
--smtp-server-addr-prefix value, --smtp_server_addr_prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
--global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
--visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
--visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
--visitor-attachment-daily-bandwidth-limit value, --visitor_attachment_daily_bandwidth_limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT]
--visitor-request-limit-burst value, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
--visitor-request-limit-replenish value, --visitor_request_limit_replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
--visitor-request-limit-exempt-hosts value, --visitor_request_limit_exempt_hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS]
--visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
--visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
--stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
--stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY]
--billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT]
--help, -h show help (default: false)
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
--base-url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
--listen-http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
--listen-https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
--listen-unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
--key-file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
--cert-file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
--firebase-key-file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
--cache-file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
--cache-duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
--auth-file value, -H value auth database file used for access control [$NTFY_AUTH_FILE]
--auth-default-access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
--attachment-cache-dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR]
--attachment-total-size-limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
--attachment-file-size-limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
--attachment-expiry-duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION]
--keepalive-interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
--manager-interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
--web-root value sets web root to landing page (home) or web app (app) (default: "app") [$NTFY_WEB_ROOT]
--smtp-sender-addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
--smtp-sender-user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
--smtp-sender-pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
--smtp-sender-from value SMTP sender address (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_FROM]
--smtp-server-listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
--smtp-server-domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
--smtp-server-addr-prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
--global-topic-limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
--visitor-subscription-limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
--visitor-attachment-total-size-limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
--visitor-attachment-daily-bandwidth-limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT]
--visitor-request-limit-burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
--visitor-request-limit-replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
--visitor-request-limit-exempt-hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS]
--visitor-email-limit-burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
--visitor-email-limit-replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
--behind-proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
--help, -h show help (default: false)
```

View File

@@ -1,44 +1,29 @@
# Deprecation notices
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
**removed after 1-3 months** from the time they were deprecated. How long the feature is deprecated
before the behavior is changed depends on the severity of the change, and how prominent the feature is.
**removed after ~3 months** from the time they were deprecated.
## Active deprecations
_No active deprecations_
## Previous deprecations
### Android app: WebSockets will become the default connection protocol
> Active since 2022-03-13, behavior will change in **June 2022**
### ntfy CLI: `ntfy publish --env-topic` will be removed
> Active since 2022-06-20, behavior changed with v1.30.1
In future versions of the Android app, instant delivery connections and connections to self-hosted servers will
be using the WebSockets protocol. This potentially requires [configuration changes in your proxy](https://ntfy.sh/docs/config/#nginxapache2caddy).
The `ntfy publish --env-topic` option will be removed. It'll still be possible to specify a topic via the
`NTFY_TOPIC` environment variable, but it won't be necessary anymore to specify the `--env-topic` flag.
=== "Before"
```
$ NTFY_TOPIC=mytopic ntfy publish --env-topic "this is the message"
```
=== "After"
```
$ NTFY_TOPIC=mytopic ntfy publish "this is the message"
```
### <del>Android app: WebSockets will become the default connection protocol</del>
> Active since 2022-03-13, behavior will not change (deprecation removed 2022-06-20)
Instant delivery connections and connections to self-hosted servers in the Android app were going to switch
to use the WebSockets protocol by default. It was decided to keep JSON stream as the most compatible default
and add a notice banner in the Android app instead.
Due to [reports of varying battery consumption](https://github.com/binwiederhier/ntfy/issues/190) (which entirely
seems to depend on the phone), JSON HTTP stream support will not be removed. Instead, I'll just flip the default to
WebSocket in June.
### Android app: Using `since=<timestamp>` instead of `since=<id>`
> Active since 2022-02-27, behavior changed with v1.14.0
> Active since 2022-02-27, behavior will change in **May 2022**
The Android app started using `since=<id>` instead of `since=<timestamp>`, which means as of Android app v1.14.0,
it will not work with servers older than v1.16.0 anymore. This is to simplify handling of deduplication in the Android app.
In about 3 months, the Android app will start using `since=<id>` instead of `since=<timestamp>`, which means that it will
not work with servers older than v1.16.0 anymore. This is to simplify handling of deduplication in the Android app.
The `since=<timestamp>` endpoint will continue to work. This is merely a notice that the Android app behavior will change.
## Previous deprecations
### Running server via `ntfy` (instead of `ntfy serve`)
> Deprecated 2021-12-17, behavior changed with v1.10.0

View File

@@ -43,13 +43,6 @@ Build related:
The `web/` and `docs/` folder are the sources for web app and documentation. During the build process,
the generated output is copied to `server/site` (web app and landing page) and `server/docs` (documentation).
### Build/test on Gitpod
To get a quick working development environment you can use [Gitpod](https://gitpod.io), an in-browser IDE
that makes it easy to develop ntfy without having to set up a desktop IDE. For any real development,
I do suggest a proper IDE like [IntelliJ IDEA](https://www.jetbrains.com/idea/).
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
### Build requirements
* [Go](https://go.dev/) (required for main server)
@@ -65,8 +58,8 @@ These steps **assume Ubuntu**. Steps may vary on different Linux distributions.
First, install [Go](https://go.dev/) (see [official instructions](https://go.dev/doc/install)):
``` shell
wget https://go.dev/dl/go1.19.1.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.19.1.linux-amd64.tar.gz
wget https://go.dev/dl/go1.18.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
go version # verifies that it worked
```
@@ -79,7 +72,7 @@ goreleaser -v # verifies that it worked
Install [nodejs](https://nodejs.org/en/) (see [official instructions](https://nodejs.org/en/download/package-manager/)):
``` shell
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
curl -fsSL https://deb.nodesource.com/setup_17.x | sudo -E bash -
sudo apt-get install -y nodejs
npm -v # verifies that it worked
```
@@ -92,6 +85,7 @@ sudo apt install \
gcc-arm-linux-gnueabi \
gcc-aarch64-linux-gnu \
python3-pip \
upx \
git
```
@@ -118,15 +112,15 @@ by typing `make`:
$ make
Typical commands (more see below):
make build - Build web app, documentation and server/client (sloowwww)
make cli-linux-amd64 - Build server/client binary (amd64, no web app or docs)
make install-linux-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)
make server-amd64 - Build server/client binary (amd64, no web app or docs)
make install-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)
make web - Build the web app
make docs - Build the documentation
make check - Run all tests, vetting/formatting checks and linters
...
```
If you want to build the **ntfy binary including web app and docs for all supported architectures** (amd64, armv7, and arm64),
If you want to build the **ntfy binary including web app and docs for all supported architectures** (amd64, armv7, and amd64),
you can simply run `make build`:
``` shell
@@ -164,52 +158,47 @@ $ make release-snapshot
During development, you may want to be more picky and build only certain things. Here are a few examples.
### Build the ntfy binary
To build only the `ntfy` binary **without the web app or documentation**, use the `make cli-...` targets:
To build only the `ntfy` binary **without the web app or documentation**, use the `make server-...` targets:
``` shell
$ make
Build server & client (using GoReleaser, not release version):
make cli - Build server & client (all architectures)
make cli-linux-amd64 - Build server & client (Linux, amd64 only)
make cli-linux-armv6 - Build server & client (Linux, armv6 only)
make cli-linux-armv7 - Build server & client (Linux, armv7 only)
make cli-linux-arm64 - Build server & client (Linux, arm64 only)
make cli-windows-amd64 - Build client (Windows, amd64 only)
make cli-darwin-all - Build client (macOS, arm64+amd64 universal binary)
Build server & client (not release version):
make server - Build server & client (all architectures)
make server-amd64 - Build server & client (amd64 only)
make server-armv7 - Build server & client (armv7 only)
make server-arm64 - Build server & client (arm64 only)
```
So if you're on an amd64/x86_64-based machine, you may just want to run `make cli-linux-amd64` during testing. On a modern
system, this shouldn't take longer than 5-10 seconds. I often combine it with `install-linux-amd64` so I can run the binary
So if you're on an amd64/x86_64-based machine, you may just want to run `make server-amd64` during testing. On a modern
system, this shouldn't take longer than 5-10 seconds. I often combine it with `install-amd64` so I can run the binary
right away:
``` shell
$ make cli-linux-amd64 install-linux-amd64
$ make server-amd64 install-amd64
$ ntfy serve
```
**During development of the main app, you can also just use `go run main.go`**, as long as you run
`make cli-deps-static-sites`at least once and `CGO_ENABLED=1`:
`make server-deps-static-sites`at least once and `CGO_ENABLED=1`:
``` shell
$ export CGO_ENABLED=1
$ make cli-deps-static-sites
$ make server-deps-static-sites
$ go run main.go serve
2022/03/18 08:43:55 Listening on :2586[http]
...
```
If you don't run `cli-deps-static-sites`, you may see an error *`pattern ...: no matching files found`*:
If you don't run `server-deps-static-sites`, you may see an error *`pattern ...: no matching files found`*:
```
$ go run main.go serve
server/server.go:85:13: pattern docs: no matching files found
```
This is because we use `go:embed` to embed the documentation and web app, so the Go code expects files to be
present at `server/docs` and `server/site`. If they are not, you'll see the above error. The `cli-deps-static-sites`
target creates dummy files that ensure that you'll be able to build.
present at `server/docs` and `server/site`. If they are not, you'll see the above error. The `server-deps-static-sites`
target creates dummy files that ensures that you'll be able to build.
While not officially supported (or released), you can build and run the server **on macOS** as well. Simply run
`make cli-darwin-server` to build a binary, or `go run main.go serve` (see above) to run it.
### Build the web app
The sources for the web app live in `web/`. As long as you have `npm` installed (see above), building the web app
@@ -221,7 +210,7 @@ $ make web
```
This will build the web app using Create React App and then **copy the production build to the `server/site` folder**, so
that when you `make cli` (or `make cli-linux-amd64`, ...), you will have the web app included in the `ntfy` binary.
that when you `make server` (or `make server-amd64`, ...), you will have the web app included in the `ntfy` binary.
If you're developing on the web app, it's best to just `cd web` and run `npm start` manually. This will open your browser
at `http://127.0.0.1:3000` with the web app, and as you edit the source files, they will be recompiled and the browser
@@ -293,13 +282,9 @@ Then either follow the steps for building with or without Firebase.
I do build the ntfy Android app using IntelliJ IDEA (Android Studio), so I don't know if these Gradle commands will
work without issues. Please give me feedback if it does/doesn't work for you.
Without Firebase, you may want to still change the default `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
Without Firebase, you may want to still change the default `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
if you're self-hosting the server. Then run:
```
# Remove Google dependencies (FCM)
sed -i -e '/google-services/d' build.gradle
sed -i -e '/google-services/d' app/build.gradle
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
./gradlew assembleFdroidRelease
@@ -316,7 +301,7 @@ To build your own version with Firebase, you must:
* Create a Firebase/FCM account
* Place your account file at `app/google-services.json`
* And change `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
* And change `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
* Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
@@ -325,78 +310,3 @@ To build your own version with Firebase, you must:
# To build a bundle .aab (app/play/release/*.aab)
./gradlew bundlePlayRelease
```
## iOS app
Building the iOS app is very involved. Please report any inconsistencies or issues with it. The requirements are
strictly based off of my development on this app. There may be other versions of macOS / XCode that work.
### Requirements
1. macOS Monterey or later
1. XCode 13.2+
1. A physical iOS device (for push notifications, Firebase does not work in the XCode simulator)
1. Firebase account
1. Apple Developer license? (I forget if it's possible to do testing without purchasing the license)
### Apple setup
!!! info
Along with this step, the [PLIST Deployment](#plist-deployment-and-configuration) step is also required
for these changes to take effect in the iOS app.
1. [Create a new key in Apple Developer Member Center](https://developer.apple.com/account/resources/authkeys/add)
1. Select "Apple Push Notifications service (APNs)"
1. Download the newly created key (should have a file name similar to `AuthKey_ZZZZZZ.p8`, where `ZZZZZZ` is the **Key ID**)
1. Record your **Team ID** - it can be seen in the top-right corner of the page, or on your Account > Membership page
1. Next, navigate to "Project Settings" in the firebase console for your project, and select the iOS app you created. Then, click "Cloud Messaging" in the left sidebar, and scroll down to the "APNs Authentication Key" section. Click "Upload Key", and upload the key you downloaded from Apple Developer.
!!! warning
If you don't do the above setups for APNS, **notifications will not post instantly or sometimes at all**. This is because of the missing APNS key, which is required for firebase to send notifications to the iOS app. See below for a snip from the firebase docs.
If you don't have an APNs authentication key, you can still send notifications to iOS devices, but they won't be delivered
instantly. Instead, they'll be delivered when the device wakes up to check for new notifications or when your application
sends a firebase request to check for them. The time to check for new notifications can vary from a few seconds to hours,
days or even weeks. Enabling APNs authentication keys ensures that notifications are delivered instantly and is strongly
recommended.
### Firebase setup
1. If you haven't already, create a Google / Firebase account
1. Visit the [Firebase console](https://console.firebase.google.com)
1. Create a new Firebase project:
1. Enter a project name
1. Disable Google Analytics (currently iOS app does not support analytics)
1. On the "Project settings" page, add an iOS app
1. Apple bundle ID - "com.copephobia.ntfy-ios" (this can be changed to match XCode's ntfy.sh target > "Bundle Identifier" value)
1. Register the app
1. Download the config file - GoogleInfo.plist (this will need to be included in the ntfy-ios repository / XCode)
1. Generate a new service account private key for the ntfy server
1. Go to "Project settings" > "Service accounts"
1. Click "Generate new private key" to generate and download a private key to use for sending messages via the ntfy server
### ntfy server
Note that the ntfy server is not officially supported on macOS. It should, however, be able to run on macOS using these
steps:
1. If not already made, make the `/etc/ntfy/` directory and move the service account private key to that folder
1. Copy the `server/server.yml` file from the ntfy repository to `/etc/ntfy/`
1. Modify the `/etc/ntfy/server.yml` file `firebase-key-file` value to the path of the private key
1. Install go: `brew install go`
1. In the ntfy repository, run `make cli-darwin-server`.
### XCode setup
1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) to install the
`firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging
1. Similarly, install the SQLite.swift package dependency in XCode
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators
### PLIST config
To have instant notifications/better notification delivery when using firebase, you will need to add the
`GoogleService-Info.plist` file to your project. Here's how to do that:
1. In XCode, find the NTFY app target. **Not** the NSE app target.
1. Find the Asset/ folder in the project navigator
1. Drag the `GoogleService-Info.plist` file into the Asset/ folder that you get from the firebase console. It can be
found in the "Project settings" > "General" > "Your apps" with a button labled "GoogleService-Info.plist"
After that, you should be all set!

View File

@@ -4,14 +4,7 @@ There are a million ways to use ntfy, but here are some inspirations. I try to c
<a href="https://github.com/binwiederhier/ntfy/tree/main/examples">examples on GitHub</a>, so be sure to check
those out, too.
!!! info
Many of these examples were contributed by ntfy users. If you have other examples of how you use ntfy, please
[create a pull request](https://github.com/binwiederhier/ntfy/pulls), and I'll happily include it. Also note, that
I cannot guarantee that all of these examples are functional. Many of them I have not tried myself.
## Cronjobs
ntfy is perfect for any kind of cronjobs or just when long processes are done (backups, pipelines, rsync copy commands, ...).
## A long process is done: backups, copying data, pipelines, ...
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
@@ -23,15 +16,6 @@ rsync -a root@laptop /backups/laptop \
|| curl -H tags:warning -H prio:high -d "Laptop backup failed" ntfy.sh/backups
```
Here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
``` cron
# Check github/ntfy user
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
```
## Low disk space alerts
Here's a simple cronjob that I use to alert me when the disk space on the root disk is running low. It's simple, but
effective.
@@ -53,7 +37,11 @@ if [ -n "$avail" ]; then
fi
```
## SSH login alerts
## Server-sent messages in your web app
Just as you can [subscribe to topics in the Web UI](subscribe/web.md), you can use ntfy in your own
web application. Check out the <a href="/example.html">live example</a> or just look the source of this page.
## Notify on SSH login
Years ago my home server was broken into. That shook me hard, so every time someone logs into any machine that I
own, I now message myself. Here's an example of how to use <a href="https://en.wikipedia.org/wiki/Linux_PAM">PAM</a>
to notify yourself on SSH login.
@@ -101,7 +89,7 @@ It looked something like this:
You can easily integrate ntfy into Ansible, Salt, or Puppet to notify you when runs are done or are highstated.
One of my co-workers uses the following Ansible task to let him know when things are done:
``` yaml
```yml
- name: Send ntfy.sh update
uri:
url: "https://ntfy.sh/{{ ntfy_channel }}"
@@ -109,38 +97,11 @@ One of my co-workers uses the following Ansible task to let him know when things
body: "{{ inventory_hostname }} reseeding complete"
```
There's also a dedicated Ansible action plugin (one which runs on the Ansible controller) called
[ansible-ntfy](https://github.com/jpmens/ansible-ntfy). The following task posts a message
to ntfy at its default URL (`attrs` and other attributes are optional):
``` yaml
- name: "Notify ntfy that we're done"
ntfy:
msg: "deployment on {{ inventory_hostname }} is complete. 🐄"
attrs:
tags: [ heavy_check_mark ]
priority: 1
```
## GitHub Actions
You can send a message during a workflow run with curl. Here is an example sending info about the repo, commit and job status.
``` yaml
- name: Actions Ntfy
run: |
curl \
-u ${{ secrets.NTFY_CRED }} \
-H "Title: Title here" \
-H "Content-Type: text/plain" \
-d $'Repo: ${{ github.repository }}\nCommit: ${{ github.sha }}\nRef: ${{ github.ref }}\nStatus: ${{ job.status}}' \
${{ secrets.NTFY_URL }}
```
## Watchtower (shoutrrr)
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
## Watchtower notifications (shoutrrr)
You can use `shoutrrr` generic webhook support to send watchtower notifications to your ntfy topic.
Example docker-compose.yml:
``` yaml
```yml
services:
watchtower:
image: containrrr/watchtower
@@ -154,17 +115,28 @@ Or, if you only want to send notifications using shoutrrr:
shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
```
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
## Random cronjobs
Alright, here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
``` cron
# Check github/ntfy user
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
~
```
## Download notifications (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd)
It's possible to use custom scripts for all the *arr services, plus SABnzbd. Notifications for downloads, warnings, grabs etc.
Some simple bash scripts to achieve this are kindly provided in [nickexyz's repository](https://github.com/nickexyz/ntfy-shellscripts).
## Node-RED
You can use the HTTP request node to send messages with [Node-RED](https://nodered.org), some examples:
<details>
<summary>Example: Send a message (click to expand)</summary>
``` json
```
[
{
"id": "c956e688cc74ad8e",
@@ -253,7 +225,7 @@ You can use the HTTP request node to send messages with [Node-RED](https://noder
<details>
<summary>Example: Send a picture (click to expand)</summary>
``` json
```
[
{
"id": "d135a13eadeb9d6d",
@@ -367,23 +339,10 @@ You can use the HTTP request node to send messages with [Node-RED](https://noder
![Node red picture flow](static/img/nodered-picture.png)
## Gatus
To use ntfy with [Gatus](https://github.com/TwiN/gatus), you can use the `ntfy` alerting provider like so:
## Gatus service health check
```yaml
alerting:
ntfy:
url: "https://ntfy.sh"
topic: "YOUR_NTFY_TOPIC"
priority: 3
An example for a custom alert with <a href="https://github.com/TwiN/gatus">Gatus</a>
```
For more information on using ntfy with Gatus, refer to [Configuring ntfy alerts](https://github.com/TwiN/gatus#configuring-ntfy-alerts).
<details>
<summary>Alternative: Using the custom alerting provider</summary>
```yaml
alerting:
custom:
url: "https://ntfy.sh"
@@ -407,192 +366,3 @@ alerting:
TRIGGERED: "warning"
RESOLVED: "white_check_mark"
```
</details>
## Jellyseerr/Overseerr webhook
Here is an example for [jellyseerr](https://github.com/Fallenbagel/jellyseerr)/[overseerr](https://overseerr.dev/) webhook
JSON payload. Remember to change the `https://request.example.com` to your URL as the value of the JSON key click.
And if you're not using the request `topic`, make sure to change it in the JSON payload to your topic.
``` json
{
"topic": "requests",
"title": "{{event}}",
"message": "{{subject}}\n{{message}}\n\nRequested by: {{requestedBy_username}}\n\nStatus: {{media_status}}\nRequest Id: {{request_id}}",
"priority": 4,
"attach": "{{image}}",
"click": "https://requests.example.com/{{media_type}}/{{media_tmdbid}}"
}
```
## Home Assistant
Here is an example for the configuration.yml file to setup a REST notify component.
Since Home Assistant is going to POST JSON, you need to specify the root of your ntfy resource.
```yaml
notify:
- name: ntfy
platform: rest
method: POST_JSON
data:
topic: YOUR_NTFY_TOPIC
title_param_name: title
message_param_name: message
resource: https://ntfy.sh
```
If you need to authenticate to your ntfy resource, define the authentication, username and password as below:
```yaml
notify:
- name: ntfy
platform: rest
method: POST_JSON
authentication: basic
username: YOUR_USERNAME
password: YOUR_PASSWORD
data:
topic: YOUR_NTFY_TOPIC
title_param_name: title
message_param_name: message
resource: https://ntfy.sh
```
If you need to add any other [ntfy specific parameters](https://ntfy.sh/docs/publish/#publish-as-json) such as priority, tags, etc., add them to the `data` array in the example yml. For example:
```yaml
notify:
- name: ntfy
platform: rest
method: POST_JSON
data:
topic: YOUR_NTFY_TOPIC
priority: 4
title_param_name: title
message_param_name: message
resource: https://ntfy.sh
```
## Uptime Kuma
Go to your [Uptime Kuma](https://github.com/louislam/uptime-kuma) Settings > Notifications, click on **Setup Notification**.
Then set your desired **title** (e.g. "Uptime Kuma"), **ntfy topic**, **Server URL** and **priority (1-5)**:
<div id="uptimekuma-screenshots" class="screenshots">
<a href="../static/img/uptimekuma-settings.png"><img src="../static/img/uptimekuma-settings.png"/></a>
<a href="../static/img/uptimekuma-setup.png"><img src="../static/img/uptimekuma-setup.png"/></a>
</div>
You can now test the notifications and apply them to monitors:
<div id="uptimekuma-monitor-screenshots" class="screenshots">
<a href="../static/img/uptimekuma-ios-test.jpg"><img src="../static/img/uptimekuma-ios-test.jpg"/></a>
<a href="../static/img/uptimekuma-ios-down.jpg"><img src="../static/img/uptimekuma-ios-down.jpg"/></a>
<a href="../static/img/uptimekuma-ios-up.jpg"><img src="../static/img/uptimekuma-ios-up.jpg"/></a>
</div>
## UptimeRobot
Go to your [UptimeRobot](https://github.com/uptimerobot) My Settings > Alert Contacts > Add Alert Contact
Select **Alert Contact Type** = Webhook. Then set your desired **Friendly Name** (e.g. "ntfy-sh-UP"), **URL to Notify**, **POST value** and select checkbox **Send as JSON (application/json)**. Make sure to send the JSON POST request to ntfy.domain.com without the topic name in the url and include the "topic" name in the JSON body.
<div id="uptimerobot-monitor-setup" class="screenshots">
<a href="../static/img/uptimerobot-setup.jpg"><img src="../static/img/uptimerobot-setup.jpg"/></a>
</div>
``` json
{
"topic":"myTopic",
"title": "*monitorFriendlyName* *alertTypeFriendlyName*",
"message": "*alertDetails*",
"tags": ["green_circle"],
"priority": 3,
"click": https://uptimerobot.com/dashboard#*monitorID*
}
```
You can create two Alert Contacts each with a different icon and priority, for example:
``` json
{
"topic":"myTopic",
"title": "*monitorFriendlyName* *alertTypeFriendlyName*",
"message": "*alertDetails*",
"tags": ["red_circle"],
"priority": 3,
"click": https://uptimerobot.com/dashboard#*monitorID*
}
```
You can now add the created Alerts Contact(s) to the monitor(s) and test the notifications:
<div id="uptimerobot-monitor-screenshots" class="screenshots">
<a href="../static/img/uptimerobot-test.jpg"><img src="../static/img/uptimerobot-test.jpg"/></a>
</div>
## Apprise
ntfy is integrated natively into [Apprise](https://github.com/caronc/apprise) (also check out the
[Apprise/ntfy wiki page](https://github.com/caronc/apprise/wiki/Notify_ntfy)).
You can use it like this:
```
apprise -vv -t "Test Message Title" -b "Test Message Body" \
ntfy://mytopic
```
Or with your own server like this:
```
apprise -vv -t "Test Message Title" -b "Test Message Body" \
ntfy://ntfy.example.com/mytopic
```
## Rundeck
Rundeck by default sends only HTML email which is not processed by ntfy SMTP server. Append following configurations to
[rundeck-config.properties](https://docs.rundeck.com/docs/administration/configuration/config-file-reference.html) :
```
# Template
rundeck.mail.template.file=/path/to/template.html
rundeck.mail.template.log.formatted=false
```
Example `template.html`:
```html
<div>Execution ${execution.id} was <b>${execution.status}</b></div>
<ul>
<li><a href="${execution.href}">Execution result</a></li>
<li><a href="${job.href}">Job</a></li>
<li><a href="${execution.projectHref}">Project: ${execution.project}</a></li>
<li><a href="${rundeck.href}">Rundeck</a></li>
</ul>
```
Add notification on Rundeck (attachment type must be: `Attached as file to email`):
![Rundeck](static/img/rundeck.png)
## Traccar
This will only work on selfhosted [traccar](https://www.traccar.org/) ([Github](https://github.com/traccar/traccar)) instances, as you need to be able to set `sms.http.*` keys, which is not possible through the UI attributes
The easiest way to integrate traccar with ntfy, is to configure ntfy as the SMS provider for your instance. You then can set your ntfy topic as your account's phone number in traccar. Sending the email notifications to ntfy will not work, as ntfy does not support HTML emails.
**Caution:** JSON publishing is only possible, when POST-ing to the root URL of the ntfy instance. (see [documentation](publish.md#publish-as-json))
```xml
<entry key='sms.http.url'>https://ntfy.sh</entry>
<entry key='sms.http.template'>
{
"topic": "{phone}",
"message": "{message}"
}
</entry>
```
If [access control](config.md#access-control) is enabled, and the target topic does not support anonymous writes, you'll also have to provide an authorization header, for example in form of a privileged token
```xml
<entry key='sms.http.authorization'>Bearer tk_JhbsnoMrgy2FcfHeofv97Pi5uXaZZ</entry>
```
or by simply providing traccar with a valid username/password combination.
```xml
<entry key='sms.http.user'>phil</entry>
<entry key='sms.http.password'>mypass</entry>
```

View File

@@ -4,35 +4,27 @@
Who knows. I didn't do a lot of research before making this. It was fun making it.
## Can I use this in my app? Will it stay free?
Yes. As long as you don't abuse it, it'll be available and free of charge. While I will always allow usage of the ntfy.sh
server without signup and free of charge, I may also offer paid plans in the future.
Yes. As long as you don't abuse it, it'll be available and free of charge. I do not plan on monetizing
the service.
## What are the uptime guarantees?
Best effort.
ntfy currently runs on a single DigitalOcean droplet, without any scale out strategy or redundancies. When the time comes,
I'll add scale out features, but for now it is what it is.
In the first year of its life, and to this day (Dec'22), ntfy had **no outages** that I can remember. Other than short
blips and some HTTP 500 spikes, it has been rock solid.
There is a [status page](https://ntfy.statuspage.io/) which is updated based on some automated checks via the amazingly
awesome [healthchecks.io](https://healthchecks.io/) (_no affiliation, just a fan_).
Best effort.
## What happens if there are multiple subscribers to the same topic?
As per usual with pub-sub, all subscribers receive notifications if they are subscribed to a topic.
As per usual with pub-sub, all subscribers receive notifications if they are
subscribed to a topic.
## Will you know what topics exist, can you spy on me?
If you don't trust me or your messages are sensitive, run your own server. It's open source.
That said, the logs do contain topic names and IP addresses, but I don't use them for anything other than
troubleshooting and rate limiting. Messages are cached for the duration configured in `server.yml` (12h by default)
to facilitate service restarts, message polling and to overcome client network disruptions.
If you don't trust me or your messages are sensitive, run your own server. It's <a href="https://github.com/binwiederhier/ntfy">open source</a>.
That said, the logs do not contain any topic names or other details about you.
Messages are cached for the duration configured in `server.yml` (12h by default) to facilitate service restarts, message polling and to overcome
client network disruptions.
## Can I self-host it?
Yes. The server (including this Web UI) can be self-hosted, and the Android/iOS app supports adding topics from
Yes. The server (including this Web UI) can be self-hosted, and the Android app supports adding topics from
your own server as well. Check out the [install instructions](install.md).
## Is Firebase used?
## Why is Firebase used?
In addition to caching messages locally and delivering them to long-polling subscribers, all messages are also
published to Firebase Cloud Messaging (FCM) (if `FirebaseKeyFile` is set, which it is on ntfy.sh). This
is to facilitate notifications on Android.
@@ -42,41 +34,16 @@ of the app and [self-host your own ntfy server](install.md).
## How much battery does the Android app use?
If you use the ntfy.sh server, and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature,
the Android/iOS app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
or you use *instant delivery* (Android only), or install from F-droid ([which does not support FCM](https://f-droid.org/docs/Inclusion_Policy/)),
the app has to maintain a constant connection to the server, which consumes about 0-1% of battery in 17h of use (on my phone).
There has been a ton of testing and improvement around this. I think it's pretty decent now.
## Paid plans? I thought it was open source?
All of ntfy will remain open source, with a free software license (Apache 2.0 and GPLv2). If you'd like to self-host, you
can (and should do that). The paid plans I am offering are for people that do not want to self-host, and/or need higher
limits.
the Android app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
or you use *instant delivery*, the app has to maintain a constant connection to the server, which consumes about 0-1% of
battery in 17h of use (on my phone). There has been a ton of testing and improvement around this. I think it's pretty
decent now.
## What is instant delivery?
[Instant delivery](subscribe/phone.md#instant-delivery) is a feature in the Android app. If turned on, the app maintains a constant connection to the
server and listens for incoming notifications. This consumes additional battery (see above),
server and listens for incoming notifications. This consumes <a href="#battery-usage">additional battery</a>,
but delivers notifications instantly.
## Can you implement feature X?
Yes, maybe. Check out [existing GitHub issues](https://github.com/binwiederhier/ntfy/issues) to see if somebody else had
the same idea before you, or file a new issue. I'll likely get back to you within a few days.
## I'm having issues with iOS, can you help? The iOS app is behind compared to the Android app, can you fix that?
The iOS is very bare bones and quite frankly a little buggy. I wanted to get something out the door to make the iOS users
happy, but halfway through I got frustrated with iOS development and paused development. I will eventually get back to
it, or hopefully, somebody else will come along and help out. Please review the [known issues](known-issues.md) for details.
## Can I disable the web app? Can I protect it with a login screen?
The web app is a static website without a backend (other than the ntfy API). All data is stored locally in the browser
cache and local storage. That means it does not need to be protected with a login screen, and it poses no additional
security risk. So technically, it does not need to be disabled.
However, if you still want to disable it, you can do so with the `web-root: disable` option in the `server.yml` file.
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
## Where can I donate?
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
appreciated.
## Why is there no iOS app (yet)?
I don't have an iPhone or a Mac, so I didn't make an iOS app yet. It'd be awesome if
<a href="https://github.com/binwiederhier/ntfy/issues/4">someone else could help out</a>.

View File

@@ -1,6 +0,0 @@
import os
import shutil
def copy_fonts(config, **kwargs):
site_dir = config['site_dir']
shutil.copytree('docs/static/fonts', os.path.join(site_dir, 'get'))

View File

@@ -5,7 +5,7 @@ or POST requests. I use it to notify myself when scripts fail, or long-running c
## Step 1: Get the app
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="../../static/img/badge-appstore.png"></a>
<a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="../../static/img/badge-appstore.png"></a>
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play or F-Droid.
Once installed, open it and subscribe to a topic of your choosing. Topics don't have to explicitly be created, so just
@@ -83,7 +83,7 @@ This will create a notification that looks like this:
</figure>
That's it. You're all set. Go play and read the rest of the docs. I highly recommend reading at least the page on
[publishing messages](publish.md), as well as the detailed page on the [Android/iOS app](subscribe/phone.md).
[publishing messages](publish.md), as well as the detailed page on the [Android app](subscribe/phone.md).
Here's another video showing the entire process:

View File

@@ -13,50 +13,50 @@ The ntfy server comes as a statically linked binary and is shipped as tarball, d
We support amd64, armv7 and arm64.
1. Install ntfy using one of the methods described below
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user) or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (or `/etc/ntfy/client.yml`, see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI][subscribe/cli.md]
for details).
## Linux binaries
## Binaries and packages
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_x86_64.tar.gz
tar zxvf ntfy_2.3.1_linux_x86_64.tar.gz
sudo cp -a ntfy_2.3.1_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.1_linux_x86_64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_x86_64.tar.gz
tar zxvf ntfy_1.21.2_linux_x86_64.tar.gz
sudo cp -a ntfy_1.21.2_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv6.tar.gz
tar zxvf ntfy_2.3.1_linux_armv6.tar.gz
sudo cp -a ntfy_2.3.1_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.1_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.tar.gz
tar zxvf ntfy_1.21.2_linux_armv6.tar.gz
sudo cp -a ntfy_1.21.2_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv7.tar.gz
tar zxvf ntfy_2.3.1_linux_armv7.tar.gz
sudo cp -a ntfy_2.3.1_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.1_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.tar.gz
tar zxvf ntfy_1.21.2_linux_armv7.tar.gz
sudo cp -a ntfy_1.21.2_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_arm64.tar.gz
tar zxvf ntfy_2.3.1_linux_arm64.tar.gz
sudo cp -a ntfy_2.3.1_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.1_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.tar.gz
tar zxvf ntfy_1.21.2_linux_arm64.tar.gz
sudo cp -a ntfy_1.21.2_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -65,10 +65,9 @@ Installation via Debian repository:
=== "x86_64/amd64"
```bash
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
sudo sh -c "echo 'deb [arch=amd64] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
@@ -78,11 +77,10 @@ Installation via Debian repository:
=== "armv7/armhf"
```bash
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=armhf signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo sh -c "echo 'deb [arch=armhf] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
@@ -91,11 +89,10 @@ Installation via Debian repository:
=== "arm64"
```bash
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=arm64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo sh -c "echo 'deb [arch=arm64] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
@@ -106,7 +103,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -114,7 +111,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -122,7 +119,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -130,7 +127,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -140,28 +137,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -179,52 +176,6 @@ cd ntfysh-bin
makepkg -si
```
## NixOS / Nix
ntfy is packaged in nixpkgs as `ntfy-sh`. It can be installed by adding the package name to the configuration file and calling `nixos-rebuild`. Alternatively, the following command can be used to install ntfy in the current user environment:
```
nix-env -iA ntfy-sh
```
NixOS also supports [declarative setup of the ntfy server](https://search.nixos.org/options?channel=unstable&show=services.ntfy-sh.enable&from=0&size=50&sort=relevance&type=packages&query=ntfy).
## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_macOS_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_macOS_all.tar.gz > ntfy_2.3.1_macOS_all.tar.gz
tar zxvf ntfy_2.3.1_macOS_all.tar.gz
sudo cp -a ntfy_2.3.1_macOS_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.3.1_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
!!! info
There is a [GitHub issue](https://github.com/binwiederhier/ntfy/issues/286) about making ntfy installable via
[Homebrew](https://brew.sh/). I'll eventually get to that, but I'd also love if somebody else stepped up to do it.
Also, you can build and run the ntfy server on macOS as well, though I don't officially support that.
Check out the [build instructions](develop.md) for details.
## Windows
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_windows_x86_64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
Also available in [Scoop's](https://scoop.sh) Main repository:
`scoop install ntfy`
!!! info
There is currently no installer for Windows, and the binary is not signed. If this is desired, please create a
[GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know.
## Docker
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
be pretty straight forward to use.
@@ -233,11 +184,6 @@ The server exposes its web UI and the API on port 80, so you need to expose that
[message cache](config.md#message-cache), you also need to map a volume to `/var/cache/ntfy`. To change other settings,
you should map `/etc/ntfy`, so you can edit `/etc/ntfy/server.yml`.
!!! info
Note that the Docker image **does not contain a `/etc/ntfy/server.yml` file**. If you'd like to use a config file,
please manually create one outside the image and map it as a volume, e.g. via `-v /etc/ntfy:/etc/ntfy`. You may
use the [`server.yml` file on GitHub](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml) as a template.
Basic usage (no cache or additional config):
```
docker run -p 80:80 -it binwiederhier/ntfy serve
@@ -250,23 +196,21 @@ docker run \
-p 80:80 \
-it \
binwiederhier/ntfy \
serve \
--cache-file /var/cache/ntfy/cache.db
--cache-file /var/cache/ntfy/cache.db \
serve
```
With other config options, timezone, and non-root user (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details):
With other config options (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details):
```bash
docker run \
-v /etc/ntfy:/etc/ntfy \
-e TZ=UTC \
-p 80:80 \
-u UID:GID \
-it \
binwiederhier/ntfy \
serve
```
Using docker-compose with non-root user and healthchecks enabled:
Using docker-compose:
```yaml
version: "2.1"
@@ -276,25 +220,14 @@ services:
container_name: ntfy
command:
- serve
environment:
- TZ=UTC # optional: set desired timezone
user: UID:GID # optional: replace with your own user/group or uid/gid
volumes:
- /var/cache/ntfy:/var/cache/ntfy
- /etc/ntfy:/etc/ntfy
ports:
- 80:80
healthcheck: # optional: remember to adapt the host:port to your environment
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
```
If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files and attachments directory to the same uid/gid.
Alternatively, you may wish to build a customized Docker image that can be run with fewer command-line arguments and without delivering the configuration file separately.
```
FROM binwiederhier/ntfy
@@ -302,300 +235,3 @@ COPY server.yml /etc/ntfy/server.yml
ENTRYPOINT ["ntfy", "serve"]
```
This image can be pushed to a container registry and shipped independently. All that's needed when running it is mapping ntfy's port to a host port.
## Kubernetes
The setup for Kubernetes is very similar to that for Docker, and requires a fairly minimal deployment or pod definition to function. There
are a few options to mix and match, including a deployment without a cache file, a stateful set with a persistent cache, and a standalone
unmanned pod.
=== "deployment"
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ntfy
spec:
selector:
matchLabels:
app: ntfy
template:
metadata:
labels:
app: ntfy
spec:
containers:
- name: ntfy
image: binwiederhier/ntfy
args: ["serve"]
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
name: http
volumeMounts:
- name: config
mountPath: "/etc/ntfy"
readOnly: true
volumes:
- name: config
configMap:
name: ntfy
---
# Basic service for port 80
apiVersion: v1
kind: Service
metadata:
name: ntfy
spec:
selector:
app: ntfy
ports:
- port: 80
targetPort: 80
```
=== "stateful set"
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ntfy
spec:
selector:
matchLabels:
app: ntfy
serviceName: ntfy
template:
metadata:
labels:
app: ntfy
spec:
containers:
- name: ntfy
image: binwiederhier/ntfy
args: ["serve", "--cache-file", "/var/cache/ntfy/cache.db"]
ports:
- containerPort: 80
name: http
volumeMounts:
- name: config
mountPath: "/etc/ntfy"
readOnly: true
- name: cache
mountPath: "/var/cache/ntfy"
volumes:
- name: config
configMap:
name: ntfy
volumeClaimTemplates:
- metadata:
name: cache
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
```
=== "pod"
```yaml
apiVersion: v1
kind: Pod
metadata:
labels:
app: ntfy
spec:
containers:
- name: ntfy
image: binwiederhier/ntfy
args: ["serve"]
resources:
limits:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
name: http
volumeMounts:
- name: config
mountPath: "/etc/ntfy"
readOnly: true
volumes:
- name: config
configMap:
name: ntfy
```
Configuration is relatively straightforward. As an example, a minimal configuration is provided.
=== "resource definition"
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ntfy
data:
server.yml: |
# Template: https://github.com/binwiederhier/ntfy/blob/main/server/server.yml
base-url: https://ntfy.sh
```
=== "from-file"
```bash
kubectl create configmap ntfy --from-file=server.yml
```
## Kustomize
ntfy can be deployed in a Kubernetes cluster with [Kustomize](https://github.com/kubernetes-sigs/kustomize), a tool used
to customize Kubernetes objects using a `kustomization.yaml` file.
1. Create new folder - `ntfy`
2. Add all files listed below
1. `kustomization.yaml` - stores all configmaps and resources used in a deployment
2. `ntfy-deployment.yaml` - define deployment type and its parameters
3. `ntfy-pvc.yaml` - describes how [persistent volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) will be created
4. `ntfy-svc.yaml` - expose application to the internal kubernetes network
5. `ntfy-ingress.yaml` - expose service to outside the network using [ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/)
6. `server.yaml` - simple server configuration
3. Replace **TESTNAMESPACE** within `kustomization.yaml` with designated namespace
4. Replace **ntfy.test** within `ntfy-ingress.yaml` with desired DNS name
5. Apply configuration to cluster set in current context:
```bash
kubectl apply -k /ntfy
```
=== "kustomization.yaml"
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ntfy-deployment.yaml # deployment definition
- ntfy-svc.yaml # service connecting pods to cluster network
- ntfy-pvc.yaml # pvc used to store cache and attachment
- ntfy-ingress.yaml # ingress definition
configMapGenerator: # will parse config from raw config to configmap,it allows for dynamic reload of application if additional app is deployed ie https://github.com/stakater/Reloader
- name: server-config
files:
- server.yml
namespace: TESTNAMESPACE # select namespace for whole application
```
=== "ntfy-deployment.yaml"
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ntfy-deployment
labels:
app: ntfy-deployment
spec:
revisionHistoryLimit: 1
replicas: 1
selector:
matchLabels:
app: ntfy-pod
template:
metadata:
labels:
app: ntfy-pod
spec:
containers:
- name: ntfy
image: binwiederhier/ntfy:v1.28.0 # set deployed version
args: ["serve"]
env: #example of adjustments made in environmental variables
- name: TZ # set timezone
value: XXXXXXX
- name: NTFY_DEBUG # enable/disable debug
value: "false"
- name: NTFY_LOG_LEVEL # adjust log level
value: INFO
- name: NTFY_BASE_URL # add base url
value: XXXXXXXXXX
ports:
- containerPort: 80
name: http-ntfy
resources:
limits:
memory: 300Mi
cpu: 200m
requests:
cpu: 150m
memory: 150Mi
volumeMounts:
- mountPath: /etc/ntfy/server.yml
subPath: server.yml
name: config-volume # generated vie configMapGenerator from kustomization file
- mountPath: /var/cache/ntfy
name: cache-volume #cache volume mounted to persistent volume
volumes:
- name: config-volume
configMap: # uses configmap generator to parse server.yml to configmap
name: server-config
- name: cache-volume
persistentVolumeClaim: # stores /cache/ntfy in defined pv
claimName: ntfy-pvc
```
=== "ntfy-pvc.yaml"
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ntfy-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path # adjust storage if needed
resources:
requests:
storage: 1Gi
```
=== "ntfy-svc.yaml"
```yaml
apiVersion: v1
kind: Service
metadata:
name: ntfy-svc
spec:
type: ClusterIP
selector:
app: ntfy-pod
ports:
- name: http-ntfy-out
protocol: TCP
port: 80
targetPort: http-ntfy
```
=== "ntfy-ingress.yaml"
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ntfy-ingress
spec:
rules:
- host: ntfy.test #select own
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ntfy-svc
port:
number: 80
```
=== "server.yml"
```yaml
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
```

View File

@@ -1,182 +0,0 @@
# Integrations + community projects
There are quite a few projects that work with ntfy, integrate ntfy, or have been built around ntfy. It's super exciting to see what you guys have come up with. Feel free to [create a pull request on GitHub](https://github.com/binwiederhier/ntfy/issues) to add your own project here.
I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community.
## Public ntfy servers
Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the
ntfy community. Thanks to everyone running a public server. **You guys rock!**
| URL | Country |
|---------------------------------------------------|--------------------|
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States |
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France |
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
and uptime of third party servers, so use of each server is **at your own discretion**.
## Official integrations
- [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs
- [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push notifications that work with just about every platform
- [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool
- [Robusta](https://docs.robusta.dev/master/catalog/sinks/webhook.html) ⭐ - open source platform for Kubernetes troubleshooting
- [borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#third-party-monitoring-services) ⭐ - configuration-driven backup software for servers and workstations
- [Radarr](https://radarr.video/) ⭐ - Movie collection manager for Usenet and BitTorrent users
- [Sonarr](https://sonarr.tv/) ⭐ - PVR for Usenet and BitTorrent users
- [Gatus](https://gatus.io/) ⭐ - Automated service health dashboard
- [Automatisch](https://automatisch.io/) ⭐ - Open source Zapier alternative / workflow automation tool
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
- [Scrt.link](https://scrt.link/) - Share a secret
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
- [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
- [Element](https://f-droid.org/packages/im.vector.app/) ⭐ - Matrix client
- [SchildiChat](https://schildi.chat/android/) ⭐ - Matrix client
- [Tusky](https://tusky.app/) ⭐ - Fediverse client
- [Fedilab](https://fedilab.app/) - Fediverse client
- [FindMyDevice](https://gitlab.com/Nulide/findmydevice/) - Find your Device with an SMS or online with the help of FMDServer
- [Tox Push Message App](https://github.com/zoff99/tox_push_msg_app) - Tox Push Message App
## Libraries
- [ntfy-php-library](https://github.com/VerifiedJoseph/ntfy-php-library) - PHP library for sending messages using a ntfy server (PHP)
- [ntfy-notifier](https://github.com/DAcodedBEAT/ntfy-notifier) - Symfony Notifier integration for ntfy (PHP)
- [ntfpy](https://github.com/Nevalicjus/ntfpy) - API Wrapper for ntfy.sh (Python)
- [pyntfy](https://github.com/DP44/pyntfy) - A module for interacting with ntfy notifications (Python)
- [vntfy](https://github.com/lmangani/vntfy) - Barebone V client for ntfy (V)
- [ntfy-middleman](https://github.com/nachotp/ntfy-middleman) - Wraps APIs and send notifications using ntfy.sh on schedule (Python)
- [ntfy-dotnet](https://github.com/nwithan8/ntfy-dotnet) - .NET client library to interact with a ntfy server (C# / .NET)
- [node-ntfy-publish](https://github.com/cityssm/node-ntfy-publish) - A Node package to publish notifications to an ntfy server (Node)
- [ntfy](https://github.com/jonocarroll/ntfy) - Wraps the ntfy API with pipe-friendly tooling (R)
- [ntfy-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi)
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
## CLIs + GUIs
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
- [ntfy Desktop client](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
- [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop
- [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy
- [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications
## Projects + scripts
- [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust)
- [Grafana-ntfy-webhook-integration](https://github.com/academo/grafana-alerting-ntfy-webhook-integration) - Integrates Grafana alerts webhooks (Go)
- [Grafana-to-ntfy](https://gitlab.com/Saibe1111/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Node Js)
- [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
- [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
- [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
- [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs)
- [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell)
- [grav-plugin-whistleblower](https://github.com/Himmlisch-Studios/grav-plugin-whistleblower) - Grav CMS plugin to get notifications via ntfy (PHP)
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
- [ntfy Discord bot](https://github.com/binwiederhier/ntfy-bot) - ntfy Discord bot (Go)
- [ntfy Discord bot](https://github.com/jr1221/ntfy_discord_bot) - An advanced modal-based bot for interacting with the ntfy.sh API (Dart)
- [Bettarr Notifications](https://github.com/NiNiyas/Bettarr-Notifications) - Better Notifications for Sonarr and Radarr (Python)
- [Notify me the intruders](https://github.com/nothingbutlucas/notify_me_the_intruders) - Notify you if they are intruders or new connections on your network (Shell)
- [Send GitHub Action to ntfy](https://github.com/NiNiyas/ntfy-action) - Send GitHub Action workflow notifications to ntfy (JS)
- [aTable/ntfy alertmanager bridge](https://github.com/aTable/ntfy_alertmanager_bridge) - Basic alertmanager bridge to ntfy (JS)
- [~xenrox/ntfy-alertmanager](https://hub.xenrox.net/~xenrox/ntfy-alertmanager) - A bridge between ntfy and Alertmanager (Go)
- [pinpox/alertmanager-ntfy](https://github.com/pinpox/alertmanager-ntfy) - Relay prometheus alertmanager alerts to ntfy (Go)
- [alexbakker/alertmanager-ntfy](https://github.com/alexbakker/alertmanager-ntfy) - Service that forwards Prometheus Alertmanager notifications to ntfy (Go)
- [restreamchat2ntfy](https://github.com/kurohuku7/restreamchat2ntfy) - Send restream.io chat to ntfy to check on the Meta Quest (JS)
- [k8s-ntfy-deployment-service](https://github.com/Christian42/k8s-ntfy-deployment-service) - Automatic Kubernetes (k8s) ntfy deployment
- [huginn-global-entry-notif](https://github.com/kylezoa/huginn-global-entry-notif) - Checks CBP API for available appointments with Huginn (JSON)
- [ntfyer](https://github.com/KikyTokamuro/ntfyer) - Sending various information to your ntfy topic by time (TypeScript)
- [git-simple-notifier](https://github.com/plamenjm/git-simple-notifier) - Script running git-log, checking for new repositories (Shell)
- [ntfy-to-slack](https://github.com/ozskywalker/ntfy-to-slack) - Tool to subscribe to a ntfy topic and send the messages to a Slack webhook (Go)
- [ansible-ntfy](https://github.com/jpmens/ansible-ntfy) - Ansible action plugin to post JSON messages to ntfy (Python)
- [ntfy-notification-channel](https://github.com/wijourdil/ntfy-notification-channel) - Laravel Notification channel for ntfy (PHP)
- [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
- [ntfy-sdk](https://github.com/yukibtc/ntfy-sdk) - ntfy client library to send notifications (Rust)
- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost
- [drone-ntfy](https://github.com/Clortox/drone-ntfy) - Drone.io plugin for sending ntfy notifications from a pipeline (Shell)
- [ignition-ntfy-module](https://github.com/Kyvis-Labs/ignition-ntfy-module) - Adds support for sending notifications via a ntfy server to Ignition (Java)
- [maubot-ntfy](https://gitlab.com/999eagle/maubot-ntfy) - Matrix bot to subscribe to ntfy topics and send messages to Matrix (Python)
- [ntfy-wrapper](https://github.com/vict0rsch/ntfy-wrapper) - Wrapper around ntfy (Python)
- [nodebb-plugin-ntfy](https://github.com/NodeBB/nodebb-plugin-ntfy) - Push notifications for NodeBB forums
- [n8n-ntfy](https://github.com/raghavanand98/n8n-ntfy.sh) - n8n community node that lets you use ntfy in your workflows
- [nlog-ntfy](https://github.com/MichelMichels/nlog-ntfy) - Send NLog messages over ntfy (C# / .NET / NLog)
- [helm-charts](https://github.com/sarab97/helm-charts) - Helm charts of some of the selfhosted services, incl. ntfy
- [ntfy_ansible_role](https://github.com/stevenengland/ntfy_ansible_role) (on [Ansible Galaxy](https://galaxy.ansible.com/stevenengland/ntfy)) - Ansible role to install ntfy
## Blog + forum posts
- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023
- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023
- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023
- [Deploying ntfy on railway](https://www.youtube.com/watch?v=auJICXtxoNA) - youtube.com - 3/2023
- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023
- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023
- [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023
- [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023
- [Video: Simple Push Notifications ntfy](https://www.youtube.com/watch?v=u9EcWrsjE20) ⭐ - youtube.com - 2/2023
- [Use ntfy.sh with Home Assistant](https://diecknet.de/en/2023/02/12/ntfy-sh-with-homeassistant/) - diecknet.de - 2/2023
- [On installe Ntfy sur Synology Docker](https://www.maison-et-domotique.com/140356-serveur-notification-jeedom-ntfy-synology-docker/) - maison-et-domotique.co - 1/2023
- [January 2023 Developer Update](https://community.nodebb.org/topic/16908/january-2023-developer-update) - nodebb.org - 1/2023
- [Comment envoyer des notifications push sur votre téléphone facilement et gratuitement?](https://korben.info/notifications-push-telephone.html) - 1/2023
- [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022
- [ntfy setup instructions](https://docs.benjamin-altpeter.de/network/vms/1001029-ntfy/) - benjamin-altpeter.de - 12/2022
- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022
- [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
- [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.com - 11/2022
- [Changelog | Tracking layoffs, tech worker demand still high, ntfy, ...](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) ⭐ - changelog.com - 11/2022
- [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022
- [Envie Push Notifications por POST (de graça e sem cadastro)](https://www.tabnews.com.br/filipedeschamps/envie-push-notifications-por-post-de-graca-e-sem-cadastro) - tabnews.com.br - 11/2022
- [Push Notifications for KDE](https://volkerkrause.eu/2022/11/12/kde-unifiedpush-push-notifications.html) - volkerkrause.eu - 11/2022
- [TLDR Newsletter Daily Update 2022-11-09](https://tldr.tech/tech/newsletter/2022-11-09) ⭐ - tldr.tech - 11/2022
- [Ntfy.sh Send push notifications to your phone via PUT/POST](https://news.ycombinator.com/item?id=33517944) ⭐ - news.ycombinator.com - 11/2022
- [Ntfy et Jeedom : un plugin](https://lunarok-domotique.com/2022/11/ntfy-et-jeedom/) - lunarok-domotique.com - 11/2022
- [Crea tu propio servidor de notificaciones con Ntfy](https://blog.parravidales.es/crea-tu-propio-servidor-de-notificaciones-con-ntfy/) - blog.parravidales.es - 11/2022
- [unRAID Notifications with ntfy.sh](https://lder.dev/posts/ntfy-Notifications-With-unRAID/) - lder.dev - 10/2022
- [Zero-cost push notifications to your phone or desktop via PUT/POST ](https://lobste.rs/s/41dq13/zero_cost_push_notifications_your_phone) - lobste.rs - 10/2022
- [A nifty push notification system: ntfy](https://jpmens.net/2022/10/30/a-nifty-push-notification-system-ntfy/) - jpmens.net - 10/2022
- [Alarmanlage der dritten Art (YouTube video)](https://www.youtube.com/watch?v=altb5QLHbaU&feature=youtu.be) - youtube.com - 10/2022
- [Neue Services: Ntfy, TikTok und RustDesk](https://adminforge.de/tools/neue-services-ntfy-tiktok-und-rustdesk/) - adminforge.de - 9/2022
- [Ntfy, le service de notifications quil vous faut](https://www.cachem.fr/ntfy-le-service-de-notifications-quil-vous-faut/) - cachem.fr - 9/2022
- [NAS Synology et notifications avec ntfy](https://www.cachem.fr/synology-notifications-ntfy/) - cachem.fr - 9/2022
- [Self hosted Mobile Push Notifications using NTFY | Thejesh GN](https://thejeshgn.com/2022/08/23/self-hosted-mobile-push-notifications-using-ntfy/) - thejeshgn.com - 8/2022
- [Fedora Magazine | 4 cool new projects to try in Copr](https://fedoramagazine.org/4-cool-new-projects-to-try-in-copr-for-august-2022/) - fedoramagazine.org - 8/2022
- [Docker로 오픈소스 푸시알람 프로젝트 ntfy.sh 설치 및 사용하기.(Feat. Uptimekuma)](https://svrforum.com/svr/398979) - svrforum.com - 8/2022
- [Easy notifications from R](https://sometimesir.com/posts/easy-notifications-from-r/) - sometimesir.com - 6/2022
- [ntfy is finally coming to iOS, and Matrix/UnifiedPush gateway support](https://www.reddit.com/r/selfhosted/comments/vdzvxi/ntfy_is_finally_coming_to_ios_with_full/) ⭐ - reddit.com - 6/2022
- [Install guide (with Docker)](https://chowdera.com/2022/150/202205301257379077.html) - chowdera.com - 5/2022
- [无需注册的通知服务ntfy](https://blog.csdn.net/wbsu2004/article/details/125040247) - blog.csdn.net - 5/2022
- [Updated review post (Jan-Lukas Else)](https://jlelse.blog/thoughts/2022/04/ntfy) - jlelse.blog - 4/2022
- [Using ntfy and Tasker together](https://lachlanlife.net/posts/2022-04-tasker-ntfy/) - lachlanlife.net - 4/2022
- [Reddit feature update post](https://www.reddit.com/r/selfhosted/comments/uetlso/ntfy_is_a_tool_to_send_push_notifications_to_your/) ⭐ - reddit.com - 4/2022
- [無料で簡単に通知の送受信ができつつオープンソースでセルフホストも可能な「ntfy」を使ってみた](https://gigazine.net/news/20220404-ntfy-push-notification/) - gigazine.net - 4/2022
- [Pocketmags ntfy review](https://pocketmags.com/us/linux-format-magazine/march-2022/articles/1104187/ntfy) - pocketmags.com - 3/2022
- [Reddit web app release post](https://www.reddit.com/r/selfhosted/comments/tc0p0u/say_hello_to_the_brand_new_ntfysh_web_app_push/) ⭐ - reddit.com- 3/2022
- [Lemmy post (Jakob)](https://lemmy.eus/post/15541) - lemmy.eus - 1/2022
- [Reddit UnifiedPush release post](https://www.reddit.com/r/selfhosted/comments/s5jylf/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 1/2022
- [ntfy: send notifications from your computer to your phone](https://rs1.es/tutorials/2022/01/19/ntfy-send-notifications-phone.html) - rs1.es - 1/2022
- [Short ntfy review (Jan-Lukas Else)](https://jlelse.blog/links/2021/12/ntfy-sh) - jlelse.blog - 12/2021
- [Free MacroDroid webhook alternative (FrameXX)](https://www.macrodroidforum.com/index.php?threads/ntfy-sh-free-macrodroid-webhook-alternative.1505/) - macrodroidforum.com - 12/2021
- [ntfy otro sistema de notificaciones pub-sub simple basado en HTTP](https://ugeek.github.io/blog/post/2021-11-05-ntfy-sh-otro-sistema-de-notificaciones-pub-sub-simple-basado-en-http.html) - ugeek.github.io - 11/2021
- [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - news.ycombinator.com - 12/2021
- [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 11/2021

View File

@@ -1,28 +0,0 @@
# Known issues
This is an incomplete list of known issues with the ntfy server, Android app, and iOS app. You can find a complete
list [on GitHub](https://github.com/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug), but I thought it may be helpful
to have the prominent ones here to link to.
## iOS app not refreshing (see [#267](https://github.com/binwiederhier/ntfy/issues/267))
For some (many?) users, the iOS app is not refreshing the view when new notifications come in. Until you manually
swipe down, you do not see the newly arrived messages, even though the popup appeared before.
This is caused by some weirdness between the Notification Service Extension (NSE), SwiftUI and Core Data. I am entirely
clueless on how to fix it, sadly, as it is ephemeral and not clear to me what is causing it.
Please send experienced iOS developers my way to help me figure this out.
## iOS app not receiving notifications (anymore)
If notifications do not show up at all anymore, there are a few causes for it (that I know of):
**Firebase+APNS are being weird and buggy**:
If this is the case, usually it helps to **remove the topic/subscription and re-add it**. That will force Firebase to
re-subscribe to the Firebase topic.
**Self-hosted only: No `upstream-base-url` set, or `base-url` mismatch**:
To make self-hosted servers work with the iOS
app, I had to do some horrible things (see [iOS instant notifications](config.md#ios-instant-notifications) for details).
Be sure that in your selfhosted server:
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to

View File

@@ -8,5 +8,5 @@ any outside service. All data is exclusively used to make the service function p
I use is Firebase Cloud Messaging (FCM) service, which is required to provide instant Android notifications (see
[FAQ](faq.md) for details). To avoid FCM altogether, download the F-Droid version.
For debugging purposes, the ntfy server may temporarily log request paths, remote IP addresses or even topics
or messages, though typically this is turned off.
The web server does not log or otherwise store request paths, remote IP addresses or even topics or messages,
aside from a short on-disk cache to support service restarts.

File diff suppressed because it is too large Load Diff

View File

@@ -2,670 +2,15 @@
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
### ntfy server v2.3.1
Released March 30, 2023
<!--
This release disables server-initiated polling of iOS devices entirely, thereby eliminating the thundering herd problem
on ntfy.sh that we observe every 20 minutes. The polling was never strictly necessary, and has actually caused duplicate
delivery issues as well, so disabling it should not have any negative effects. iOS users, please reach out via Discord
or Matrix if there are issues.
**Bug fixes + maintenance:**
* Disable iOS polling entirely ([#677](https://github.com/binwiederhier/ntfy/issues/677)/[#509](https://github.com/binwiederhier/ntfy/issues/509))
## ntfy server v2.3.0
Released March 29, 2023
This release primarily fixes an issue with delayed messages, and it adds support for Go's profiler (if enabled), which
will allow investigating usage spikes in more detail. There will likely be a follow-up release this week to fix the
actual spikes [caused by iOS devices](https://github.com/binwiederhier/ntfy/issues/677).
## ntfy Android app v1.13.0 (UNRELEASED)
**Features:**
* ntfy now supports Go's `pprof` profiler, if enabled (relates to [#677](https://github.com/binwiederhier/ntfy/issues/677))
* Cards in notification detail view ([#175](https://github.com/binwiederhier/ntfy/issues/224), thanks to [@cmeis](https://github.com/cmeis) for reporting)
**Bug fixes + maintenance:**
* Fix delayed message sending from authenticated users ([#679](https://github.com/binwiederhier/ntfy/issues/679))
* Fixed plural for Polish and other translations ([#678](https://github.com/binwiederhier/ntfy/pull/678), thanks to [@bmoczulski](https://github.com/bmoczulski))
## ntfy server v2.2.0
Released March 17, 2023
With this release, ntfy is now able to expose metrics via a `/metrics` endpoint for [Prometheus](https://prometheus.io/), if enabled.
The endpoint exposes about 20 different counters and gauges, from the number of published messages and emails, to active subscribers,
visitors and topics. If you'd like more metrics, pop in the Discord/Matrix or file an issue on GitHub.
On top of this, you can now use access tokens in the ntfy CLI (defined in the `client.yml` file), fixed a bug in `ntfy subscribe`,
removed the dependency on Google Fonts, and more.
🔥 Reminder: Purchase one of three **ntfy Pro plans** for **50% off** for a limited time (if you use promo code `MYTOPIC`).
ntfy Pro gives you higher rate limits and lets you reserve topic names. [Buy through web app](https://ntfy.sh/app).
❤️ If you don't need ntfy Pro, please consider sponsoring ntfy via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/). ntfy will stay open source forever.
**Features:**
* Monitoring: ntfy now exposes a `/metrics` endpoint for [Prometheus](https://prometheus.io/) if [configured](config.md#monitoring) ([#210](https://github.com/binwiederhier/ntfy/issues/210), thanks to [@rogeliodh](https://github.com/rogeliodh) for reporting)
* You can now use tokens in `client.yml` for publishing and subscribing ([#653](https://github.com/binwiederhier/ntfy/issues/653), thanks to [@wunter8](https://github.com/wunter8))
**Bug fixes + maintenance:**
* `ntfy sub --poll --from-config` will now include authentication headers from client.yml (if applicable) ([#658](https://github.com/binwiederhier/ntfy/issues/658), thanks to [@wunter8](https://github.com/wunter8))
* Docs: Removed dependency on Google Fonts in docs ([#554](https://github.com/binwiederhier/ntfy/issues/554), thanks to [@bt90](https://github.com/bt90) for reporting, and [@ozskywalker](https://github.com/ozskywalker) for implementing)
* Increase allowed auth failure attempts per IP address to 30 (no ticket)
* Web app: Increase maximum incremental backoff retry interval to 2 minutes (no ticket)
**Documentation:**
* Make query parameter description more clear ([#630](https://github.com/binwiederhier/ntfy/issues/630), thanks to [@bbaa-bbaa](https://github.com/bbaa-bbaa) for reporting, and to [@wunter8](https://github.com/wunter8) for a fix)
## ntfy server v2.1.2
Released March 4, 2023
This is a hotfix release, mostly to combat the ridiculous amount of Matrix requests with invalid/dead pushkeys, and the
corresponding HTTP 507 responses the ntfy.sh server is sending out. We're up to >600k HTTP 507 responses per day 🤦. This
release solves this issue by rejecting Matrix pushkeys, if nobody has subscribed to the corresponding topic for 12 hours.
The release furthermore reverts the default rate limiting behavior for UnifiedPush to be publisher-based, and introduces
a flag to enable [subscriber-based rate limiting](config.md#subscriber-based-rate-limiting) for high volume servers.
**Features:**
* Support SMTP servers without auth ([#645](https://github.com/binwiederhier/ntfy/issues/645), thanks to [@Sharknoon](https://github.com/Sharknoon) for reporting)
**Bug fixes + maintenance:**
* Token auth doesn't work if default user credentials are defined in `client.yml` ([#650](https://github.com/binwiederhier/ntfy/issues/650), thanks to [@Xinayder](https://github.com/Xinayder))
* Add `visitor-subscriber-rate-limiting` flag to allow enabling [subscriber-based rate limiting](config.md#subscriber-based-rate-limiting) (off by default now, [#649](https://github.com/binwiederhier/ntfy/issues/649)/[#655](https://github.com/binwiederhier/ntfy/pull/655), thanks to [@barathrm](https://github.com/barathrm) for reporting, and to [@karmanyaahm](https://github.com/karmanyaahm) and [@p1gp1g](https://github.com/p1gp1g) for help with the design)
* Reject Matrix pushkey after 12 hours of inactivity on a topic, if `visitor-subscriber-rate-limiting` is enabled ([#643](https://github.com/binwiederhier/ntfy/pull/643), thanks to [@karmanyaahm](https://github.com/karmanyaahm) and [@p1gp1g](https://github.com/p1gp1g) for help with the design)
**Additional languages:**
* Danish (thanks to [@Andersbiha](https://hosted.weblate.org/user/Andersbiha/))
## ntfy server v2.1.1
Released March 1, 2023
This is a tiny release with a few bug fixes, but it's big for me personally. After almost three months of work,
**today I am finally launching the paid plans on ntfy.sh** 🥳 🎉.
You are now able to purchase one of three plans that'll give you **higher rate limits** (messages, emails, attachment sizes, ...),
as well as the ability to **reserve topic names** for your personal use, while at the same time supporting me and the
ntfy open source project ❤️. You can check out the pricing, and [purchase plans through the web app](https://ntfy.sh/app) (use
promo code `MYTOPIC` for a **50% discount**, limited time only).
And as I've said many times: Do not worry. **ntfy will always stay open source**, and that includes all features. There
are no closed-source features. So if you'd like to run your own server, you can!
**Bug fixes + maintenance:**
* Fix panic when using Firebase without users ([#641](https://github.com/binwiederhier/ntfy/issues/641), thanks to [u/heavybell](https://www.reddit.com/user/heavybell/) for reporting)
* Remove health check from `Dockerfile` and [document it](config.md#health-checks) ([#635](https://github.com/binwiederhier/ntfy/issues/635), thanks to [@Andersbiha](https://github.com/Andersbiha))
* Upgrade dialog: Disable submit button for free tier (no ticket)
* Allow multiple `log-level-overrides` on the same field (no ticket)
* Actually remove `ntfy publish --env-topic` flag (as per [deprecations](deprecations.md), no ticket)
* Added `billing-contact` config option (no ticket)
## ntfy server v2.1.0
Released February 25, 2023
This release changes the way UnifiedPush (UP) topics are rate limited from publisher-based rate limiting to subscriber-based
rate limiting. This allows UP application servers to send higher volumes, since the subscribers carry the rate limits.
However, it also means that UP clients have to subscribe to a topic first before they are allowed to publish. If they do
no, clients will receive an HTTP 507 response from the server.
We also fixed another issue with UnifiedPush: Some Mastodon servers were sending unsupported `Authorization` headers,
which ntfy rejected with an HTTP 401. We now ignore unsupported header values.
As of this release, ntfy also supports sending emails to protected topics, and it ships code to support annual billing
cycles (not live yet).
As part of this release, I also enabled sign-up and login (free accounts only), and I also started reducing the rate
limits for anonymous & free users a bit. With the next release and the launch of the paid plan, I'll reduce the limits
a bit more. For 90% of users, you should not feel the difference.
**Features:**
* UnifiedPush: Subscriber-based rate limiting for `up*` topics ([#584](https://github.com/binwiederhier/ntfy/pull/584)/[#609](https://github.com/binwiederhier/ntfy/pull/609)/[#633](https://github.com/binwiederhier/ntfy/pull/633), thanks to [@karmanyaahm](https://github.com/karmanyaahm))
* Support for publishing to protected topics via email with access tokens ([#612](https://github.com/binwiederhier/ntfy/pull/621), thanks to [@tamcore](https://github.com/tamcore))
* Support for base64-encoded and nested multipart emails ([#610](https://github.com/binwiederhier/ntfy/issues/610), thanks to [@Robert-litts](https://github.com/Robert-litts))
* Payments: Add support for annual billing intervals (no ticket)
**Bug fixes + maintenance:**
* Web: Do not disable "Reserve topic" checkbox for admins (no ticket, thanks to @xenrox for reporting)
* UnifiedPush: Treat non-Basic/Bearer `Authorization` header like header was not sent ([#629](https://github.com/binwiederhier/ntfy/issues/629), thanks to [@Boebbele](https://github.com/Boebbele) and [@S1m](https://github.com/S1m) for reporting)
**Documentation:**
* Added example for [Traccar](https://ntfy.sh/docs/examples/#traccar) ([#631](https://github.com/binwiederhier/ntfy/pull/631), thanks to [tamcore](https://github.com/tamcore))
**Additional languages:**
* Arabic (thanks to [@ButterflyOfFire](https://hosted.weblate.org/user/ButterflyOfFire/))
## ntfy server v2.0.1
Released February 17, 2023
This is a quick bugfix release to address a panic that happens when `attachment-cache-dir` is not set.
**Bug fixes + maintenance:**
* Avoid panic in manager when `attachment-cache-dir` is not set ([#617](https://github.com/binwiederhier/ntfy/issues/617), thanks to [@ksurl](https://github.com/ksurl))
* Ensure that calls to standard logger `log.Println` also output JSON (no ticket)
## ntfy server v2.0.0
Released February 16, 2023
This is the biggest ntfy server release I've ever done 🥳 . Lots of new and exciting features.
**Brand-new features:**
* **User signup/login & account sync**: If enabled, users can now register to create a user account, and then login to
the web app. Once logged in, topic subscriptions and user settings are stored server-side in the user account (as
opposed to only in the browser storage). So far, this is implemented only in the web app only. Once it's in the Android/iOS
app, you can easily keep your account in sync. Relevant [config options](config.md#config-options) are `enable-signup` and
`enable-login`.
<div id="account-screenshots" class="screenshots">
<a href="../../static/img/web-signup.png"><img src="../../static/img/web-signup.png"/></a>
<a href="../../static/img/web-account.png"><img src="../../static/img/web-account.png"/></a>
</div>
* **Topic reservations** 🎉: If enabled, users can now **reserve topics and restrict access to other users**.
Once this is fully rolled out, you may reserve `ntfy.sh/philbackups` and define access so that only you can publish/subscribe
to the topic. Reservations let you claim ownership of a topic, and you can define access permissions for others as
`deny-all` (only you have full access), `read-only` (you can publish/subscribe, others can subscribe), `write-only` (you
can publish/subscribe, others can publish), `read-write` (everyone can publish/subscribe, but you remain the owner).
Topic reservations can be [configured](config.md#config-options) in the web app if `enable-reservations` is enabled, and
only if the user has a [tier](config.md#tiers) that supports reservations.
<div id="reserve-screenshots" class="screenshots">
<a href="../../static/img/web-reserve-topic.png"><img src="../../static/img/web-reserve-topic.png"/></a>
<a href="../../static/img/web-reserve-topic-dialog.png"><img src="../../static/img/web-reserve-topic-dialog.png"/></a>
</div>
* **Access tokens:** It is now possible to create user access tokens for a user account. Access tokens are useful
to avoid having to paste your password to various applications or scripts. For instance, you may want to use a
dedicated token to publish from your backup host, and one from your home automation system. Tokens can be configured
in the web app, or via the `ntfy token` command. See [creating tokens](config.md#access-tokens),
and [publishing using tokens](publish.md#access-tokens).
<div id="token-screenshots" class="screenshots">
<a href="../../static/img/web-token-create.png"><img src="../../static/img/web-token-create.png"/></a>
<a href="../../static/img/web-token-list.png"><img src="../../static/img/web-token-list.png"/></a>
</div>
* **Structured logging:** I've redone a lot of the logging to make it more structured, and to make it easier to debug and
troubleshoot. Logs can now be written to a file, and as JSON (if configured). Each log event carries context fields
that you can filter and search on using tools like `jq`. On top of that, you can override the log level if certain fields
match. For instance, you can say `user_name=phil -> debug` to log everything related to a certain user with debug level.
See [logging & debugging](config.md#logging-debugging).
* **Tiers:** You can now define and associate usage tiers to users. Tiers can be used to grant users higher limits, such as
daily message limits, attachment size, or make it possible for users to reserve topics. You could, for instance, have
a tier `Standard` that allows 500 messages/day, 15 MB attachments and 5 allowed topic reservations, and another
tier `Friends & Family` with much higher limits. For ntfy.sh, I'll mostly use these tiers to facilitate paid plans (see below).
Tiers can be configured via the `ntfy tier ...` command. See [tiers](config.md#tiers).
* **Paid tiers:** Starting very soon, I will be offering paid tiers for ntfy.sh on top of the free service. You'll be
able to subscribe to tiers with higher rate limits (more daily messages, bigger attachments) and topic reservations.
Paid tiers are facilitated by integrating [Stripe](https://stripe.com) as a payment provider. See [payments](config.md#payments)
for details.
**ntfy is forever open source!**
Yes, I will be offering some paid plans. But you don't need to panic! I won't be taking any features away, and everything
will remain forever open source, so you can self-host if you like. Similar to the donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/), paid plans will help pay for the service and keep me motivated to keep
going. It'll only make ntfy better.
**Other tickets:**
* User account signup, login, topic reservations, access tokens, tiers etc. ([#522](https://github.com/binwiederhier/ntfy/issues/522))
* `OPTIONS` method calls are not serviced when the UI is disabled ([#598](https://github.com/binwiederhier/ntfy/issues/598), thanks to [@enticedwanderer](https://github.com/enticedwanderer) for reporting)
**Special thanks:**
A big Thank-you goes to everyone who tested the user account and payments work. I very much appreciate all the feedback,
suggestions, and bug reports. Thank you, @nwithan8, @deadcade, @xenrox, @cmeis, @wunter8 and the others who I forgot.
## ntfy server v1.31.0
Released February 14, 2023
This is a tiny release before the really big release, and also the last before the big v2.0.0. The most interesting
things in this release are the new preliminary health endpoint to allow monitoring in K8s (and others), and the removal
of `upx` binary packing (which was causing erroneous virus flagging). Aside from that, the `go-smtp` library did a
breaking-change upgrade, which required some work to get working again.
**Features:**
* Preliminary `/v1/health` API endpoint for service monitoring (no ticket)
* Add basic health check to `Dockerfile` ([#555](https://github.com/binwiederhier/ntfy/pull/555), thanks to [@bt90](https://github.com/bt90))
**Bug fixes + maintenance:**
* Fix `chown` issues with RHEL-like based systems ([#566](https://github.com/binwiederhier/ntfy/issues/566)/[#565](https://github.com/binwiederhier/ntfy/pull/565), thanks to [@danieldemus](https://github.com/danieldemus))
* Removed `upx` (binary packing) for all builds due to false virus warnings ([#576](https://github.com/binwiederhier/ntfy/issues/576), thanks to [@shawnhwei](https://github.com/shawnhwei) for reporting)
* Upgraded `go-smtp` library and tests to v0.16.0 ([#569](https://github.com/binwiederhier/ntfy/issues/569))
**Documentation:**
* Add HTTP/2 and TLSv1.3 support to nginx docs ([#553](https://github.com/binwiederhier/ntfy/issues/553), thanks to [@bt90](https://github.com/bt90))
* Small wording change for `client.yml` ([#562](https://github.com/binwiederhier/ntfy/pull/562), thanks to [@fleopaulD](https://github.com/fleopaulD))
* Fix K8s install docs ([#582](https://github.com/binwiederhier/ntfy/pull/582), thanks to [@Remedan](https://github.com/Remedan))
* Updated Jellyseer docs ([#604](https://github.com/binwiederhier/ntfy/pull/604), thanks to [@Y0ngg4n](https://github.com/Y0ngg4n))
* Updated iOS developer docs ([#605](https://github.com/binwiederhier/ntfy/pull/605), thanks to [@SticksDev](https://github.com/SticksDev))
**Additional languages:**
* Portuguese (thanks to [@ssantos](https://hosted.weblate.org/user/ssantos/))
## ntfy server v1.30.1
Released December 23, 2022 🎅
This is a special holiday edition version of ntfy, with all sorts of holiday fun and games, and hidden quests.
Nahh, just kidding. This release is an intermediate release mainly to eliminate warnings in the logs, so I can
roll out the TLSv1.3, HTTP/2 and Unix mode changes on ntfy.sh (see [#552](https://github.com/binwiederhier/ntfy/issues/552)).
**Features:**
* Web: Generate random topic name button ([#453](https://github.com/binwiederhier/ntfy/issues/453), thanks to [@yardenshoham](https://github.com/yardenshoham))
* Add [Gitpod config](https://github.com/binwiederhier/ntfy/blob/main/.gitpod.yml) ([#540](https://github.com/binwiederhier/ntfy/pull/540), thanks to [@yardenshoham](https://github.com/yardenshoham))
**Bug fixes + maintenance:**
* Remove `--env-topic` option from `ntfy publish` as per [deprecation](deprecations.md) (no ticket)
* Prepared statements for message cache writes ([#542](https://github.com/binwiederhier/ntfy/pull/542), thanks to [@nicois](https://github.com/nicois))
* Do not warn about invalid IP address when behind proxy in unix socket mode (relates to [#552](https://github.com/binwiederhier/ntfy/issues/552))
* Upgrade nginx/ntfy config on ntfy.sh to work with TLSv1.3, HTTP/2 ([#552](https://github.com/binwiederhier/ntfy/issues/552), thanks to [@bt90](https://github.com/bt90))
## ntfy Android app v1.16.0
Released December 11, 2022
This is a feature and platform/dependency upgrade release. You can now have per-subscription notification settings
(including sounds, DND, etc.), and you can make notifications continue ringing until they are dismissed. There's also
support for thematic/adaptive launcher icon for Android 13.
There are a few more Android 13 specific things, as well as many bug fixes: No more crashes from large images, no more
opening the wrong subscription, and we also fixed the icon color issue.
**Features:**
* Custom per-subscription notification settings incl. sounds, DND, etc. ([#6](https://github.com/binwiederhier/ntfy/issues/6), thanks to [@doits](https://github.com/doits))
* Insistent notifications that ring until dismissed ([#417](https://github.com/binwiederhier/ntfy/issues/417), thanks to [@danmed](https://github.com/danmed) for reporting)
* Add thematic/adaptive launcher icon ([#513](https://github.com/binwiederhier/ntfy/issues/513), thanks to [@daedric7](https://github.com/daedric7) for reporting)
**Bug fixes + maintenance:**
* Upgrade Android dependencies and build toolchain to SDK 33 (no ticket)
* Simplify F-Droid build: Disable tasks for Google Services ([#516](https://github.com/binwiederhier/ntfy/issues/516), thanks to [@markosopcic](https://github.com/markosopcic))
* Android 13: Ask for permission to post notifications ([#508](https://github.com/binwiederhier/ntfy/issues/508))
* Android 13: Do not allow swiping away the foreground notification ([#521](https://github.com/binwiederhier/ntfy/issues/521), thanks to [@alexhorner](https://github.com/alexhorner) for reporting)
* Android 5 (SDK 21): Fix crash on unsubscribing ([#528](https://github.com/binwiederhier/ntfy/issues/528), thanks to Roger M.)
* Remove timestamp when copying message text ([#471](https://github.com/binwiederhier/ntfy/issues/471), thanks to [@wunter8](https://github.com/wunter8))
* Fix auto-delete if some icons do not exist anymore ([#506](https://github.com/binwiederhier/ntfy/issues/506))
* Fix notification icon color ([#480](https://github.com/binwiederhier/ntfy/issues/480), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d) for reporting)
* Fix topics do not re-subscribe to Firebase after restoring from backup ([#511](https://github.com/binwiederhier/ntfy/issues/511))
* Fix crashes from large images ([#474](https://github.com/binwiederhier/ntfy/issues/474), thanks to [@daedric7](https://github.com/daedric7) for reporting)
* Fix notification click opens wrong subscription ([#261](https://github.com/binwiederhier/ntfy/issues/261), thanks to [@SMAW](https://github.com/SMAW) for reporting)
* Fix Firebase-only "link expired" issue ([#529](https://github.com/binwiederhier/ntfy/issues/529))
* Remove "Install .apk" feature in Google Play variant due to policy change ([#531](https://github.com/binwiederhier/ntfy/issues/531))
* Add donate button (no ticket)
**Additional translations:**
* Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
* Portuguese (thanks to [@victormagalhaess](https://hosted.weblate.org/user/victormagalhaess/))
## ntfy server v1.29.1
Released November 17, 2022
This is mostly a bugfix release to address the high load on ntfy.sh. There are now two new options that allow
synchronous batch-writing of messages to the cache. This avoids database locking, and subsequent pileups of waiting
requests.
**Bug fixes:**
* High-load servers: Allow asynchronous batch-writing of messages to cache via `cache-batch-*` options ([#498](https://github.com/binwiederhier/ntfy/issues/498)/[#502](https://github.com/binwiederhier/ntfy/pull/502))
* Sender column in cache.db shows invalid IP ([#503](https://github.com/binwiederhier/ntfy/issues/503))
**Documentation:**
* GitHub Actions example ([#492](https://github.com/binwiederhier/ntfy/pull/492), thanks to [@ksurl](https://github.com/ksurl))
* UnifiedPush ACL clarification ([#497](https://github.com/binwiederhier/ntfy/issues/497), thanks to [@bt90](https://github.com/bt90))
* Install instructions for Kustomize ([#463](https://github.com/binwiederhier/ntfy/pull/463), thanks to [@l-maciej](https://github.com/l-maciej))
**Other things:**
* Put ntfy.sh docs on GitHub pages to reduce AWS outbound traffic cost ([#491](https://github.com/binwiederhier/ntfy/issues/491))
* The ntfy.sh server hardware was upgraded to a bigger box. If you'd like to help out carrying the server cost, **[sponsorships and donations](https://github.com/sponsors/binwiederhier)** 💸 would be very much appreciated
## ntfy server v1.29.0
Released November 12, 2022
This release adds the ability to add rate limit exemptions for IP ranges instead of just specific IP addresses. It also fixes
a few bugs in the web app and the CLI and adds lots of new examples and install instructions.
Thanks to [some love on HN](https://news.ycombinator.com/item?id=33517944), we got so many new ntfy users trying out ntfy
and joining the [chat rooms](https://github.com/binwiederhier/ntfy#chat--forum). **Welcome to the ntfy community to all of you!**
We also got a ton of new **[sponsors and donations](https://github.com/sponsors/binwiederhier)** 💸, which is amazing. I'd like to thank
all of you for believing in the project, and for helping me pay the server cost. The HN spike increased the AWS cost quite a bit.
**Features:**
* Allow IP CIDRs in `visitor-request-limit-exempt-hosts` ([#423](https://github.com/binwiederhier/ntfy/issues/423), thanks to [@karmanyaahm](https://github.com/karmanyaahm))
**Bug fixes + maintenance:**
* Subscriptions can now have a display name ([#370](https://github.com/binwiederhier/ntfy/issues/370), thanks to [@tfheen](https://github.com/tfheen) for reporting)
* Bump Go version to Go 18.x ([#422](https://github.com/binwiederhier/ntfy/issues/422))
* Web: Strip trailing slash when subscribing ([#428](https://github.com/binwiederhier/ntfy/issues/428), thanks to [@raining1123](https://github.com/raining1123) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
* Web: Strip trailing slash after server URL in publish dialog ([#441](https://github.com/binwiederhier/ntfy/issues/441), thanks to [@wunter8](https://github.com/wunter8))
* Allow empty passwords in `client.yml` ([#374](https://github.com/binwiederhier/ntfy/issues/374), thanks to [@cyqsimon](https://github.com/cyqsimon) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
* `ntfy pub` will now use default username and password from `client.yml` ([#431](https://github.com/binwiederhier/ntfy/issues/431), thanks to [@wunter8](https://github.com/wunter8) for fixing)
* Make `ntfy sub` work with `NTFY_USER` env variable ([#447](https://github.com/binwiederhier/ntfy/pull/447), thanks to [SuperSandro2000](https://github.com/SuperSandro2000))
* Web: Disallow GET/HEAD requests with body in actions ([#468](https://github.com/binwiederhier/ntfy/issues/468), thanks to [@ollien](https://github.com/ollien))
**Documentation:**
* Updated developer docs, bump nodejs and go version ([#414](https://github.com/binwiederhier/ntfy/issues/414), thanks to [@YJSoft](https://github.com/YJSoft) for reporting)
* Officially document `?auth=..` query parameter ([#433](https://github.com/binwiederhier/ntfy/pull/433), thanks to [@wunter8](https://github.com/wunter8))
* Added Rundeck example ([#427](https://github.com/binwiederhier/ntfy/pull/427), thanks to [@demogorgonz](https://github.com/demogorgonz))
* Fix Debian installation instructions ([#237](https://github.com/binwiederhier/ntfy/issues/237), thanks to [@Joeharrison94](https://github.com/Joeharrison94) for reporting)
* Updated [example](https://ntfy.sh/docs/examples/#gatus) with official [Gatus](https://github.com/TwiN/gatus) integration (thanks to [@TwiN](https://github.com/TwiN))
* Added [Kubernetes install instructions](https://ntfy.sh/docs/install/#kubernetes) ([#452](https://github.com/binwiederhier/ntfy/pull/452), thanks to [@gmemstr](https://github.com/gmemstr))
* Added [additional NixOS links for self-hosting](https://ntfy.sh/docs/install/#nixos-nix) ([#462](https://github.com/binwiederhier/ntfy/pull/462), thanks to [@wamserma](https://github.com/wamserma))
* Added additional [more secure nginx config example](https://ntfy.sh/docs/config/#nginxapache2caddy) ([#451](https://github.com/binwiederhier/ntfy/pull/451), thanks to [SuperSandro2000](https://github.com/SuperSandro2000))
* Minor fixes in the config table ([#470](https://github.com/binwiederhier/ntfy/pull/470), thanks to [snh](https://github.com/snh))
* Fix broken link ([#476](https://github.com/binwiederhier/ntfy/pull/476), thanks to [@shuuji3](https://github.com/shuuji3))
**Additional translations:**
* Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
**Sponsorships:**:
Thank you to the amazing folks who decided to [sponsor ntfy](https://github.com/sponsors/binwiederhier). Thank you for
helping carry the cost of the public server and developer licenses, and more importantly: Thank you for believing in ntfy!
You guys rock!
A list of all the sponsors can be found in the [README](https://github.com/binwiederhier/ntfy/blob/main/README.md).
## ntfy Android app v1.14.0
Released September 27, 2022
This release adds the ability to set a custom icon to each notification, as well as a display name to subscriptions. We
also moved the action buttons in the detail view to a more logical place, fixed a bunch of bugs, and added four more
languages. Hurray!
**Features:**
* Subscriptions can now have a display name ([#313](https://github.com/binwiederhier/ntfy/issues/313), thanks to [@wunter8](https://github.com/wunter8))
* Display name for UnifiedPush subscriptions ([#355](https://github.com/binwiederhier/ntfy/issues/355), thanks to [@wunter8](https://github.com/wunter8))
* Polling is now done with `since=<id>` API, which makes deduping easier ([#165](https://github.com/binwiederhier/ntfy/issues/165))
* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
* Move action buttons in notification cards ([#236](https://github.com/binwiederhier/ntfy/issues/236), thanks to [@wunter8](https://github.com/wunter8))
* Icons can be set for each individual notification ([#126](https://github.com/binwiederhier/ntfy/issues/126), thanks to [@wunter8](https://github.com/wunter8))
**Bug fixes:**
* Long-click selecting of notifications doesn't scroll to the top anymore ([#235](https://github.com/binwiederhier/ntfy/issues/235), thanks to [@wunter8](https://github.com/wunter8))
* Add attachment and click URL extras to MESSAGE_RECEIVED broadcast ([#329](https://github.com/binwiederhier/ntfy/issues/329), thanks to [@wunter8](https://github.com/wunter8))
* Accessibility: Clear/choose service URL button in base URL dropdown now has a label ([#292](https://github.com/binwiederhier/ntfy/issues/292), thanks to [@mhameed](https://github.com/mhameed) for reporting)
**Additional translations:**
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
* Dutch (thanks to [@SchoNie](https://hosted.weblate.org/user/SchoNie/))
* Ukranian (thanks to [@v.kopitsa](https://hosted.weblate.org/user/v.kopitsa/))
* Polish (thanks to [@Namax0r](https://hosted.weblate.org/user/Namax0r/))
Thank you to [@wunter8](https://github.com/wunter8) for proactively picking up some Android tickets, and fixing them! You rock!
## ntfy server v1.28.0
Released September 27, 2022
This release primarily adds icon support for the Android app, and adds a display name to subscriptions in the web app.
Aside from that, we fixed a few random bugs, most importantly the `Priority` header bug that allows the use behind
Cloudflare. We also added a ton of documentation. Most prominently, an [integrations + projects page](https://ntfy.sh/docs/integrations/).
As of now, I also have started accepting **[donations and sponsorships](https://github.com/sponsors/binwiederhier)** 💸.
I would be very humbled if you consider donating.
**Features:**
* Subscription display name for the web app ([#348](https://github.com/binwiederhier/ntfy/pull/348))
* Allow setting socket permissions via `--listen-unix-mode` ([#356](https://github.com/binwiederhier/ntfy/pull/356), thanks to [@koro666](https://github.com/koro666))
* Icons can be set for each individual notification ([#126](https://github.com/binwiederhier/ntfy/issues/126), thanks to [@wunter8](https://github.com/wunter8))
* CLI: Allow default username/password in `client.yml` ([#372](https://github.com/binwiederhier/ntfy/pull/372), thanks to [@wunter8](https://github.com/wunter8))
* Build support for other Unix systems ([#393](https://github.com/binwiederhier/ntfy/pull/393), thanks to [@la-ninpre](https://github.com/la-ninpre))
**Bug fixes:**
* `ntfy user` commands don't work with `auth_file` but works with `auth-file` ([#344](https://github.com/binwiederhier/ntfy/issues/344), thanks to [@Histalek](https://github.com/Histalek) for reporting)
* Ignore new draft HTTP `Priority` header ([#351](https://github.com/binwiederhier/ntfy/issues/351), thanks to [@ksurl](https://github.com/ksurl) for reporting)
* Delete expired attachments based on mod time instead of DB entry to avoid races (no ticket)
* Better logging for Matrix push key errors ([#384](https://github.com/binwiederhier/ntfy/pull/384), thanks to [@christophehenry](https://github.com/christophehenry))
* Web: Switched "Pop" and "Pop Swoosh" sounds ([#352](https://github.com/binwiederhier/ntfy/issues/352), thanks to [@coma-toast](https://github.com/coma-toast) for reporting)
**Documentation:**
* Added [integrations + projects page](https://ntfy.sh/docs/integrations/) (**so many integrations, whoa!**)
* Added example for [UptimeRobot](https://ntfy.sh/docs/examples/#uptimerobot)
* Fix some PowerShell publish docs ([#345](https://github.com/binwiederhier/ntfy/pull/345), thanks to [@noahpeltier](https://github.com/noahpeltier))
* Clarified Docker install instructions ([#361](https://github.com/binwiederhier/ntfy/issues/361), thanks to [@barart](https://github.com/barart) for reporting)
* Mismatched quotation marks ([#392](https://github.com/binwiederhier/ntfy/pull/392)], thanks to [@connorlanigan](https://github.com/connorlanigan))
**Additional translations:**
* Ukranian (thanks to [@v.kopitsa](https://hosted.weblate.org/user/v.kopitsa/))
* Polish (thanks to [@Namax0r](https://hosted.weblate.org/user/Namax0r/))
## ntfy server v1.27.2
Released June 23, 2022
This release brings two new CLI options to wait for a command to finish, or for a PID to exit. It also adds more detail
to trace debug output. Aside from other bugs, it fixes a performance issue that occurred in large installations every
minute or so, due to competing stats gathering (personal installations will likely be unaffected by this).
**Features:**
* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#wal-for-message-cache) (no ticket)
* ntfy CLI can now [wait for a command or PID](subscribe/cli.md#wait-for-pidcommand) before publishing ([#263](https://github.com/binwiederhier/ntfy/issues/263), thanks to the [original ntfy](https://github.com/dschep/ntfy) for the idea)
* Trace: Log entire HTTP request to simplify debugging (no ticket)
* Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3))
**Bug fixes:**
* Fix slow requests due to excessive locking ([#338](https://github.com/binwiederhier/ntfy/issues/338))
* Return HTTP 500 for `GET /_matrix/push/v1/notify` when `base-url` is not configured (no ticket)
* Disallow setting `upstream-base-url` to the same value as `base-url` ([#334](https://github.com/binwiederhier/ntfy/issues/334), thanks to [@oester](https://github.com/oester) for reporting)
* Fix `since=<id>` implementation for multiple topics ([#336](https://github.com/binwiederhier/ntfy/issues/336), thanks to [@karmanyaahm](https://github.com/karmanyaahm) for reporting)
* Simple parsing in `Actions` header now supports settings Android `intent=` key ([#341](https://github.com/binwiederhier/ntfy/pull/341), thanks to [@wunter8](https://github.com/wunter8))
**Deprecations:**
* The `ntfy publish --env-topic` option is deprecated as of now (see [deprecations](deprecations.md) for details)
## ntfy server v1.26.0
Released June 16, 2022
This release adds a Matrix Push Gateway directly into ntfy, to make self-hosting a Matrix server easier. The Windows
CLI is now available via Scoop, and ntfy is now natively supported in Uptime Kuma.
**Features:**
* ntfy now is a [Matrix Push Gateway](https://spec.matrix.org/v1.2/push-gateway-api/) (in combination with [UnifiedPush](https://unifiedpush.org) as the [Provider Push Protocol](https://unifiedpush.org/developers/gateway/), [#319](https://github.com/binwiederhier/ntfy/issues/319)/[#326](https://github.com/binwiederhier/ntfy/pull/326), thanks to [@MayeulC](https://github.com/MayeulC) for reporting)
* Windows CLI is now available via [Scoop](https://scoop.sh) ([ScoopInstaller#3594](https://github.com/ScoopInstaller/Main/pull/3594), [#311](https://github.com/binwiederhier/ntfy/pull/311), [#269](https://github.com/binwiederhier/ntfy/issues/269), thanks to [@kzshantonu](https://github.com/kzshantonu))
* [Uptime Kuma](https://github.com/louislam/uptime-kuma) now allows publishing to ntfy ([uptime-kuma#1674](https://github.com/louislam/uptime-kuma/pull/1674), thanks to [@philippdormann](https://github.com/philippdormann))
* Display ntfy version in `ntfy serve` command ([#314](https://github.com/binwiederhier/ntfy/issues/314), thanks to [@poblabs](https://github.com/poblabs))
**Bug fixes:**
* Web app: Show "notifications not supported" alert on HTTP ([#323](https://github.com/binwiederhier/ntfy/issues/323), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
* Use last address in `X-Forwarded-For` header as visitor address ([#328](https://github.com/binwiederhier/ntfy/issues/328))
**Documentation**
* Added [example](examples.md) for [Uptime Kuma](https://github.com/louislam/uptime-kuma) integration ([#315](https://github.com/binwiederhier/ntfy/pull/315), thanks to [@philippdormann](https://github.com/philippdormann))
* Fix Docker install instructions ([#320](https://github.com/binwiederhier/ntfy/issues/320), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
* Add clarifying comments to base-url ([#322](https://github.com/binwiederhier/ntfy/issues/322), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
* Update FAQ for iOS app ([#321](https://github.com/binwiederhier/ntfy/issues/321), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
## ntfy iOS app v1.2
Released June 16, 2022
This release adds support for authentication/authorization for self-hosted servers. It also allows you to
set your server as the default server for new topics.
**Features:**
* Support for auth and user management ([#277](https://github.com/binwiederhier/ntfy/issues/277))
* Ability to add default server ([#295](https://github.com/binwiederhier/ntfy/issues/295))
**Bug fixes:**
* Add validation for selfhosted server URL ([#290](https://github.com/binwiederhier/ntfy/issues/290))
## ntfy server v1.25.2
Released June 2, 2022
This release adds the ability to set a log level to facilitate easier debugging of live systems. It also solves a
production problem with a few over-users that resulted in Firebase quota problems (only applying to the over-users).
We now block visitors from using Firebase if they trigger a quota exceeded response.
On top of that, we updated the Firebase SDK and are now building the release in GitHub Actions. We've also got two
more translations: Chinese/Simplified and Dutch.
**Features:**
* Advanced logging, with different log levels and hot reloading of the log level ([#284](https://github.com/binwiederhier/ntfy/pull/284))
**Bugs**:
* Respect Firebase "quota exceeded" response for topics, block Firebase publishing for user for 10min ([#289](https://github.com/binwiederhier/ntfy/issues/289))
* Fix documentation header blue header due to mkdocs-material theme update (no ticket)
**Maintenance:**
* Upgrade Firebase Admin SDK to 4.x ([#274](https://github.com/binwiederhier/ntfy/issues/274))
* CI: Build from pipeline instead of locally ([#36](https://github.com/binwiederhier/ntfy/issues/36))
**Documentation**:
* ⚠️ [Privacy policy](privacy.md) updated to reflect additional debug/tracing feature (no ticket)
* [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs))
* Install instructions for [NixOS/Nix](https://ntfy.sh/docs/install/#nixos-nix) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@arjan-s](https://github.com/arjan-s))
* Clarify `poll_request` wording for [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) ([#300](https://github.com/binwiederhier/ntfy/issues/300), thanks to [@prabirshrestha](https://github.com/prabirshrestha) for reporting)
* Example for using ntfy with docker-compose.yml without root privileges ([#304](https://github.com/binwiederhier/ntfy/pull/304), thanks to [@ksurl](https://github.com/ksurl))
**Additional translations:**
* Chinese/Simplified (thanks to [@yufei.im](https://hosted.weblate.org/user/yufei.im/))
* Dutch (thanks to [@SchoNie](https://hosted.weblate.org/user/SchoNie/))
## ntfy iOS app v1.1
Released May 31, 2022
In this release of the iOS app, we add message priorities (mapped to iOS interruption levels), tags and emojis,
action buttons to open websites or perform HTTP requests (in the notification and the detail view), a custom click
action when the notification is tapped, and various other fixes.
It also adds support for self-hosted servers (albeit not supporting auth yet). The self-hosted server needs to be
configured to forward poll requests to upstream ntfy.sh for push notifications to work (see [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications)
for details).
**Features:**
* [Message priority](https://ntfy.sh/docs/publish/#message-priority) support (no ticket)
* [Tags/emojis](https://ntfy.sh/docs/publish/#tags-emojis) support (no ticket)
* [Action buttons](https://ntfy.sh/docs/publish/#action-buttons) support (no ticket)
* [Click action](https://ntfy.sh/docs/publish/#click-action) support (no ticket)
* Open topic when notification clicked (no ticket)
* Notification now makes a sound and vibrates (no ticket)
* Cancel notifications when navigating to topic (no ticket)
* iOS 14.0 support (no ticket, [PR#1](https://github.com/binwiederhier/ntfy-ios/pull/1), thanks to [@callum-99](https://github.com/callum-99))
**Bug fixes:**
* iOS UI not always updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267))
## ntfy server v1.24.0
Released May 28, 2022
This release of the ntfy server brings supporting features for the ntfy iOS app. Most importantly, it
enables support for self-hosted servers in combination with the iOS app. This is to overcome the restrictive
Apple development environment.
**Features:**
* Regularly send Firebase keepalive messages to ~poll topic to support self-hosted servers (no ticket)
* Add subscribe filter to query exact messages by ID (no ticket)
* Support for `poll_request` messages to support [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) for self-hosted servers (no ticket)
**Bug fixes:**
* Support emails without `Content-Type` ([#265](https://github.com/binwiederhier/ntfy/issues/265), thanks to [@dmbonsall](https://github.com/dmbonsall))
**Additional translations:**
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
## ntfy iOS app v1.0
Released May 25, 2022
This is the first version of the ntfy iOS app. It supports only ntfy.sh (no selfhosted servers) and only messages + title
(no priority, tags, attachments, ...). I'll rapidly add (hopefully) most of the other ntfy features, and then I'll focus
on self-hosted servers.
The app is now available in the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
**Tickets:**
* iOS app ([#4](https://github.com/binwiederhier/ntfy/issues/4), see also: [TestFlight summary](https://github.com/binwiederhier/ntfy/issues/4#issuecomment-1133767150))
**Thanks:**
* Thank you to all the testers who tried out the app. You guys gave me the confidence that it's ready to release (albeit with
some known issues which will be addressed in follow-up releases).
## ntfy server v1.23.0
Released May 21, 2022
This release ships a CLI for Windows and macOS, as well as the ability to disable the web app entirely. On top of that,
it adds support for APNs, the iOS messaging service. This is needed for the (soon to be released) iOS app.
**Features:**
* [Windows](https://ntfy.sh/docs/install/#windows) and [macOS](https://ntfy.sh/docs/install/#macos) builds for the [ntfy CLI](https://ntfy.sh/docs/subscribe/cli/) ([#112](https://github.com/binwiederhier/ntfy/issues/112))
* Ability to disable the web app entirely ([#238](https://github.com/binwiederhier/ntfy/issues/238)/[#249](https://github.com/binwiederhier/ntfy/pull/249), thanks to [@Curid](https://github.com/Curid))
* Add APNs config to Firebase messages to support [iOS app](https://github.com/binwiederhier/ntfy/issues/4) ([#247](https://github.com/binwiederhier/ntfy/pull/247), thanks to [@Copephobia](https://github.com/Copephobia))
**Bug fixes:**
* Support underscores in server.yml config options ([#255](https://github.com/binwiederhier/ntfy/issues/255), thanks to [@ajdelgado](https://github.com/ajdelgado))
* Force MAKEFLAGS to --jobs=1 in `Makefile` ([#257](https://github.com/binwiederhier/ntfy/pull/257), thanks to [@oddlama](https://github.com/oddlama))
**Documentation:**
* Typo in install instructions ([#252](https://github.com/binwiederhier/ntfy/pull/252)/[#251](https://github.com/binwiederhier/ntfy/issues/251), thanks to [@oddlama](https://github.com/oddlama))
* Fix typo in private server example ([#262](https://github.com/binwiederhier/ntfy/pull/262), thanks to [@MayeulC](https://github.com/MayeulC))
* [Examples](examples.md) for [jellyseerr](https://github.com/Fallenbagel/jellyseerr)/[overseerr](https://overseerr.dev/) ([#264](https://github.com/binwiederhier/ntfy/pull/264), thanks to [@Fallenbagel](https://github.com/Fallenbagel))
**Additional translations:**
* Portuguese/Brazil (thanks to [@tiagotriques](https://hosted.weblate.org/user/tiagotriques/) and [@pireshenrique22](https://hosted.weblate.org/user/pireshenrique22/))
Thank you to the many translators, who helped translate the new strings so quickly. I am humbled and amazed by your help.
## ntfy Android app v1.13.0
Released May 11, 2022
This release brings a slightly altered design for the detail view, featuring a card layout to make notifications more easily
distinguishable from one another. It also ships per-topic settings that allow overriding minimum priority, auto delete threshold
and custom icons. Aside from that, we've got tons of bug fixes as usual.
**Features:**
* Per-subscription settings, custom subscription icons ([#155](https://github.com/binwiederhier/ntfy/issues/155), thanks to [@mztiq](https://github.com/mztiq) for reporting)
* Cards in notification detail view ([#175](https://github.com/binwiederhier/ntfy/issues/175), thanks to [@cmeis](https://github.com/cmeis) for reporting)
**Bug fixes:**
**Bugs:**
* Accurate naming of "mute notifications" from "pause notifications" ([#224](https://github.com/binwiederhier/ntfy/issues/224), thanks to [@shadow00](https://github.com/shadow00) for reporting)
* Make messages with links selectable ([#226](https://github.com/binwiederhier/ntfy/issues/226), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
@@ -673,53 +18,41 @@ and custom icons. Aside from that, we've got tons of bug fixes as usual.
* Fix app icon on old Android versions ([#128](https://github.com/binwiederhier/ntfy/issues/128), thanks to [@shadow00](https://github.com/shadow00) for reporting)
* Fix races in UnifiedPush registration ([#230](https://github.com/binwiederhier/ntfy/issues/230), thanks to @Jakob for reporting)
* Prevent view action from crashing the app ([#233](https://github.com/binwiederhier/ntfy/issues/233))
* Prevent long topic names and icons from overlapping ([#240](https://github.com/binwiederhier/ntfy/issues/240), thanks to [@cmeis](https://github.com/cmeis) for reporting)
**Additional translations:**
**Thanks for testing:**
* Dutch (*incomplete*, thanks to [@diony](https://hosted.weblate.org/user/diony/))
Thanks to [@cmeis](https://github.com/cmeis), [@StoyanDimitrov](https://github.com/StoyanDimitrov), [@Fallenbagel](https://github.com/Fallenbagel) for testing, and
to [@Joeharrison94](https://github.com/Joeharrison94) for the input.
**Thank you:**
Thanks to [@cmeis](https://github.com/cmeis), [@StoyanDimitrov](https://github.com/StoyanDimitrov), [@Fallenbagel](https://github.com/Fallenbagel) for testing, and
to [@Joeharrison94](https://github.com/Joeharrison94) for the input. And thank you very much to all the translators for catching up so quickly.
## ntfy server v1.22.0
Released May 7, 2022
This release makes the web app more accessible to people with disabilities, and introduces a "mark as read" icon in the web app.
It also fixes a curious bug with WebSockets and Apache and makes the notification sounds in the web app a little quieter.
We've also improved the documentation a little and added translations for three more languages.
## ntfy server v1.22.0 (UNRELEASED)
**Features:**
* Make web app more accessible ([#217](https://github.com/binwiederhier/ntfy/issues/217))
* Better parsing of the user actions, allowing quotes (no ticket)
* Add "mark as read" icon button to notification ([#243](https://github.com/binwiederhier/ntfy/pull/243), thanks to [@wunter8](https://github.com/wunter8))
* Make web app more accessible ([#217](https://github.com/binwiederhier/ntfy/issues/217))
**Bug fixes:**
**Bugs:**
* `Upgrade` header check is now case in-sensitive ([#228](https://github.com/binwiederhier/ntfy/issues/228), thanks to [@wunter8](https://github.com/wunter8) for finding it)
* Made web app sounds quieter ([#222](https://github.com/binwiederhier/ntfy/issues/222))
* Add "private browsing"-specific error message for Firefox/Safari ([#208](https://github.com/binwiederhier/ntfy/issues/208), thanks to [@julianfoad](https://github.com/julianfoad) for reporting)
* Add "private browsing"-specific error message for Firefox/Safari ([#208](https://github.com/binwiederhier/ntfy/issues/208), thanks to [@julianfoad](https://github.com/julianfoad) for reporting)
**Documentation:**
* Improved caddy configuration (no ticket, thanks to @Stnby)
* Additional multi-line examples on the [publish page](https://ntfy.sh/docs/publish/) ([#234](https://github.com/binwiederhier/ntfy/pull/234), thanks to [@aTable](https://github.com/aTable))
* Fixed PowerShell auth example to use UTF-8 ([#242](https://github.com/binwiederhier/ntfy/pull/242), thanks to [@SMAW](https://github.com/SMAW))
**Additional translations:**
* Czech (thanks to [@waclaw66](https://hosted.weblate.org/user/waclaw66/))
* French (thanks to [@nathanaelhoun](https://hosted.weblate.org/user/nathanaelhoun/))
* Hungarian (thanks to [@agocsdaniel](https://hosted.weblate.org/user/agocsdaniel/))
**Thanks for testing:**
Thanks to [@wunter8](https://github.com/wunter8) for testing.
-->
## ntfy Android app v1.12.0
Released Apr 25, 2022
@@ -740,7 +73,7 @@ languages and fixed a ton of bugs.
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
* Channel settings option to configure DND override, sounds, etc. ([#91](https://github.com/binwiederhier/ntfy/issues/91))
**Bug fixes:**
**Bugs:**
* Validate URLs when changing default server and server in user management ([#193](https://github.com/binwiederhier/ntfy/issues/193),
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
@@ -781,7 +114,7 @@ Limited support is available in the web app.
* Added ARMv6 build ([#200](https://github.com/binwiederhier/ntfy/issues/200), thanks to [@jcrubioa](https://github.com/jcrubioa) for reporting)
* Web app internationalization support 🇧🇬 🇩🇪 🇺🇸 🌎 ([#189](https://github.com/binwiederhier/ntfy/issues/189))
**Bug fixes:**
**Bugs:**
* Web app: English language strings fixes, additional descriptions for settings ([#203](https://github.com/binwiederhier/ntfy/issues/203), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* Web app: Show error message snackbar when sending test notification fails ([#205](https://github.com/binwiederhier/ntfy/issues/205), thanks to [@cmeis](https://github.com/cmeis))
@@ -821,7 +154,7 @@ Released Apr 7, 2022
* Translations to different languages ([#188](https://github.com/binwiederhier/ntfy/issues/188), thanks to
[@StoyanDimitrov](https://github.com/StoyanDimitrov) for initiating things)
**Bug fixes:**
**Bugs:**
* IllegalStateException: Failed to build unique file ([#177](https://github.com/binwiederhier/ntfy/issues/177), thanks to [@Fallenbagel](https://github.com/Fallenbagel) for reporting)
* SQLiteConstraintException: Crash during UP registration ([#185](https://github.com/binwiederhier/ntfy/issues/185))
@@ -855,7 +188,7 @@ Released Apr 6, 2022
* Added message bar and publish dialog ([#196](https://github.com/binwiederhier/ntfy/issues/196))
**Bug fixes:**
**Bugs:**
* Added `EXPOSE 80/tcp` to Dockerfile to support auto-discovery in [Traefik](https://traefik.io/) ([#195](https://github.com/binwiederhier/ntfy/issues/195), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d))
@@ -871,7 +204,7 @@ Released Apr 6, 2022
## ntfy server v1.19.0
Released Mar 30, 2022
**Bug fixes:**
**Bugs:**
* Do not pack binary with `upx` for armv7/arm64 due to `illegal instruction` errors ([#191](https://github.com/binwiederhier/ntfy/issues/191), thanks to [@iexos](https://github.com/iexos))
* Do not allow comma in topic name in publish via GET endpoint (no ticket)
@@ -1146,20 +479,3 @@ Released Dec 28, 2021
## Older releases
For older releases, check out the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
## Not released yet
### ntfy Android app v1.16.1 (UNRELEASED)
**Features:**
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
**Bug fixes + maintenance:**
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
**Additional languages:**
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))

View File

@@ -1,18 +1,15 @@
:root > * {
:root {
--md-primary-fg-color: #338574;
--md-primary-fg-color--light: #338574;
--md-primary-fg-color--dark: #338574;
--md-footer-bg-color: #353744;
--md-text-font: "Roboto";
--md-code-font: "Roboto Mono";
}
.md-header__button.md-logo :is(img, svg) {
width: unset !important;
}
.md-header__topic:first-child {
font-weight: 400;
.md-sidebar {
width: 12.5rem !important;
}
.md-typeset h4 {
@@ -33,30 +30,12 @@ figure img, figure video {
border-radius: 7px;
}
header {
background: linear-gradient(150deg, rgba(51,133,116,1) 0%, rgba(86,189,168,1) 100%);
}
body[data-md-color-scheme="default"] header {
filter: drop-shadow(0 5px 10px #ccc);
}
body[data-md-color-scheme="slate"] header {
filter: drop-shadow(0 5px 10px #333);
}
body[data-md-color-scheme="default"] figure img,
body[data-md-color-scheme="default"] figure video,
body[data-md-color-scheme="default"] .screenshots img,
body[data-md-color-scheme="default"] .screenshots video {
body[data-md-color-scheme="default"] figure img, body[data-md-color-scheme="default"] figure video {
filter: drop-shadow(3px 3px 3px #ccc);
}
body[data-md-color-scheme="slate"] figure img,
body[data-md-color-scheme="slate"] figure video,
body[data-md-color-scheme="slate"] .screenshots img,
body[data-md-color-scheme="slate"] .screenshots video {
filter: drop-shadow(3px 3px 3px #353744);
body[data-md-color-scheme="slate"] figure img, body[data-md-color-scheme="slate"] figure video {
filter: drop-shadow(3px 3px 3px #1a1313);
}
figure video {
@@ -81,8 +60,7 @@ figure video {
}
.screenshots img {
max-height: 230px;
max-width: 300px;
height: 230px;
margin: 3px;
border-radius: 5px;
filter: drop-shadow(2px 2px 2px #ddd);
@@ -149,57 +127,3 @@ figure video {
.lightbox .close-lightbox:hover::before {
background-color: #fff;
}
/* roboto-300 - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url('../fonts/roboto-v30-latin-300.woff2') format('woff2');
}
/* roboto-regular - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url('../fonts/roboto-v30-latin-regular.woff2') format('woff2');
}
/* roboto-italic - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
src: url('../fonts/roboto-v30-latin-italic.woff2') format('woff2');
}
/* roboto-500 - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url('../fonts/roboto-v30-latin-500.woff2') format('woff2');
}
/* roboto-700 - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url('../fonts/roboto-v30-latin-700.woff2') format('woff2');
}
/* roboto-mono - latin */
@font-face {
font-display: swap;
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url('../fonts/roboto-mono-v22-latin-regular.woff2') format('woff2');
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,8 +1,8 @@
// Link tabs, as per https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs
const savedCodeTab = localStorage.getItem('savedTab')
const codeTabs = document.querySelectorAll(".tabbed-set > input")
for (const tab of codeTabs) {
const savedTab = localStorage.getItem('savedTab')
const tabs = document.querySelectorAll(".tabbed-set > input")
for (const tab of tabs) {
tab.addEventListener("click", () => {
const current = document.querySelector(`label[for=${tab.id}]`)
const pos = current.getBoundingClientRect().top
@@ -25,7 +25,7 @@ for (const tab of codeTabs) {
// Select saved tab
const current = document.querySelector(`label[for=${tab.id}]`)
const labelContent = current.innerHTML
if (savedCodeTab === labelContent) {
if (savedTab === labelContent) {
tab.checked = true
}
}

View File

@@ -87,7 +87,7 @@ recommended way to subscribe to a topic**. The notable exception is JavaScript,
### Subscribe as SSE stream
Using [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) in JavaScript, you can consume
notifications via a [Server-Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events) stream. It's incredibly
easy to use. Here's what it looks like. You may also want to check out the [full example on GitHub](https://github.com/binwiederhier/ntfy/tree/main/examples/web-example-eventsource).
easy to use. Here's what it looks like. You may also want to check out the [live example](/example.html).
=== "Command line (curl)"
```
@@ -267,7 +267,7 @@ curl -s "ntfy.sh/mytopic/json?poll=1&sched=1"
```
### Filter messages
You can filter which messages are returned based on the well-known message fields `id`, `message`, `title`, `priority` and
You can filter which messages are returned based on the well-known message fields `message`, `title`, `priority` and
`tags`. Here's an example that only returns messages of high or urgent priority that contains the both tags
"zfs-error" and "error". Note that the `priority` filter is a logical OR and the `tags` filter is a logical AND.
@@ -280,13 +280,12 @@ $ curl "ntfy.sh/alerts/json?priority=high&tags=zfs-error"
Available filters (all case-insensitive):
| Filter variable | Alias | Example | Description |
|-----------------|---------------------------|-----------------------------------------------|-------------------------------------------------------------------------|
| `id` | `X-ID` | `ntfy.sh/mytopic/json?poll=1&id=pbkiz8SD7ZxG` | Only return messages that match this exact message ID |
| `message` | `X-Message`, `m` | `ntfy.sh/mytopic/json?message=lalala` | Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | `ntfy.sh/mytopic/json?title=some+title` | Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic/json?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) |
| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?/jsontags=error,alert` | Only return messages that match *all listed tags* (comma-separated) |
| Filter variable | Alias | Example | Description |
|-----------------|---------------------------|------------------------------------|-------------------------------------------------------------------------|
| `message` | `X-Message`, `m` | `ntfy.sh/mytopic?message=lalala` | Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | `ntfy.sh/mytopic?title=some+title` | Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) |
| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated) |
### Subscribe to multiple topics
It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics
@@ -302,12 +301,13 @@ $ curl -s ntfy.sh/mytopic1,mytopic2/json
### Authentication
Depending on whether the server is configured to support [access control](../config.md#access-control), some topics
may be read/write protected so that only users with the correct credentials can subscribe or publish to them.
To publish/subscribe to protected topics, you can:
To publish/subscribe to protected topics, you can use [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
with a valid username/password. For your self-hosted server, **be sure to use HTTPS to avoid eavesdropping** and exposing
your password.
* Use [basic auth](../publish.md#basic-auth), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk`
* or use the [`auth` query parameter](../publish.md#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw`
Please refer to the [publishing documentation](../publish.md#authentication) for additional details.
```
curl -u phil:mypass -s "https://ntfy.example.com/mytopic/json"
```
## JSON message format
Both the [`/json` endpoint](#subscribe-as-json-stream) and the [`/sse` endpoint](#subscribe-as-sse-stream) return a JSON
@@ -315,20 +315,18 @@ format of the message. It's very straight forward:
**Message**:
| Field | Required | Type | Example | Description |
|--------------|----------|---------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
| `expires` | (✔) | *number* | `1673542291` | Unix time stamp indicating when the message will be deleted, not set if `Cache: no` is sent |
| `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
| `message` | - | *string* | `Some message` | Message body; always present in `message` events |
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
| `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification |
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
| Field | Required | Type | Example | Description |
|--------------|----------|---------------------------------------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
| `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
| `message` | - | *string* | `Some message` | Message body; always present in `message` events |
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
**Attachment** (part of the message, see [attachments](../publish.md#attachments) for details):
@@ -347,7 +345,6 @@ Here's an example for each message type:
{
"id": "sPs71M8A2T",
"time": 1643935928,
"expires": 1643936928,
"event": "message",
"topic": "mytopic",
"priority": 5,
@@ -374,7 +371,6 @@ Here's an example for each message type:
{
"id": "wze9zgqK41",
"time": 1638542110,
"expires": 1638543112,
"event": "message",
"topic": "phil_alerts",
"message": "Remote access to phils-laptop detected. Act right away."
@@ -420,7 +416,6 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
| `poll` | `X-Poll`, `po` | Return cached messages and close connection |
| `since` | `X-Since`, `si` | Return cached messages since timestamp, duration or message ID |
| `scheduled` | `X-Scheduled`, `sched` | Include scheduled/delayed messages in message list |
| `id` | `X-ID` | Filter: Only return messages that match this exact message ID |
| `message` | `X-Message`, `m` | Filter: Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | Filter: Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | Filter: Only return messages that match *any priority listed* (comma-separated) |

View File

@@ -56,71 +56,6 @@ quick ones:
ntfy pub mywebhook
```
### Attaching a local file
You can easily upload and attach a local file to a notification:
```
$ ntfy pub --file README.md mytopic | jq .
{
"id": "meIlClVLABJQ",
"time": 1655825460,
"event": "message",
"topic": "mytopic",
"message": "You received a file: README.md",
"attachment": {
"name": "README.md",
"type": "text/plain; charset=utf-8",
"size": 2892,
"expires": 1655836260,
"url": "https://ntfy.sh/file/meIlClVLABJQ.txt"
}
}
```
### Wait for PID/command
If you have a long-running command and want to **publish a notification when the command completes**,
you may wrap it with `ntfy publish --wait-cmd` (aliases: `--cmd`, `--done`). Or, if you forgot to wrap it, and the
command is already running, you can wait for the process to complete with `ntfy publish --wait-pid` (alias: `--pid`).
Run a command and wait for it to complete (here: `rsync ...`):
```
$ ntfy pub --wait-cmd mytopic rsync -av ./ root@example.com:/backups/ | jq .
{
"id": "Re0rWXZQM8WB",
"time": 1655825624,
"event": "message",
"topic": "mytopic",
"message": "Command succeeded after 56.553s: rsync -av ./ root@example.com:/backups/"
}
```
Or, if you already started the long-running process and want to wait for it using its process ID (PID), you can do this:
=== "Using a PID directly"
```
$ ntfy pub --wait-pid 8458 mytopic | jq .
{
"id": "orM6hJKNYkWb",
"time": 1655825827,
"event": "message",
"topic": "mytopic",
"message": "Process with PID 8458 exited after 2.003s"
}
```
=== "Using a `pidof`"
```
$ ntfy pub --wait-pid $(pidof rsync) mytopic | jq .
{
"id": "orM6hJKNYkWb",
"time": 1655825827,
"event": "message",
"topic": "mytopic",
"message": "Process with PID 8458 exited after 2.003s"
}
```
## Subscribe to topics
You can subscribe to topics using `ntfy subscribe`. Depending on how it is called, this command
will either print or execute a command for every arriving message. There are a few different ways
@@ -188,7 +123,7 @@ which will read the `subscribe` config from the config file. Please also check o
Here's an example config file that subscribes to three different topics, executing a different command for each of them:
=== "~/.config/ntfy/client.yml (Linux)"
=== "~/.config/ntfy/client.yml"
```yaml
subscribe:
- topic: echo-this
@@ -210,42 +145,12 @@ Here's an example config file that subscribes to three different topics, executi
fi
```
=== "~/Library/Application Support/ntfy/client.yml (macOS)"
```yaml
subscribe:
- topic: echo-this
command: 'echo "Message received: $message"'
- topic: alerts
command: osascript -e "display notification \"$message\""
if:
priority: high,urgent
- topic: calc
command: open -a Calculator
```
=== "%AppData%\ntfy\client.yml (Windows)"
```yaml
subscribe:
- topic: echo-this
command: 'echo Message received: %message%'
- topic: alerts
command: |
notifu /m "%NTFY_MESSAGE%"
exit 0
if:
priority: high,urgent
- topic: calc
command: calc
```
In this example, when `ntfy subscribe --from-config` is executed:
* Messages to `echo-this` simply echos to standard out
* Messages to `alerts` display as desktop notification for high priority messages using [notify-send](https://manpages.ubuntu.com/manpages/focal/man1/notify-send.1.html) (Linux),
[notifu](https://www.paralint.com/projects/notifu/) (Windows) or `osascript` (macOS)
* Messages to `calc` open the calculator 😀 (*because, why not*)
* Messages to `print-temp` execute an inline script and print the CPU temperature (Linux version only)
* Messages to `alerts` display as desktop notification for high priority messages using [notify-send](https://manpages.ubuntu.com/manpages/focal/man1/notify-send.1.html)
* Messages to `calc` open the gnome calculator 😀 (*because, why not*)
* Messages to `print-temp` execute an inline script and print the CPU temperature
I hope this shows how powerful this command is. Here's a short video that demonstrates the above example:
@@ -254,14 +159,6 @@ I hope this shows how powerful this command is. Here's a short video that demons
<figcaption>Execute all the things</figcaption>
</figure>
If most (or all) of your subscriptions use the same credentials, you can set defaults in `client.yml`. Use `default-user` and `default-password` or `default-token` (but not both).
You can also specify a `default-command` that will run when a message is received. If a subscription does not include credentials to use or does not have a command, the defaults
will be used, otherwise, the subscription settings will override the defaults.
!!! warning
Because the `default-user`, `default-password`, and `default-token` will be sent for each topic that does not have its own username/password (even if the topic does not
require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
### Using the systemd service
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
to subscribe to multiple topics just like in the example above. The service is automatically installed (but not started)

View File

@@ -1,16 +1,14 @@
# Subscribe from your phone
You can use the ntfy [Android App](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [iOS app](https://apps.apple.com/us/app/ntfy/id1625396347)
to receive notifications directly on your phone. Just like the server, this app is also open source, and the code is available
on GitHub ([Android](https://github.com/binwiederhier/ntfy-android), [iOS](https://github.com/binwiederhier/ntfy-ios)). Feel free to
contribute, or [build your own](../develop.md).
You can use the [ntfy Android App](https://play.google.com/store/apps/details?id=io.heckel.ntfy) to receive
notifications directly on your phone. Just like the server, this app is also [open source](https://github.com/binwiederhier/ntfy-android).
Since I don't have an iPhone or a Mac, I didn't make an iOS app yet. I'd be awesome if [someone else could help out](https://github.com/binwiederhier/ntfy/issues/4).
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="../../static/img/badge-appstore.png"></a>
You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that
the F-Droid flavor does not use Firebase. The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
the F-Droid flavor does not use Firebase.
## Overview
A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
@@ -33,8 +31,6 @@ If those screenshots are still not enough, here's a video:
</figure>
## Message priority
_Supported on:_ :material-android: :material-apple:
When you [publish messages](../publish.md#message-priority) to a topic, you can **define a priority**. This priority defines
how urgently Android will notify you about the notification, and whether they make a sound and/or vibrate.
@@ -63,8 +59,6 @@ setting, and other settings such as popover or notification dot:
</figure>
## Instant delivery
_Supported on:_ :material-android:
Instant delivery allows you to receive messages on your phone instantly, **even when your phone is in doze mode**, i.e.
when the screen turns off, and you leave it on the desk for a while. This is achieved with a foreground service, which
you'll see as a permanent notification that looks like this:
@@ -95,8 +89,6 @@ The ntfy Android app uses Firebase only for the main host `ntfy.sh`, and only in
It won't use Firebase for any self-hosted servers, and not at all in the the F-Droid flavor.
## Share to topic
_Supported on:_ :material-android:
You can share files to a topic using Android's "Share" feature. This works in almost any app that supports sharing files
or text, and it's useful for sending yourself links, files or other things. The feature remembers a few of the last topics
you shared content to and lists them at the bottom.
@@ -109,8 +101,6 @@ The feature is pretty self-explanatory, and one picture says more than a thousan
</div>
## ntfy:// links
_Supported on:_ :material-android:
The ntfy Android app supports deep linking directly to topics. This is useful when integrating with [automation apps](#automation-apps)
such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm),
or to simply directly link to a topic from a mobile website.
@@ -129,8 +119,6 @@ or to simply directly link to a topic from a mobile website.
## Integrations
### UnifiedPush
_Supported on:_ :material-android:
[UnifiedPush](https://unifiedpush.org) is a standard for receiving push notifications without using the Google-owned
[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) service. It puts push notifications
in the control of the user. ntfy can act as a **UnifiedPush distributor**, forwarding messages to apps that support it.
@@ -146,8 +134,6 @@ to handle messages. Here's an example with [FluffyChat](https://fluffychat.im/):
</div>
### Automation apps
_Supported on:_ :material-android:
The ntfy Android app integrates nicely with automation apps such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm). Using Android intents, you can
**react to incoming messages**, as well as **send messages**.
@@ -180,27 +166,21 @@ notification popups:
Here's a list of extras you can access. Most likely, you'll want to filter for `topic` and react on `message`:
| Extra name | Type | Example | Description |
|----------------------|------------------------------|------------------------------------------|------------------------------------------------------------------------------------|
| `id` | *String* | `bP8dMjO8ig` | Randomly chosen message identifier (likely not very useful for task automation) |
| `base_url` | *String* | `https://ntfy.sh` | Root URL of the ntfy server this message came from |
| `topic` ❤️ | *String* | `mytopic` | Topic name; **you'll likely want to filter for a specific topic** |
| `muted` | *Boolean* | `true` | Indicates whether the subscription was muted in the app |
| `muted_str` | *String (`true` or `false`)* | `true` | Same as `muted`, but as string `true` or `false` |
| `time` | *Int* | `1635528741` | Message date time, as Unix time stamp |
| `title` | *String* | `Some title` | Message [title](../publish.md#message-title); may be empty if not set |
| `message` ❤️ | *String* | `Some message` | Message body; **this is likely what you're interested in** |
| `message_bytes` | *ByteArray* | `(binary data)` | Message body as binary data |
| `encoding` | *String* | - | Message encoding (empty or "base64") |
| `tags` | *String* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) |
| `tags_map` | *String* | `0=tag1,1=tag2,..` | Map of tags to make it easier to map first, second, ... tag |
| `priority` | *Int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
| `click` | *String* | `https://google.com` | [Click action](../publish.md#click-action) URL, or empty if not set |
| `attachment_name` | *String* | `attachment.jpg` | Filename of the attachment; may be empty if not set |
| `attachment_type` | *String* | `image/jpeg` | Mime type of the attachment; may be empty if not set |
| `attachment_size` | *Long* | `9923111` | Size in bytes of the attachment; may be zero if not set |
| `attachment_expires` | *Long* | `1655514244` | Expiry date as Unix timestamp of the attachment URL; may be zero if not set |
| `attachment_url` | *String* | `https://ntfy.sh/file/afUbjadfl7ErP.jpg` | URL of the attachment; may be empty if not set |
| Extra name | Type | Example | Description |
|-----------------|------------------------------|--------------------|------------------------------------------------------------------------------------|
| `id` | *String* | `bP8dMjO8ig` | Randomly chosen message identifier (likely not very useful for task automation) |
| `base_url` | *String* | `https://ntfy.sh` | Root URL of the ntfy server this message came from |
| `topic` ❤️ | *String* | `mytopic` | Topic name; **you'll likely want to filter for a specific topic** |
| `muted` | *Boolean* | `true` | Indicates whether the subscription was muted in the app |
| `muted_str` | *String (`true` or `false`)* | `true` | Same as `muted`, but as string `true` or `false` |
| `time` | *Int* | `1635528741` | Message date time, as Unix time stamp |
| `title` | *String* | `Some title` | Message [title](../publish.md#message-title); may be empty if not set |
| `message` ❤️ | *String* | `Some message` | Message body; **this is likely what you're interested in** |
| `message_bytes` | *ByteArray* | `(binary data)` | Message body as binary data |
| `encoding` | *String* | - | Message encoding (empty or "base64") |
| `tags` | *String* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) |
| `tags_map` | *String* | `0=tag1,1=tag2,..` | Map of tags to make it easier to map first, second, ... tag |
| `priority` | *Int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
#### Send messages using intents
To send messages from other apps (such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
@@ -230,3 +210,9 @@ The following intent extras are supported when for the intent with the `io.hecke
| `message` ❤️ | ✔ | *String* | `Some message` | Message body; **you must set this** |
| `tags` | - | *String* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) |
| `priority` | - | *String or Int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
## iPhone/iOS
I almost feel devious for putting the *Download on the App Store* button on this page. Currently, there is no iOS app
for ntfy, but it's in the works. You can track the status on GitHub.
<a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="../../static/img/badge-appstore.png"></a>

View File

@@ -18,10 +18,3 @@ is to pin the tab so that it's always open, but sort of out of the way:
![pinned](../static/img/web-pin.png){ width=500 }
<figcaption>Pin web app to move it out of the way</figcaption>
</figure>
If topic reservations are enabled, you can claim ownership over topics and define access to it:
<div id="reserve-screenshots" class="screenshots">
<a href="../../static/img/web-reserve-topic.png"><img src="../../static/img/web-reserve-topic.png"/></a>
<a href="../../static/img/web-reserve-topic-dialog.png"><img src="../../static/img/web-reserve-topic-dialog.png"/></a>
</div>

View File

@@ -1,131 +0,0 @@
# Troubleshooting
This page lists a few suggestions of what to do when things don't work as expected. This is not a complete list.
If this page does not help, feel free to drop by the [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)
and ask there. We're happy to help.
## ntfy server
If you host your own ntfy server, and you're having issues with any component, it is always helpful to enable debugging/tracing
in the server. You can find detailed instructions in the [Logging & Debugging](config.md#logging-debugging) section, but it ultimately
boils down to setting `log-level: debug` or `log-level: trace` in the `server.yml` file:
=== "server.yml (debug)"
``` yaml
log-level: debug
```
=== "server.yml (trace)"
``` yaml
log-level: trace
```
If you're using environment variables, set `NTFY_LOG_LEVEL=debug` (or `trace`) instead. You can also pass `--debug` or `--trace`
to the `ntfy serve` command, e.g. `ntfy serve --trace`. If you're using systemd (i.e. `systemctl`) to run ntfy, you can look at
the logs using `journalctl -u ntfy -f`. The logs will look something like this:
=== "Example logs (debug)"
```
$ ntfy serve --debug
2023/03/20 14:45:38 INFO Listening on :2586[http] :1025[smtp], ntfy 2.1.2, log level is DEBUG (tag=startup)
2023/03/20 14:45:38 DEBUG Waiting until 2023-03-21 00:00:00 +0000 UTC to reset visitor stats (tag=resetter)
2023/03/20 14:45:39 DEBUG Rate limiters reset for visitor (visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:45:39.7-04:00)
2023/03/20 14:45:39 DEBUG HTTP request started (http_method=POST, http_path=/mytopic, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:45:39.7-04:00)
2023/03/20 14:45:39 DEBUG Received message (http_method=POST, http_path=/mytopic, message_body_size=2, message_delayed=false, message_email=, message_event=message, message_firebase=true, message_id=EZu6i2WZjH0v, message_sender=127.0.0.1, message_time=1679337939, message_unifiedpush=false, tag=publish, topic=mytopic, topic_last_access=2023-03-20T14:45:38.319-04:00, topic_subscribers=0, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002132248, visitor_seen=2023-03-20T14:45:39.7-04:00)
2023/03/20 14:45:39 DEBUG Adding message to cache (http_method=POST, http_path=/mytopic, message_body_size=2, message_event=message, message_id=EZu6i2WZjH0v, message_sender=127.0.0.1, message_time=1679337939, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.000259165, visitor_seen=2023-03-20T14:45:39.7-04:00)
2023/03/20 14:45:39 DEBUG HTTP request finished (http_method=POST, http_path=/mytopic, tag=http, time_taken_ms=2, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0004147334, visitor_seen=2023-03-20T14:45:39.7-04:00)
2023/03/20 14:45:39 DEBUG Wrote 1 message(s) in 8.285712ms (tag=message_cache)
...
```
=== "Example logs (trace)"
```
$ ntfy serve --trace
2023/03/20 14:40:42 INFO Listening on :2586[http] :1025[smtp], ntfy 2.1.2, log level is TRACE (tag=startup)
2023/03/20 14:40:42 DEBUG Waiting until 2023-03-21 00:00:00 +0000 UTC to reset visitor stats (tag=resetter)
2023/03/20 14:40:59 DEBUG Rate limiters reset for visitor (visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:40:59.893-04:00)
2023/03/20 14:40:59 TRACE HTTP request started (http_method=POST, http_path=/mytopic, http_request=POST /mytopic HTTP/1.1
User-Agent: curl/7.81.0
Accept: */*
Content-Length: 2
Content-Type: application/x-www-form-urlencoded
hi, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:40:59.893-04:00)
2023/03/20 14:40:59 TRACE Received message (http_method=POST, http_path=/mytopic, message_body={
"id": "Khaup1RVclU3",
"time": 1679337659,
"expires": 1679380859,
"event": "message",
"topic": "mytopic",
"message": "hi"
}, message_body_size=2, message_delayed=false, message_email=, message_event=message, message_firebase=true, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, message_unifiedpush=false, tag=publish, topic=mytopic, topic_last_access=2023-03-20T14:40:59.893-04:00, topic_subscribers=0, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0001785048, visitor_seen=2023-03-20T14:40:59.893-04:00)
2023/03/20 14:40:59 DEBUG Adding message to cache (http_method=POST, http_path=/mytopic, message_body_size=2, message_event=message, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002044368, visitor_seen=2023-03-20T14:40:59.893-04:00)
2023/03/20 14:40:59 DEBUG HTTP request finished (http_method=POST, http_path=/mytopic, tag=http, time_taken_ms=1, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.000220502, visitor_seen=2023-03-20T14:40:59.893-04:00)
2023/03/20 14:40:59 TRACE No stream or WebSocket subscribers, not forwarding (message_body_size=2, message_event=message, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002369212, visitor_seen=2023-03-20T14:40:59.893-04:00)
2023/03/20 14:41:00 DEBUG Wrote 1 message(s) in 9.529196ms (tag=message_cache)
...
```
## Android app
On Android, you can turn on logging in the settings under **Settings → Record logs**. This will store up to 1,000 log
entries, which you can then copy or upload.
<figure markdown>
![Recording logs on Android](static/img/android-screenshot-logs.jpg){ width=400 }
<figcaption>Recording logs on Android</figcaption>
</figure>
When you copy or upload the logs, you can censor them to make it easier to share them with others. ntfy will replace all
topics and hostnames with fruits. Here's an example:
```
This is a log of the ntfy Android app. The log shows up to 1,000 entries.
Server URLs (aside from ntfy.sh) and topics have been replaced with fruits 🍌🥝🍋🥥🥑🍊🍎🍑.
Device info:
--
ntfy: 1.16.0 (play)
OS: 4.19.157-perf+
Android: 13 (SDK 33)
...
Logs
--
1679339199507 2023-03-20 15:06:39.507 D NtfyMainActivity Battery: ignoring optimizations = true (we want this to be true); instant subscriptions = true; remind time reached = true; banner = false
1679339199507 2023-03-20 15:06:39.507 D NtfySubscriberMgr Enqueuing work to refresh subscriber service
1679339199589 2023-03-20 15:06:39.589 D NtfySubscriberMgr ServiceStartWorker: Starting foreground service with action START (work ID: a7eeeae9-9356-40df-afbd-236e5ed10a0b)
1679339199602 2023-03-20 15:06:39.602 D NtfySubscriberService onStartCommand executed with startId: 262
1679339199602 2023-03-20 15:06:39.602 D NtfySubscriberService using an intent with action START
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService Refreshing subscriptions
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService - Desired connections: [ConnectionId(baseUrl=https://ntfy.sh, topicsToSubscriptionIds={avocado=23801492, lemon=49013182, banana=1309176509201171073, peach=573300885184666424, pineapple=-5956897229801209316, durian=81453333, starfruit=30489279, fruit12=82532869}), ConnectionId(baseUrl=https://orange.example.com, topicsToSubscriptionIds={apple=4971265, dragonfruit=66809328})]
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService - Active connections: [ConnectionId(baseUrl=https://orange.example.com, topicsToSubscriptionIds={apple=4971265, dragonfruit=66809328}), ConnectionId(baseUrl=https://ntfy.sh, topicsToSubscriptionIds={avocado=23801492, lemon=49013182, banana=1309176509201171073, peach=573300885184666424, pineapple=-5956897229801209316, durian=81453333, starfruit=30489279, fruit12=82532869})]
...
```
To get live logs, or to get more advanced access to an Android phone, you can use [adb](https://developer.android.com/studio/command-line/adb).
After you install and [enable adb debugging](https://developer.android.com/studio/command-line/adb#Enabling), you can
get detailed logs like so:
```
# Connect to phone (enable Wireless debugging first)
adb connect 192.168.1.137:39539
# Print all logs; you may have to pass the -s option
adb logcat
adb -s 192.168.1.137:39539 logcat
# Only list ntfy logs
adb logcat --pid=$(adb shell pidof -s io.heckel.ntfy)
adb -s 192.168.1.137:39539 logcat --pid=$(adb -s 192.168.1.137:39539 shell pidof -s io.heckel.ntfy)
```
## Web app
The web app logs everything to the **developer console**, which you can open by **pressing the F12 key** on your
keyboard.
<figure markdown>
![Web app logs](static/img/web-logs.png)
<figcaption>Web app logs in the developer console</figcaption>
</figure>
## iOS app
Sorry, there is no way to debug or get the logs from the iOS app (yet), outside of running the app in Xcode.

File diff suppressed because it is too large Load Diff

90
go.mod
View File

@@ -1,73 +1,53 @@
module heckel.io/ntfy
go 1.18
go 1.17
require (
cloud.google.com/go/firestore v1.9.0 // indirect
cloud.google.com/go/storage v1.30.1 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/emersion/go-smtp v0.16.0
github.com/gabriel-vasile/mimetype v1.4.2
cloud.google.com/go/firestore v1.6.1 // indirect
cloud.google.com/go/storage v1.22.0 // indirect
firebase.google.com/go v3.13.0+incompatible
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/emersion/go-smtp v0.15.0
github.com/gabriel-vasile/mimetype v1.4.0
github.com/gorilla/websocket v1.5.0
github.com/mattn/go-sqlite3 v1.14.16
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.25.1
golang.org/x/crypto v0.7.0
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/sync v0.1.0
golang.org/x/term v0.6.0
golang.org/x/time v0.3.0
google.golang.org/api v0.114.0
github.com/mattn/go-sqlite3 v1.14.12
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.4.7
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
golang.org/x/time v0.0.0-20220411224347-583f2d630306
google.golang.org/api v0.75.0
gopkg.in/yaml.v2 v2.4.0
)
require github.com/pkg/errors v0.9.1 // indirect
require (
firebase.google.com/go/v4 v4.10.0
github.com/prometheus/client_golang v1.14.0
github.com/stripe/stripe-go/v74 v74.14.0
)
require (
cloud.google.com/go v0.110.0 // indirect
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.13.0 // indirect
cloud.google.com/go/longrunning v0.4.1 // indirect
cloud.google.com/go v0.101.0 // indirect
cloud.google.com/go/compute v1.6.1 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/googleapis/gax-go/v2 v2.3.0 // indirect
github.com/googleapis/go-type-adapters v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/appengine/v2 v2.0.2 // indirect
google.golang.org/genproto v0.0.0-20230330200707-38013875ee22 // indirect
google.golang.org/grpc v1.54.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 // indirect
google.golang.org/grpc v1.46.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

686
go.sum
View File

@@ -1,62 +1,137 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM=
cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E=
firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.101.0 h1:g+LL+JvpvdyGtcaD2xw2mSByE/6F9s471eJSoaysM84=
cloud.google.com/go v0.101.0/go.mod h1:hEiddgDb77jDQ+I80tURYNJEnuwPzFU8awCFFRLKjW0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.22.0 h1:NUV0NNp9nkBuW66BFRLuMgldN60C57ET3dhbwLIYio8=
cloud.google.com/go/storage v1.22.0/go.mod h1:GbaLEoMqbVm6sx3Z0R++gSiBlgMv6yUi2q1DeGFKQgE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -64,153 +139,519 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI=
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stripe/stripe-go/v74 v74.14.0 h1:hB1Ocu/m3BUZ+PrTePsPSv8TKcXTrleCL5Y5JfB8zCo=
github.com/stripe/stripe-go/v74 v74.14.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
github.com/urfave/cli/v2 v2.4.7 h1:nUgKLTC/InVYwUx26HZUBGIBZaptiW97W8vVlhuYawo=
github.com/urfave/cli/v2 v2.4.7/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 h1:yssD99+7tqHWO5Gwh81phT+67hg+KttniBr6UnEXOY8=
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0 h1:0AYh/ae6l9TDUvIQrDw5QRpM100P6oHgD+o3dYHMzJg=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230330200707-38013875ee22 h1:n3ThVoQnHbCbnkhZZ1fx3+3fBAisViSwrpbtLV7vydY=
google.golang.org/genproto v0.0.0-20230330200707-38013875ee22/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 h1:nquqdM9+ps0JZcIiI70+tqoaIFS5Ql4ZuK8UXnz3HfE=
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -219,17 +660,30 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -1,240 +0,0 @@
package log
import (
"encoding/json"
"fmt"
"heckel.io/ntfy/util"
"log"
"os"
"sort"
"strings"
"time"
)
const (
fieldTag = "tag"
fieldError = "error"
fieldTimeTaken = "time_taken_ms"
fieldExitCode = "exit_code"
tagStdLog = "stdlog"
)
// Event represents a single log event
type Event struct {
Timestamp string `json:"time"`
Level Level `json:"level"`
Message string `json:"message"`
time time.Time
contexters []Contexter
fields Context
}
// newEvent creates a new log event
//
// We delay allocations and processing for efficiency, because most log events
// are never actually rendered, so we don't format the time, or allocate a fields map.
func newEvent() *Event {
return &Event{
time: time.Now(),
}
}
// Fatal logs the event as FATAL, and exits the program with exit code 1
func (e *Event) Fatal(message string, v ...any) {
e.Field(fieldExitCode, 1).maybeLog(FatalLevel, message, v...)
fmt.Fprintf(os.Stderr, message+"\n", v...) // Always output error to stderr
os.Exit(1)
}
// Error logs the event with log level error
func (e *Event) Error(message string, v ...any) {
e.maybeLog(ErrorLevel, message, v...)
}
// Warn logs the event with log level warn
func (e *Event) Warn(message string, v ...any) {
e.maybeLog(WarnLevel, message, v...)
}
// Info logs the event with log level info
func (e *Event) Info(message string, v ...any) {
e.maybeLog(InfoLevel, message, v...)
}
// Debug logs the event with log level debug
func (e *Event) Debug(message string, v ...any) {
e.maybeLog(DebugLevel, message, v...)
}
// Trace logs the event with log level trace
func (e *Event) Trace(message string, v ...any) {
e.maybeLog(TraceLevel, message, v...)
}
// Tag adds a "tag" field to the log event
func (e *Event) Tag(tag string) *Event {
return e.Field(fieldTag, tag)
}
// Time sets the time field
func (e *Event) Time(t time.Time) *Event {
e.time = t
return e
}
// Timing runs f and records the time if took to execute it in "time_taken_ms"
func (e *Event) Timing(f func()) *Event {
start := time.Now()
f()
return e.Field(fieldTimeTaken, time.Since(start).Milliseconds())
}
// Err adds an "error" field to the log event
func (e *Event) Err(err error) *Event {
if err == nil {
return e
} else if c, ok := err.(Contexter); ok {
return e.With(c)
}
return e.Field(fieldError, err.Error())
}
// Field adds a custom field and value to the log event
func (e *Event) Field(key string, value any) *Event {
if e.fields == nil {
e.fields = make(Context)
}
e.fields[key] = value
return e
}
// Fields adds a map of fields to the log event
func (e *Event) Fields(fields Context) *Event {
if e.fields == nil {
e.fields = make(Context)
}
for k, v := range fields {
e.fields[k] = v
}
return e
}
// With adds the fields of the given Contexter structs to the log event by calling their Context method
func (e *Event) With(contexters ...Contexter) *Event {
if e.contexters == nil {
e.contexters = contexters
} else {
e.contexters = append(e.contexters, contexters...)
}
return e
}
// Render returns the rendered log event as a string, or an empty string. The event is only rendered,
// if either the global log level is >= l, or if the log level in one of the overrides matches
// the level.
//
// If no overrides are defined (default), the Contexter array is not applied unless the event
// is actually logged. If overrides are defined, then Contexters have to be applied in any case
// to determine if they match. This is super complicated, but required for efficiency.
func (e *Event) Render(l Level, message string, v ...any) string {
appliedContexters := e.maybeApplyContexters()
if !e.shouldLog(l) {
return ""
}
e.Message = fmt.Sprintf(message, v...)
e.Level = l
e.Timestamp = util.FormatTime(e.time)
if !appliedContexters {
e.applyContexters()
}
if CurrentFormat() == JSONFormat {
return e.JSON()
}
return e.String()
}
// maybeLog logs the event to the defined output, or does nothing if Render returns an empty string
func (e *Event) maybeLog(l Level, message string, v ...any) {
if m := e.Render(l, message, v...); m != "" {
log.Println(m)
}
}
// Loggable returns true if the given log level is lower or equal to the current log level
func (e *Event) Loggable(l Level) bool {
return e.globalLevelWithOverride() <= l
}
// IsTrace returns true if the current log level is TraceLevel
func (e *Event) IsTrace() bool {
return e.Loggable(TraceLevel)
}
// IsDebug returns true if the current log level is DebugLevel or below
func (e *Event) IsDebug() bool {
return e.Loggable(DebugLevel)
}
// JSON returns the event as a JSON representation
func (e *Event) JSON() string {
b, _ := json.Marshal(e)
s := string(b)
if len(e.fields) > 0 {
b, _ := json.Marshal(e.fields)
s = fmt.Sprintf("{%s,%s}", s[1:len(s)-1], string(b[1:len(b)-1]))
}
return s
}
// String returns the event as a string
func (e *Event) String() string {
if len(e.fields) == 0 {
return fmt.Sprintf("%s %s", e.Level.String(), e.Message)
}
fields := make([]string, 0)
for k, v := range e.fields {
fields = append(fields, fmt.Sprintf("%s=%v", k, v))
}
sort.Strings(fields)
return fmt.Sprintf("%s %s (%s)", e.Level.String(), e.Message, strings.Join(fields, ", "))
}
func (e *Event) shouldLog(l Level) bool {
return e.globalLevelWithOverride() <= l
}
func (e *Event) globalLevelWithOverride() Level {
mu.RLock()
l, ov := level, overrides
mu.RUnlock()
if e.fields == nil {
return l
}
for field, fieldOverrides := range ov {
value, exists := e.fields[field]
if exists {
for _, o := range fieldOverrides {
if o.value == "" || o.value == value || o.value == fmt.Sprintf("%v", value) {
return o.level
}
}
}
}
return l
}
func (e *Event) maybeApplyContexters() bool {
mu.RLock()
hasOverrides := len(overrides) > 0
mu.RUnlock()
if hasOverrides {
e.applyContexters()
}
return hasOverrides // = applied
}
func (e *Event) applyContexters() {
for _, c := range e.contexters {
e.Fields(c.Context())
}
}

View File

@@ -1,206 +0,0 @@
package log
import (
"io"
"log"
"os"
"strings"
"sync"
"time"
)
// Defaults for package level variables
var (
DefaultLevel = InfoLevel
DefaultFormat = TextFormat
DefaultOutput = &peekLogWriter{os.Stderr}
)
var (
level = DefaultLevel
format = DefaultFormat
overrides = make(map[string][]*levelOverride)
output io.Writer = DefaultOutput
filename = ""
mu = &sync.RWMutex{}
)
// init sets the default log output (including log.SetOutput)
//
// This has to be explicitly called, because DefaultOutput is a peekLogWriter,
// which wraps os.Stderr.
func init() {
SetOutput(DefaultOutput)
}
// Fatal prints the given message, and exits the program
func Fatal(message string, v ...any) {
newEvent().Fatal(message, v...)
}
// Error prints the given message, if the current log level is ERROR or lower
func Error(message string, v ...any) {
newEvent().Error(message, v...)
}
// Warn prints the given message, if the current log level is WARN or lower
func Warn(message string, v ...any) {
newEvent().Warn(message, v...)
}
// Info prints the given message, if the current log level is INFO or lower
func Info(message string, v ...any) {
newEvent().Info(message, v...)
}
// Debug prints the given message, if the current log level is DEBUG or lower
func Debug(message string, v ...any) {
newEvent().Debug(message, v...)
}
// Trace prints the given message, if the current log level is TRACE
func Trace(message string, v ...any) {
newEvent().Trace(message, v...)
}
// With creates a new log event and adds the fields of the given Contexter structs
func With(contexts ...Contexter) *Event {
return newEvent().With(contexts...)
}
// Field creates a new log event and adds a custom field and value to it
func Field(key string, value any) *Event {
return newEvent().Field(key, value)
}
// Fields creates a new log event and adds a map of fields to it
func Fields(fields Context) *Event {
return newEvent().Fields(fields)
}
// Tag creates a new log event and adds a "tag" field to it
func Tag(tag string) *Event {
return newEvent().Tag(tag)
}
// Time creates a new log event and sets the time field
func Time(time time.Time) *Event {
return newEvent().Time(time)
}
// Timing runs f and records the time if took to execute it in "time_taken_ms"
func Timing(f func()) *Event {
return newEvent().Timing(f)
}
// CurrentLevel returns the current log level
func CurrentLevel() Level {
mu.RLock()
defer mu.RUnlock()
return level
}
// SetLevel sets a new log level
func SetLevel(newLevel Level) {
mu.Lock()
defer mu.Unlock()
level = newLevel
}
// SetLevelOverride adds a log override for the given field
func SetLevelOverride(field string, value string, level Level) {
mu.Lock()
defer mu.Unlock()
if _, ok := overrides[field]; !ok {
overrides[field] = make([]*levelOverride, 0)
}
overrides[field] = append(overrides[field], &levelOverride{value: value, level: level})
}
// ResetLevelOverrides removes all log level overrides
func ResetLevelOverrides() {
mu.Lock()
defer mu.Unlock()
overrides = make(map[string][]*levelOverride)
}
// CurrentFormat returns the current log format
func CurrentFormat() Format {
mu.RLock()
defer mu.RUnlock()
return format
}
// SetFormat sets a new log format
func SetFormat(newFormat Format) {
mu.Lock()
defer mu.Unlock()
format = newFormat
if newFormat == JSONFormat {
DisableDates()
}
}
// SetOutput sets the log output writer
func SetOutput(w io.Writer) {
mu.Lock()
defer mu.Unlock()
output = &peekLogWriter{w}
if f, ok := w.(*os.File); ok {
filename = f.Name()
} else {
filename = ""
}
log.SetOutput(output)
}
// File returns the log file, if any, or an empty string otherwise
func File() string {
mu.RLock()
defer mu.RUnlock()
return filename
}
// IsFile returns true if the output is a non-default file
func IsFile() bool {
mu.RLock()
defer mu.RUnlock()
return filename != ""
}
// DisableDates disables the date/time prefix
func DisableDates() {
log.SetFlags(0)
}
// Loggable returns true if the given log level is lower or equal to the current log level
func Loggable(l Level) bool {
return CurrentLevel() <= l
}
// IsTrace returns true if the current log level is TraceLevel
func IsTrace() bool {
return Loggable(TraceLevel)
}
// IsDebug returns true if the current log level is DebugLevel or below
func IsDebug() bool {
return Loggable(DebugLevel)
}
// peekLogWriter is an io.Writer which will peek at the rendered log event,
// and ensure that the rendered output is valid JSON. This is a hack!
type peekLogWriter struct {
w io.Writer
}
func (w *peekLogWriter) Write(p []byte) (n int, err error) {
if len(p) == 0 || p[0] == '{' || CurrentFormat() == TextFormat {
return w.w.Write(p)
}
m := newEvent().Tag(tagStdLog).Render(InfoLevel, strings.TrimSpace(string(p)))
if m == "" {
return 0, nil
}
return w.w.Write([]byte(m + "\n"))
}

Some files were not shown because too many files have changed in this diff Show More