Compare commits

..

1 Commits

Author SHA1 Message Date
binwiederhier
f3c955b0f8 PoC: Load external images 2023-06-01 22:13:31 -04:00
210 changed files with 2756 additions and 13680 deletions

View File

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

View File

@@ -7,28 +7,35 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
-
name: Checkout code
uses: actions/checkout@v3
- name: Install Go
-
name: Install Go
uses: actions/setup-go@v4
with:
go-version: '1.22.x'
- name: Install node
go-version: '1.19.x'
-
name: Install node
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: '18'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- name: Docker login
-
name: Docker login
uses: docker/login-action@v2
with:
username: ${{ github.repository_owner }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Install dependencies
-
name: Install dependencies
run: make build-deps-ubuntu
- name: Build and publish
-
name: Build and publish
run: make release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Print build results and checksums
-
name: Print build results and checksums
run: make cli-build-results

View File

@@ -1,30 +1,39 @@
name: test
on: [ push, pull_request ]
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
-
name: Checkout code
uses: actions/checkout@v3
- name: Install Go
-
name: Install Go
uses: actions/setup-go@v4
with:
go-version: '1.22.x'
- name: Install node
go-version: '1.19.x'
-
name: Install node
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: '18'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- name: Install dependencies
-
name: Install dependencies
run: make build-deps-ubuntu
- name: Build docs (required for tests)
-
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

2
.gitignore vendored
View File

@@ -13,5 +13,3 @@ secrets/
node_modules/
.DS_Store
__pycache__
web/dev-dist/
venv/

View File

@@ -119,6 +119,8 @@ archives:
- server/ntfy.service
- client/client.yml
- client/ntfy-client.service
replacements:
amd64: x86_64
-
id: ntfy_windows
builds:
@@ -129,6 +131,8 @@ archives:
- LICENSE
- README.md
- client/client.yml
replacements:
amd64: x86_64
-
id: ntfy_darwin
builds:
@@ -138,6 +142,8 @@ archives:
- LICENSE
- README.md
- client/client.yml
replacements:
darwin: macOS
universal_binaries:
-
id: ntfy_darwin_all
@@ -164,14 +170,14 @@ dockers:
- image_templates:
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
use: buildx
dockerfile: Dockerfile-arm
dockerfile: Dockerfile
goarch: arm64
build_flag_templates:
- "--platform=linux/arm64/v8"
- image_templates:
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
use: buildx
dockerfile: Dockerfile-arm
dockerfile: Dockerfile
goarch: arm
goarm: 7
build_flag_templates:
@@ -179,7 +185,7 @@ dockers:
- image_templates:
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
use: buildx
dockerfile: Dockerfile-arm
dockerfile: Dockerfile
goarch: arm
goarm: 6
build_flag_templates:

View File

@@ -9,7 +9,6 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
RUN apk add --no-cache tzdata
COPY ntfy /usr/bin
EXPOSE 80/tcp

View File

@@ -1,18 +0,0 @@
FROM alpine
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
# Alpine does not support adding "tzdata" on ARM anymore, see
# https://github.com/binwiederhier/ntfy/issues/894
COPY ntfy /usr/bin
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]

View File

@@ -1,20 +1,14 @@
FROM golang:1.22-bullseye as builder
FROM golang:1.19-bullseye as builder
ARG VERSION=dev
ARG COMMIT=unknown
ARG NODE_MAJOR=18
RUN apt-get update && apt-get install -y \
build-essential ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y \
python3-pip \
python3-venv \
nodejs \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash
RUN apt-get install -y \
build-essential \
nodejs \
python3-pip
WORKDIR /app
ADD Makefile .
@@ -25,7 +19,7 @@ RUN make docs-deps
ADD ./mkdocs.yml .
ADD ./docs ./docs
RUN make docs-build
# web
ADD ./web/package.json ./web/package-lock.json ./web/
RUN make web-deps

View File

@@ -1,6 +1,4 @@
MAKEFLAGS := --jobs=1
PYTHON := python3
PIP := pip3
VERSION := $(shell git describe --tag)
COMMIT := $(shell git rev-parse --short HEAD)
@@ -41,8 +39,8 @@ help:
@echo " make web-deps - Install web app dependencies (npm install the universe)"
@echo " make web-build - Actually build the web app"
@echo " make web-lint - Run eslint on the web app"
@echo " make web-fmt - Run prettier on the web app"
@echo " make web-fmt-check - Run prettier on the web app, but don't change anything"
@echo " make web-format - Run prettier on the web app"
@echo " make web-format-check - Run prettier on the web app, but don't change anything"
@echo
@echo "Build documentation:"
@echo " make docs - Build the documentation"
@@ -97,7 +95,6 @@ docker-dev:
--build-arg COMMIT=$(COMMIT) \
./
# Ubuntu-specific
build-deps-ubuntu:
@@ -106,27 +103,32 @@ build-deps-ubuntu:
curl \
gcc-aarch64-linux-gnu \
gcc-arm-linux-gnueabi \
python3 \
python3-venv \
jq
which pip3 || sudo apt-get install -y python3-pip
# Documentation
docs: docs-deps docs-build
docs-venv: .PHONY
$(PYTHON) -m venv ./venv
docs-build: .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-build: docs-venv
(. venv/bin/activate && $(PYTHON) -m mkdocs build)
docs-deps: docs-venv
(. venv/bin/activate && $(PIP) install -r requirements.txt)
docs-deps: .PHONY
pip3 install -r requirements.txt
docs-deps-update: .PHONY
(. venv/bin/activate && $(PIP) install -r requirements.txt --upgrade)
pip3 install -r requirements.txt --upgrade
# Web app
@@ -149,10 +151,10 @@ web-deps:
web-deps-update:
cd web && npm update
web-fmt:
web-format:
cd web && npm run format
web-fmt-check:
web-format-check:
cd web && npm run format:check
web-lint:
@@ -246,7 +248,7 @@ cli-build-results:
# Test/check targets
check: test web-fmt-check fmt-check vet web-lint lint staticcheck
check: test web-format-check fmt-check vet web-lint lint staticcheck
test: .PHONY
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
@@ -273,7 +275,7 @@ coverage-upload:
# Lint/formatting targets
fmt: web-fmt
fmt:
gofmt -s -w .
fmt-check:

View File

@@ -2,13 +2,14 @@
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest)
[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy/v2)
[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy)
[![Tests](https://github.com/binwiederhier/ntfy/workflows/test/badge.svg)](https://github.com/binwiederhier/ntfy/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/binwiederhier/ntfy)](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
[![codecov](https://codecov.io/gh/binwiederhier/ntfy/branch/main/graph/badge.svg?token=A597KQ463G)](https://codecov.io/gh/binwiederhier/ntfy)
[![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w)
[![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org)
[![Matrix space](https://img.shields.io/matrix/ntfy-space:matrix.org?label=Matrix+space)](https://matrix.to/#/#ntfy-space:matrix.org)
[![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)
@@ -17,7 +18,7 @@ notification service. With ntfy, you can **send notifications to your phone or d
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do
so since ntfy is open source.
You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open-source Android app](https://github.com/binwiederhier/ntfy-android)
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).
@@ -30,10 +31,7 @@ as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) a
</p>
## [ntfy Pro](https://ntfy.sh/app) 💸 🎉
I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of
ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $5/month**.
You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy).
I would be very humbled by your sponsorship. ❤️
I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $3.33/month** (if you use promo code `MYTOPIC`, limited time only). You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy). I would be very humbled by your sponsorship. ❤️
## **[Documentation](https://ntfy.sh/docs/)**
@@ -43,21 +41,23 @@ I would be very humbled by your sponsorship. ❤️
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
[Building](https://ntfy.sh/docs/develop/)
## Chat/forum
## 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
## 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 contributions. Just create a PR or an issue. For larger features/ideas, please reach out
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
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
@@ -69,7 +69,7 @@ for the server and the Android app. Or, if you'd like to help translate 🇩🇪
## 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 who have sponsored ntfy in the past, or are still sponsoring ntfy:
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>
@@ -141,53 +141,8 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
<a href="https://github.com/CreativeWarlock"><img src="https://github.com/CreativeWarlock.png" width="40px" /></a>
<a href="https://github.com/darkdragon-001"><img src="https://github.com/darkdragon-001.png" width="40px" /></a>
<a href="https://github.com/jonathan-kosgei"><img src="https://github.com/jonathan-kosgei.png" width="40px" /></a>
<a href="https://github.com/KevinWang15"><img src="https://github.com/KevinWang15.png" width="40px" /></a>
<a href="https://github.com/darkmattercoder"><img src="https://github.com/darkmattercoder.png" width="40px" /></a>
<a href="https://github.com/bmcgonag"><img src="https://github.com/bmcgonag.png" width="40px" /></a>
<a href="https://github.com/skorokithakis"><img src="https://github.com/skorokithakis.png" width="40px" /></a>
<a href="https://github.com/eenturk"><img src="https://github.com/eenturk.png" width="40px" /></a>
<a href="https://github.com/spirossi"><img src="https://github.com/spirossi.png" width="40px" /></a>
<a href="https://github.com/teomarcdhio"><img src="https://github.com/teomarcdhio.png" width="40px" /></a>
<a href="https://github.com/MarcMichalsky"><img src="https://github.com/MarcMichalsky.png" width="40px" /></a>
<a href="https://github.com/LuckVintage"><img src="https://github.com/LuckVintage.png" width="40px" /></a>
<a href="https://github.com/spartan"><img src="https://github.com/spartan.png" width="40px" /></a>
<a href="https://github.com/alexandzors"><img src="https://github.com/alexandzors.png" width="40px" /></a>
<a href="https://github.com/dkramer95"><img src="https://github.com/dkramer95.png" width="40px" /></a>
<a href="https://github.com/YezGotIt"><img src="https://github.com/YezGotIt.png" width="40px" /></a>
<a href="https://github.com/thomasskou"><img src="https://github.com/thomasskou.png" width="40px" /></a>
<a href="https://github.com/surfernv"><img src="https://github.com/surfernv.png" width="40px" /></a>
<a href="https://github.com/richardleach"><img src="https://github.com/richardleach.png" width="40px" /></a>
<a href="https://github.com/bear"><img src="https://github.com/bear.png" width="40px" /></a>
<a href="https://github.com/cminter"><img src="https://github.com/cminter.png" width="40px" /></a>
<a href="https://github.com/bahur142"><img src="https://github.com/bahur142.png" width="40px" /></a>
<a href="https://github.com/pgwiebes"><img src="https://github.com/pgwiebes.png" width="40px" /></a>
<a href="https://github.com/ralhei"><img src="https://github.com/ralhei.png" width="40px" /></a>
<a href="https://github.com/TechMDW"><img src="https://github.com/TechMDW.png" width="40px" /></a>
<a href="https://github.com/ubipo"><img src="https://github.com/ubipo.png" width="40px" /></a>
<a href="https://github.com/tka85"><img src="https://github.com/tka85.png" width="40px" /></a>
<a href="https://github.com/beekeeb"><img src="https://github.com/beekeeb.png" width="40px" /></a>
<a href="https://github.com/Emiliaaah"><img src="https://github.com/Emiliaaah.png" width="40px" /></a>
<a href="https://github.com/zark0s"><img src="https://github.com/zark0s.png" width="40px" /></a>
<a href="https://github.com/tomershvueli"><img src="https://github.com/tomershvueli.png" width="40px" /></a>
<a href="https://github.com/CataIana"><img src="https://github.com/CataIana.png" width="40px" /></a>
<a href="https://github.com/ajay-actuary"><img src="https://github.com/ajay-actuary.png" width="40px" /></a>
<a href="https://github.com/mursec"><img src="https://github.com/mursec.png" width="40px" /></a>
<a href="https://github.com/FrameXX"><img src="https://github.com/FrameXX.png" width="40px" /></a>
<a href="https://github.com/vovayartsev"><img src="https://github.com/vovayartsev.png" width="40px" /></a>
<a href="https://github.com/dwain-lab"><img src="https://github.com/dwain-lab.png" width="40px" /></a>
<a href="https://github.com/brookmg"><img src="https://github.com/brookmg.png" width="40px" /></a>
<a href="https://github.com/siebej"><img src="https://github.com/siebej.png" width="40px" /></a>
<a href="https://github.com/rxsantos"><img src="https://github.com/rxsantos.png" width="40px" /></a>
<a href="https://github.com/hermannx5"><img src="https://github.com/hermannx5.png" width="40px" /></a>
<a href="https://github.com/rwxd"><img src="https://github.com/rwxd.png" width="40px" /></a>
<a href="https://github.com/Integral-Tech"><img src="https://github.com/Integral-Tech.png" width="40px" /></a>
<a href="https://github.com/TheTomik1"><img src="https://github.com/TheTomik1.png" width="40px" /></a>
<a href="https://github.com/dav23r"><img src="https://github.com/dav23r.png" width="40px" /></a>
<a href="https://github.com/stannynuytkens"><img src="https://github.com/stannynuytkens.png" width="40px" /></a>
<a href="https://github.com/danbartram"><img src="https://github.com/danbartram.png" width="40px" /></a>
<a href="https://github.com/arthurgleckler"><img src="https://github.com/arthurgleckler.png" width="40px" /></a>
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
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>
@@ -203,7 +158,7 @@ _Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
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:
Third party libraries and resources:
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
@@ -223,4 +178,3 @@ Third-party libraries and resources:
* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used)
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)
* [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications

View File

@@ -7,8 +7,8 @@ import (
"encoding/json"
"errors"
"fmt"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"io"
"net/http"
"regexp"

View File

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

View File

@@ -3,9 +3,9 @@ package client_test
import (
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/test"
"os"
"testing"
"time"

View File

@@ -2,7 +2,6 @@ package client
import (
"gopkg.in/yaml.v2"
"heckel.io/ntfy/v2/log"
"os"
)
@@ -24,9 +23,9 @@ type Config struct {
// Subscribe is the struct for a Subscription within Config
type Subscribe struct {
Topic string `yaml:"topic"`
User *string `yaml:"user"`
User string `yaml:"user"`
Password *string `yaml:"password"`
Token *string `yaml:"token"`
Token string `yaml:"token"`
Command string `yaml:"command"`
If map[string]string `yaml:"if"`
}
@@ -45,7 +44,6 @@ func NewConfig() *Config {
// LoadConfig loads the Client config from a yaml file
func LoadConfig(filename string) (*Config, error) {
log.Debug("Loading client config from %s", filename)
b, err := os.ReadFile(filename)
if err != nil {
return nil, err

View File

@@ -2,7 +2,7 @@ package client_test
import (
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/client"
"os"
"path/filepath"
"testing"
@@ -37,7 +37,7 @@ subscribe:
require.Equal(t, 4, len(conf.Subscribe))
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
require.Equal(t, "", conf.Subscribe[0].Command)
require.Equal(t, "phil", *conf.Subscribe[0].User)
require.Equal(t, "phil", conf.Subscribe[0].User)
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
@@ -67,7 +67,7 @@ subscribe:
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, "phil", conf.Subscribe[0].User)
require.Equal(t, "", *conf.Subscribe[0].Password)
}
@@ -91,7 +91,7 @@ subscribe:
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, "phil", conf.Subscribe[0].User)
require.Nil(t, conf.Subscribe[0].Password)
}
@@ -113,7 +113,7 @@ subscribe:
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, "phil", conf.Subscribe[0].User)
require.Nil(t, conf.Subscribe[0].Password)
}
@@ -134,7 +134,7 @@ subscribe:
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
require.Equal(t, 1, len(conf.Subscribe))
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
require.Nil(t, conf.Subscribe[0].User)
require.Equal(t, "", conf.Subscribe[0].User)
require.Nil(t, conf.Subscribe[0].Password)
require.Nil(t, conf.Subscribe[0].Token)
require.Equal(t, "", conf.Subscribe[0].Token)
}

View File

@@ -2,7 +2,7 @@ package client
import (
"fmt"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
"net/http"
"strings"
"time"
@@ -72,11 +72,6 @@ func WithAttach(attach string) PublishOption {
return WithHeader("X-Attach", attach)
}
// WithMarkdown instructs the server to interpret the message body as Markdown
func WithMarkdown() PublishOption {
return WithHeader("X-Markdown", "yes")
}
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
func WithFilename(filename string) PublishOption {
return WithHeader("X-Filename", filename)
@@ -97,11 +92,6 @@ func WithBearerAuth(token string) PublishOption {
return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
}
// WithEmptyAuth clears the Authorization header
func WithEmptyAuth() PublishOption {
return RemoveHeader("Authorization")
}
// WithNoCache instructs the server not to cache the message server-side
func WithNoCache() PublishOption {
return WithHeader("X-Cache", "no")
@@ -192,13 +182,3 @@ func WithQueryParam(param, value string) RequestOption {
return nil
}
}
// RemoveHeader is a generic option to remove a header from a request
func RemoveHeader(header string) RequestOption {
return func(r *http.Request) error {
if header != "" {
delete(r.Header, header)
}
return nil
}
}

View File

@@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
)
func init() {

View File

@@ -4,13 +4,12 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"testing"
)
func TestCLI_Access_Show(t *testing.T) {
t.Parallel()
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
@@ -20,7 +19,6 @@ func TestCLI_Access_Show(t *testing.T) {
}
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
t.Parallel()
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/log"
"os"
"regexp"
)

View File

@@ -4,8 +4,8 @@ import (
"bytes"
"encoding/json"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"os"
"strings"
"testing"

View File

@@ -5,7 +5,7 @@ import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"gopkg.in/yaml.v2"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
"os"
)

View File

@@ -8,7 +8,6 @@ import (
)
func TestNewYamlSourceFromFile(t *testing.T) {
t.Parallel()
filename := filepath.Join(t.TempDir(), "server.yml")
contents := `
# Normal options

View File

@@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"io"
"os"
"os/exec"
@@ -31,7 +31,6 @@ var flagsPublish = append(
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
&cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"},
&cli.StringFlag{Name: "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"},
@@ -96,7 +95,6 @@ func execPublish(c *cli.Context) error {
icon := c.String("icon")
actions := c.String("actions")
attach := c.String("attach")
markdown := c.Bool("markdown")
filename := c.String("filename")
file := c.String("file")
email := c.String("email")
@@ -142,9 +140,6 @@ func execPublish(c *cli.Context) error {
if attach != "" {
options = append(options, client.WithAttach(attach))
}
if markdown {
options = append(options, client.WithMarkdown())
}
if filename != "" {
options = append(options, client.WithFilename(filename))
}

View File

@@ -3,8 +3,8 @@ package cmd
import (
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
"net/http"
"net/http/httptest"
"os"
@@ -17,7 +17,6 @@ import (
)
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
t.Parallel()
testMessage := util.RandomString(10)
app, _, _, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
@@ -36,7 +35,6 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
}
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
t.Parallel()
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
@@ -53,7 +51,6 @@ func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
}
func TestCLI_Publish_All_The_Things(t *testing.T) {
t.Parallel()
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)

View File

@@ -6,22 +6,23 @@ import (
"errors"
"fmt"
"github.com/stripe/stripe-go/v74"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"io/fs"
"math"
"net"
"net/netip"
"net/url"
"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"
)
func init() {
@@ -34,7 +35,7 @@ const (
var flagsServe = append(
append([]cli.Flag{}, flagsDefault...),
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, Usage: "config file"},
&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"}),
@@ -44,19 +45,19 @@ var flagsServe = append(
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.NewStringFlag(&cli.StringFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: util.FormatDuration(server.DefaultCacheDuration), Usage: "buffer messages for this time to allow `since` requests"}),
altsrc.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.NewStringFlag(&cli.StringFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Value: util.FormatDuration(server.DefaultCacheBatchTimeout), Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
altsrc.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"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultAttachmentExpiryDuration), Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: util.FormatDuration(server.DefaultKeepaliveInterval), Usage: "interval of keepalive messages"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: util.FormatDuration(server.DefaultManagerInterval), Usage: "interval of for message pruning and stats printing"}),
altsrc.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: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
@@ -75,18 +76,16 @@ var flagsServe = append(
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-phone-number", Aliases: []string{"twilio_phone_number"}, EnvVars: []string{"NTFY_TWILIO_PHONE_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-size-limit", Aliases: []string{"message_size_limit"}, EnvVars: []string{"NTFY_MESSAGE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultMessageSizeLimit), Usage: "size limit for the message (see docs for limitations)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-delay-limit", Aliases: []string{"message_delay_limit"}, EnvVars: []string{"NTFY_MESSAGE_DELAY_LIMIT"}, Value: util.FormatDuration(server.DefaultMessageDelayMax), Usage: "max duration a message can be scheduled into the future"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultVisitorAttachmentTotalSizeLimit), Usage: "total storage limit used for attachments per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-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.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorRequestLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.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.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.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"}),
@@ -95,11 +94,6 @@ var flagsServe = append(
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}),
)
var cmdServe = &cli.Command{
@@ -127,7 +121,7 @@ func execServe(c *cli.Context) error {
// Read all the options
config := c.String("config")
baseURL := strings.TrimSuffix(c.String("base-url"), "/")
baseURL := c.String("base-url")
listenHTTP := c.String("listen-http")
listenHTTPS := c.String("listen-https")
listenUnix := c.String("listen-unix")
@@ -135,25 +129,20 @@ func execServe(c *cli.Context) error {
keyFile := c.String("key-file")
certFile := c.String("cert-file")
firebaseKeyFile := c.String("firebase-key-file")
webPushPrivateKey := c.String("web-push-private-key")
webPushPublicKey := c.String("web-push-public-key")
webPushFile := c.String("web-push-file")
webPushEmailAddress := c.String("web-push-email-address")
webPushStartupQueries := c.String("web-push-startup-queries")
cacheFile := c.String("cache-file")
cacheDurationStr := c.String("cache-duration")
cacheDuration := c.Duration("cache-duration")
cacheStartupQueries := c.String("cache-startup-queries")
cacheBatchSize := c.Int("cache-batch-size")
cacheBatchTimeoutStr := c.String("cache-batch-timeout")
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")
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
attachmentExpiryDurationStr := c.String("attachment-expiry-duration")
keepaliveIntervalStr := c.String("keepalive-interval")
managerIntervalStr := c.String("manager-interval")
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")
@@ -172,19 +161,17 @@ func execServe(c *cli.Context) error {
twilioAuthToken := c.String("twilio-auth-token")
twilioPhoneNumber := c.String("twilio-phone-number")
twilioVerifyService := c.String("twilio-verify-service")
messageSizeLimitStr := c.String("message-size-limit")
messageDelayLimitStr := c.String("message-delay-limit")
totalTopicLimit := c.Int("global-topic-limit")
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
visitorSubscriberRateLimiting := c.Bool("visitor-subscriber-rate-limiting")
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
visitorRequestLimitReplenishStr := c.String("visitor-request-limit-replenish")
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")
visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish")
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
behindProxy := c.Bool("behind-proxy")
stripeSecretKey := c.String("stripe-secret-key")
stripeWebhookKey := c.String("stripe-webhook-key")
@@ -193,69 +180,9 @@ func execServe(c *cli.Context) error {
enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
profileListenHTTP := c.String("profile-listen-http")
// Convert durations
cacheDuration, err := util.ParseDuration(cacheDurationStr)
if err != nil {
return fmt.Errorf("invalid cache duration: %s", cacheDurationStr)
}
cacheBatchTimeout, err := util.ParseDuration(cacheBatchTimeoutStr)
if err != nil {
return fmt.Errorf("invalid cache batch timeout: %s", cacheBatchTimeoutStr)
}
attachmentExpiryDuration, err := util.ParseDuration(attachmentExpiryDurationStr)
if err != nil {
return fmt.Errorf("invalid attachment expiry duration: %s", attachmentExpiryDurationStr)
}
keepaliveInterval, err := util.ParseDuration(keepaliveIntervalStr)
if err != nil {
return fmt.Errorf("invalid keepalive interval: %s", keepaliveIntervalStr)
}
managerInterval, err := util.ParseDuration(managerIntervalStr)
if err != nil {
return fmt.Errorf("invalid manager interval: %s", managerIntervalStr)
}
messageDelayLimit, err := util.ParseDuration(messageDelayLimitStr)
if err != nil {
return fmt.Errorf("invalid message delay limit: %s", messageDelayLimitStr)
}
visitorRequestLimitReplenish, err := util.ParseDuration(visitorRequestLimitReplenishStr)
if err != nil {
return fmt.Errorf("invalid visitor request limit replenish: %s", visitorRequestLimitReplenishStr)
}
visitorEmailLimitReplenish, err := util.ParseDuration(visitorEmailLimitReplenishStr)
if err != nil {
return fmt.Errorf("invalid visitor email limit replenish: %s", visitorEmailLimitReplenishStr)
}
// Convert sizes to bytes
messageSizeLimit, err := util.ParseSize(messageSizeLimitStr)
if err != nil {
return fmt.Errorf("invalid message size limit: %s", messageSizeLimitStr)
}
attachmentTotalSizeLimit, err := util.ParseSize(attachmentTotalSizeLimitStr)
if err != nil {
return fmt.Errorf("invalid attachment total size limit: %s", attachmentTotalSizeLimitStr)
}
attachmentFileSizeLimit, err := util.ParseSize(attachmentFileSizeLimitStr)
if err != nil {
return fmt.Errorf("invalid attachment file size limit: %s", attachmentFileSizeLimitStr)
}
visitorAttachmentTotalSizeLimit, err := util.ParseSize(visitorAttachmentTotalSizeLimitStr)
if err != nil {
return fmt.Errorf("invalid visitor attachment total size limit: %s", visitorAttachmentTotalSizeLimitStr)
}
visitorAttachmentDailyBandwidthLimit, err := util.ParseSize(visitorAttachmentDailyBandwidthLimitStr)
if err != nil {
return fmt.Errorf("invalid visitor attachment daily bandwidth limit: %s", visitorAttachmentDailyBandwidthLimitStr)
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
}
// Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
return errors.New("if set, FCM key file must exist")
} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
} else if keepaliveInterval < 5*time.Second {
return errors.New("keepalive interval cannot be lower than five seconds")
} else if managerInterval < 5*time.Second {
@@ -274,15 +201,10 @@ func execServe(c *cli.Context) error {
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 != "" {
u, err := url.Parse(baseURL)
if err != nil {
return fmt.Errorf("if set, base-url must be a valid URL, e.g. https://ntfy.mydomain.com: %v", err)
} else if u.Scheme != "http" && u.Scheme != "https" {
return errors.New("if set, base-url must be a valid URL starting with http:// or https://, e.g. https://ntfy.mydomain.com")
} else if u.Path != "" {
return fmt.Errorf("if set, base-url must not have a path (%s), as hosting ntfy on a sub-path is not supported, e.g. https://ntfy.mydomain.com", u.Path)
}
} 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 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, "/") {
@@ -299,11 +221,6 @@ func execServe(c *cli.Context) error {
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
return errors.New("if twilio-account is set, twilio-auth-token, twilio-phone-number, twilio-verify-service, base-url, and auth-file must also be set")
} else if messageSizeLimit > server.DefaultMessageSizeLimit {
log.Warn("message-size-limit is greater than 4K, this is not recommended and largely untested, and may lead to issues with some clients")
if messageSizeLimit > 5*1024*1024 {
return errors.New("message-size-limit cannot be higher than 5M")
}
}
// Backwards compatibility
@@ -328,6 +245,26 @@ func execServe(c *cli.Context) error {
listenHTTP = ""
}
// Convert sizes to bytes
attachmentTotalSizeLimit, err := parseSize(attachmentTotalSizeLimitStr, server.DefaultAttachmentTotalSizeLimit)
if err != nil {
return err
}
attachmentFileSizeLimit, err := parseSize(attachmentFileSizeLimitStr, server.DefaultAttachmentFileSizeLimit)
if err != nil {
return err
}
visitorAttachmentTotalSizeLimit, err := parseSize(visitorAttachmentTotalSizeLimitStr, server.DefaultVisitorAttachmentTotalSizeLimit)
if err != nil {
return err
}
visitorAttachmentDailyBandwidthLimit, err := parseSize(visitorAttachmentDailyBandwidthLimitStr, server.DefaultVisitorAttachmentDailyBandwidthLimit)
if err != nil {
return err
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
}
// Resolve hosts
visitorRequestLimitExemptIPs := make([]netip.Prefix, 0)
for _, host := range visitorRequestLimitExemptHosts {
@@ -388,8 +325,6 @@ func execServe(c *cli.Context) error {
conf.TwilioAuthToken = twilioAuthToken
conf.TwilioPhoneNumber = twilioPhoneNumber
conf.TwilioVerifyService = twilioVerifyService
conf.MessageSizeLimit = int(messageSizeLimit)
conf.MessageDelayMax = messageDelayLimit
conf.TotalTopicLimit = totalTopicLimit
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
@@ -412,11 +347,6 @@ func execServe(c *cli.Context) error {
conf.MetricsListenHTTP = metricsListenHTTP
conf.ProfileListenHTTP = profileListenHTTP
conf.Version = c.App.Version
conf.WebPushPrivateKey = webPushPrivateKey
conf.WebPushPublicKey = webPushPublicKey
conf.WebPushFile = webPushFile
conf.WebPushEmailAddress = webPushEmailAddress
conf.WebPushStartupQueries = webPushStartupQueries
// Set up hot-reloading of config
go sigHandlerConfigReload(config)
@@ -432,6 +362,17 @@ func execServe(c *cli.Context) error {
return nil
}
func parseSize(s string, defaultValue int64) (v int64, err error) {
if s == "" {
return defaultValue, nil
}
v, err = util.ParseSize(s)
if err != nil {
return 0, err
}
return v, nil
}
func sigHandlerConfigReload(config string) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP)

View File

@@ -12,11 +12,15 @@ import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/client"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
)
func init() {
rand.Seed(time.Now().UnixMilli())
}
func TestCLI_Serve_Unix_Curl(t *testing.T) {
sockFile := filepath.Join(t.TempDir(), "ntfy.sock")
configFile := newEmptyFile(t) // Avoid issues with existing server.yml file on system

View File

@@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"os"
"os/exec"
"os/user"
@@ -225,17 +225,12 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
}
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
// if an explicit empty token or empty user:pass is given, exit without auth
if (s.Token != nil && *s.Token == "") || (s.User != nil && *s.User == "" && s.Password != nil && *s.Password == "") {
return client.WithEmptyAuth()
}
// check for subscription token then subscription user:pass
if s.Token != nil && *s.Token != "" {
return client.WithBearerAuth(*s.Token)
if s.Token != "" {
return client.WithBearerAuth(s.Token)
}
if s.User != nil && *s.User != "" && s.Password != nil {
return client.WithBasicAuth(*s.User, *s.Password)
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
@@ -310,43 +305,28 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
if filename != "" {
return client.LoadConfig(filename)
}
configFile, err := defaultClientConfigFile()
if err != nil {
log.Warn("Could not determine default client config file: %s", err.Error())
} else {
if s, _ := os.Stat(configFile); s != nil {
return client.LoadConfig(configFile)
}
log.Debug("Config file %s not found", configFile)
configFile := defaultClientConfigFile()
if s, _ := os.Stat(configFile); s != nil {
return client.LoadConfig(configFile)
}
log.Debug("Loading default config")
return client.NewConfig(), nil
}
//lint:ignore U1000 Conditionally used in different builds
func defaultClientConfigFileUnix() (string, error) {
u, err := user.Current()
if err != nil {
return "", fmt.Errorf("could not determine current user: %w", err)
}
func defaultClientConfigFileUnix() string {
u, _ := user.Current()
configFile := clientRootConfigFileUnixAbsolute
if u.Uid != "0" {
homeDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("could not determine user config dir: %w", err)
}
return filepath.Join(homeDir, clientUserConfigFileUnixRelative), nil
homeDir, _ := os.UserConfigDir()
return filepath.Join(homeDir, clientUserConfigFileUnixRelative)
}
return configFile, nil
return configFile
}
//lint:ignore U1000 Conditionally used in different builds
func defaultClientConfigFileWindows() (string, error) {
homeDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("could not determine user config dir: %w", err)
}
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative), nil
func defaultClientConfigFileWindows() string {
homeDir, _ := os.UserConfigDir()
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative)
}
func logMessagePrefix(m *client.Message) string {

View File

@@ -11,6 +11,6 @@ var (
scriptLauncher = []string{"sh", "-c"}
)
func defaultClientConfigFile() (string, error) {
func defaultClientConfigFile() string {
return defaultClientConfigFileUnix()
}

View File

@@ -330,7 +330,7 @@ default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
@@ -355,63 +355,7 @@ default-password: mypass
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_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, "", 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
user: ""
password: ""
`, 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_Override_Default_Token_With_Empty_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, "", 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
token: ""
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}

View File

@@ -13,6 +13,6 @@ var (
scriptLauncher = []string{"sh", "-c"}
)
func defaultClientConfigFile() (string, error) {
func defaultClientConfigFile() string {
return defaultClientConfigFileUnix()
}

View File

@@ -10,6 +10,6 @@ var (
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
)
func defaultClientConfigFile() (string, error) {
func defaultClientConfigFile() string {
return defaultClientConfigFileWindows()
}

View File

@@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
)
func init() {
@@ -366,9 +366,9 @@ func printTier(c *cli.Context, tier *user.Tier) {
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
fmt.Fprintf(c.App.ErrWriter, "- Phone call limit: %d\n", tier.CallLimit)
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSizeHuman(tier.AttachmentFileSizeLimit))
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSizeHuman(tier.AttachmentTotalSizeLimit))
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.FormatSizeHuman(tier.AttachmentBandwidthLimit))
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

@@ -3,8 +3,8 @@ package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"testing"
)

View File

@@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"net/netip"
"time"
)

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"regexp"
"testing"
)

View File

@@ -6,13 +6,13 @@ import (
"crypto/subtle"
"errors"
"fmt"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/user"
"os"
"strings"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
)
const (
@@ -198,6 +198,7 @@ func execUserAdd(c *cli.Context) error {
if err != nil {
return err
}
password = p
}
if err := manager.AddUser(username, password, role); err != nil {
@@ -342,8 +343,6 @@ func readPasswordAndConfirm(c *cli.Context) (string, error) {
password, err := util.ReadPassword(c.App.Reader)
if err != nil {
return "", err
} else if len(password) == 0 {
return "", errors.New("password cannot be empty")
}
fmt.Fprintf(c.App.ErrWriter, "\r%s\rconfirm: ", strings.Repeat(" ", 25))
confirm, err := util.ReadPassword(c.App.Reader)

View File

@@ -3,9 +3,9 @@ package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"heckel.io/ntfy/user"
"os"
"path/filepath"
"testing"

View File

@@ -1,48 +0,0 @@
//go:build !noserver
package cmd
import (
"fmt"
"github.com/SherClockHolmes/webpush-go"
"github.com/urfave/cli/v2"
)
func init() {
commands = append(commands, cmdWebPush)
}
var cmdWebPush = &cli.Command{
Name: "webpush",
Usage: "Generate keys, in the future manage web push subscriptions",
UsageText: "ntfy webpush [keys]",
Category: categoryServer,
Subcommands: []*cli.Command{
{
Action: generateWebPushKeys,
Name: "keys",
Usage: "Generate VAPID keys to enable browser background push notifications",
UsageText: "ntfy webpush keys",
Category: categoryServer,
},
},
}
func generateWebPushKeys(c *cli.Context) error {
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
if err != nil {
return err
}
_, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file:
web-push-public-key: %s
web-push-private-key: %s
web-push-file: /var/cache/ntfy/webpush.db # or similar
web-push-email-address: <email address>
See https://ntfy.sh/docs/config/#web-push for details.
`, publicKey, privateKey)
return err
}

View File

@@ -1,24 +0,0 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
)
func TestCLI_WebPush_GenerateKeys(t *testing.T) {
app, _, _, stderr := newTestApp()
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys"))
require.Contains(t, stderr.String(), "Web Push keys generated.")
}
func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error {
webPushArgs := []string{
"ntfy",
"--log-level=ERROR",
"webpush",
}
return app.Run(append(webPushArgs, args...))
}

View File

@@ -24,7 +24,7 @@ get a list of [command line options](#command-line-options).
The most basic settings are `base-url` (the external URL of the ntfy server), the HTTP/HTTPS listen address (`listen-http`
and `listen-https`), and socket path (`listen-unix`). All the other things are additional features.
Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
Here are a few working sample configs:
=== "server.yml (HTTP-only, with cache + attachments)"
``` yaml
@@ -44,14 +44,6 @@ Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
attachment-cache-dir: "/var/cache/ntfy/attachments"
```
=== "server.yml (behind proxy, with cache + attachments)"
``` yaml
base-url: "http://ntfy.example.com"
listen-http: ":2586"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
```
=== "server.yml (ntfy.sh config)"
``` yaml
# All the things: Behind a proxy, Firebase, cache, attachments,
@@ -73,58 +65,6 @@ Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
keepalive-interval: "45s"
```
Alternatively, you can also use command line arguments or environment variables to configure the server. Here's an example
using Docker Compose (i.e. `docker-compose.yml`):
=== "Docker Compose (w/ auth, cache, attachments)"
``` yaml
version: '3'
services:
ntfy:
image: binwiederhier/ntfy
restart: unless-stopped
environment:
NTFY_BASE_URL: http://ntfy.example.com
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
NTFY_AUTH_DEFAULT_ACCESS: deny-all
NTFY_BEHIND_PROXY: true
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
NTFY_ENABLE_LOGIN: true
volumes:
- ./:/var/lib/ntfy
ports:
- 80:80
command: serve
```
=== "Docker Compose (w/ auth, cache, web push, iOS)"
``` yaml
version: '3'
services:
ntfy:
image: binwiederhier/ntfy
restart: unless-stopped
environment:
NTFY_BASE_URL: http://ntfy.example.com
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
NTFY_AUTH_DEFAULT_ACCESS: deny-all
NTFY_BEHIND_PROXY: true
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
NTFY_ENABLE_LOGIN: true
NTFY_UPSTREAM_BASE_URL: https://ntfy.sh
NTFY_WEB_PUSH_PUBLIC_KEY: <public_key>
NTFY_WEB_PUSH_PRIVATE_KEY: <private_key>
NTFY_WEB_PUSH_FILE: /var/lib/ntfy/webpush.db
NTFY_WEB_PUSH_EMAIL_ADDRESS: <email>
volumes:
- ./:/var/lib/ntfy
ports:
- 8093:80
command: serve
```
## Message cache
If desired, ntfy can temporarily keep notifications in an in-memory or an on-disk cache. Caching messages for a short period
of time is important to allow [phones](subscribe/phone.md) and other devices with brittle Internet connections to be able to retrieve
@@ -404,10 +344,10 @@ 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/developers/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
has anonymous write access to the [topic](https://unifiedpush.org/developers/spec/definitions/#endpoint) used for push messages.
[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-acl)** for more details.
**[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:
@@ -518,31 +458,6 @@ $ dig A mx1.ntfy.sh +short
3.139.215.220
```
### Local-only email
If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to
worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be
anything).
=== "/etc/ntfy/server.yml"
``` yaml
smtp-server-listen: ":25"
smtp-server-domain: "example.com"
smtp-server-addr-prefix: "ntfy-" # optional
```
Then, in the email settings of your internal service, set the SMTP server address to the IP address of your
ntfy instance. Set the port to the value you defined in `smtp-server-listen`. Leave any username and password
fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); the value doesn't matter.
In the "To" address, put in an email address that follows this pattern: `[topic]@[smtp-server-domain]` (or
`[smtp-server-addr-prefix][topic]@[smtp-server-domain]` if you set `smtp-server-addr-prefix`).
So if you used `example.com` as the SMTP server domain, and you want to send a message to the `email-alerts`
topic, set the "To" address to `email-alerts@example.com`. If the topic has access restrictions, you will need
to include an access token in the "To" address, such as `email-alerts+tk_AbC123dEf456@example.com`.
If the internal service lets you use define an email "Subject", it will become the title of the notification.
The body of the email will become the message of the notification.
## Behind a proxy (TLS, etc.)
!!! warning
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
@@ -734,8 +649,8 @@ or the root domain:
<VirtualHost *:80>
ServerName ntfy.sh
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
# Proxy connections to ntfy (requires "a2enmod proxy")
ProxyPass / http://127.0.0.1:2586/
ProxyPassReverse / http://127.0.0.1:2586/
SetEnv proxy-nokeepalive 1
@@ -743,13 +658,19 @@ or the root domain:
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
# Enable mod_rewrite (requires "a2enmod rewrite")
RewriteEngine on
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
# it to work with curl without the annoying https:// prefix (requires "a2enmod alias")
<If "%{REQUEST_METHOD} == 'GET'">
RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1"
</If>
# it to work with curl without the annoying https:// prefix
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>
<VirtualHost *:443>
@@ -760,8 +681,8 @@ or the root domain:
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
# Proxy connections to ntfy (requires "a2enmod proxy")
ProxyPass / http://127.0.0.1:2586/
ProxyPassReverse / http://127.0.0.1:2586/
SetEnv proxy-nokeepalive 1
@@ -769,7 +690,14 @@ or the root domain:
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
# Enable mod_rewrite (requires "a2enmod rewrite")
RewriteEngine on
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
</VirtualHost>
```
@@ -861,57 +789,6 @@ Note that the self-hosted server literally sends the message `New message` for e
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.
## Web Push
[Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030))
allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed.
When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the
user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then
forward it to the browser.
To configure Web Push, you need to generate and configure a [VAPID](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) keypair (via `ntfy webpush keys`),
a database to keep track of the browser's subscriptions, and an admin email address (you):
- `web-push-public-key` is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
- `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
- `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
- `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com`
- `web-push-startup-queries` is an optional list of queries to run on startup`
Limitations:
- Like foreground browser notifications, background push notifications require the web app to be served over HTTPS. A _valid_
certificate is required, as service workers will not run on origins with untrusted certificates.
- Web Push is only supported for the same server. You cannot use subscribe to web push on a topic on another server. This
is due to a limitation of the Push API, which doesn't allow multiple push servers for the same origin.
To configure VAPID keys, first generate them:
```sh
$ ntfy webpush keys
Web Push keys generated.
...
```
Then copy the generated values into your `server.yml` or use the corresponding environment variables or command line arguments:
```yaml
web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890
web-push-file: /var/cache/ntfy/webpush.db
web-push-email-address: sysadmin@example.com
```
The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days,
and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed),
subscriptions are also removed automatically.
The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription
file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then.
Changing your public/private keypair is **not recommended**. Browsers only allow one server identity (public key) per origin, and
if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission.
## 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),
@@ -995,15 +872,6 @@ are the easiest), and then configure the following options:
After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message.
## Message limits
There are a few message limits that you can configure:
* `message-size-limit` defines the max size of a message body. Please note message sizes >4K are **not recommended,
and largely untested**. The Android/iOS and other clients may not work, or work properly. If FCM and/or APNS is used,
the limit should stay 4K, because their limits are around that size. If you increase this size limit regardless,
FCM and APNS will NOT work for large messages.
* `message-delay-limit` defines the max delay of a message when using the "Delay" header and [scheduled delivery](publish.md#scheduled-delivery).
## Rate limiting
!!! info
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
@@ -1087,23 +955,20 @@ By default, ntfy puts almost all rate limits on the message publisher, e.g. numb
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 subscriber-based rate limiting is enabled, **messages published on UnifiedPush topics** (topics starting with `up`, e.g. `up123456789012`)
will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.
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 subscribing to UnifiedPush topics via HTTP stream, or websockets, will be automatically registered as
a "rate visitor", i.e. the visitor whose rate limits will be used when publishing on this topic. Note that setting the rate visitor
requires **read-write permission** on the topic.
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.
If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
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`.
!!! info
Due to a [denial-of-service issue](https://github.com/binwiederhier/ntfy/issues/1048), support for the `Rate-Topics`
header was removed entirely. This is unfortunate, but subscriber-based rate limiting will still work for `up*` topics.
## 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**.
@@ -1244,10 +1109,10 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
## 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 `healthy` field is `false` the ntfy service should be considered as unhealthy.
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
{"healthy":true}
{"health":true}
```
See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment.
@@ -1400,8 +1265,6 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `twilio-verify-service` | `NTFY_TWILIO_VERIFY_SERVICE` | *string* | - | Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 |
| `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. |
| `message-size-limit` | `NTFY_MESSAGE_SIZE_LIMIT` | *size* | 4K | The size limit for the message body. Please note that this is largely untested, and that FCM/APNS have limits around 4KB. If you increase this size limit, FCM and APNS will NOT work for large messages. |
| `message-delay-limit` | `NTFY_MESSAGE_DELAY_LIMIT` | *duration* | 3d | Amount of time a message can be [scheduled](publish.md#scheduled-delivery) into the future when using the `Delay` header |
| `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 |
| `upstream-access-token` | `NTFY_UPSTREAM_ACCESS_TOKEN` | *string* | `tk_zyYLYj...` | Access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth |
@@ -1422,17 +1285,13 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `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 |
| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy webpush keys` to generate |
| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy webpush keys` to generate |
| `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions |
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
| `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup |
The format for a *duration* is: `<number>(smhd)`, e.g. 30s, 20m, 1h or 3d.
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.
## Command line options
```
$ ntfy serve --help
NAME:
ntfy serve - Run the ntfy server
@@ -1460,36 +1319,35 @@ OPTIONS:
--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]
--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 as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
--listen-https value, --listen_https value, -L value ip:port used as HTTPS listen address [$NTFY_LISTEN_HTTPS]
--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: "12h") [$NTFY_CACHE_DURATION]
--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-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: "1m") [$NTFY_MANAGER_INTERVAL]
--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 root of the web app (e.g. /, or /app), or disables it (disable) (default: "/") [$NTFY_WEB_ROOT]
--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]
--upstream-access-token value, --upstream_access_token value access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth [$NTFY_UPSTREAM_ACCESS_TOKEN]
--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]
@@ -1497,34 +1355,20 @@ OPTIONS:
--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]
--twilio-account value, --twilio_account value Twilio account SID, used for phone calls, e.g. AC123... [$NTFY_TWILIO_ACCOUNT]
--twilio-auth-token value, --twilio_auth_token value Twilio auth token [$NTFY_TWILIO_AUTH_TOKEN]
--twilio-phone-number value, --twilio_phone_number value Twilio number to use for outgoing calls [$NTFY_TWILIO_PHONE_NUMBER]
--twilio-verify-service value, --twilio_verify_service value Twilio Verify service ID, used for phone number verification [$NTFY_TWILIO_VERIFY_SERVICE]
--message-size-limit value, --message_size_limit value size limit for the message (see docs for limitations) (default: "4K") [$NTFY_MESSAGE_SIZE_LIMIT]
--message-delay-limit value, --message_delay_limit value max duration a message can be scheduled into the future (default: "3d") [$NTFY_MESSAGE_DELAY_LIMIT]
--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-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: "1h") [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
--visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
--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]
--enable-metrics, --enable_metrics if set, Prometheus metrics are exposed via the /metrics endpoint (default: false) [$NTFY_ENABLE_METRICS]
--metrics-listen-http value, --metrics_listen_http value ip:port used to expose the metrics endpoint (implicitly enables metrics) [$NTFY_METRICS_LISTEN_HTTP]
--profile-listen-http value, --profile_listen_http value ip:port used to expose the profiling endpoints (implicitly enables profiling) [$NTFY_PROFILE_LISTEN_HTTP]
--web-push-public-key value, --web_push_public_key value public key used for web push notifications [$NTFY_WEB_PUSH_PUBLIC_KEY]
--web-push-private-key value, --web_push_private_key value private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY]
--web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE]
--web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
--web-push-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
--help, -h show help
--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)
```

View File

@@ -1,4 +1,4 @@
# Deprecations and breaking changes
# 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.

View File

@@ -16,7 +16,7 @@ server consists of three components:
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
build the docs.
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Vite](https://vitejs.dev/)
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Create React App](https://create-react-app.dev/)
to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`)
and install all the 100,000 dependencies (*sigh*).
@@ -241,41 +241,6 @@ $ cd web
$ npm start
```
### Testing Web Push locally
Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-service-workers-via-http>
#### With the dev servers
1. Get web push keys `go run main.go webpush keys`
2. Run the server with web push enabled
```sh
go run main.go \
--log-level debug \
serve \
--web-push-public-key KEY \
--web-push-private-key KEY \
--web-push-email-address <email> \
--web-push-file=/tmp/webpush.db
```
3. In `web/public/config.js`:
- Set `base_url` to `http://localhost`, This is required as web push can only be used with the server matching the `base_url`.
- Set the `web_push_public_key` correctly.
4. Run `npm run start`
#### With a built package
1. Run `make web-build`
2. Run the server (step 2 above)
3. Open <http://localhost/>
### Build the docs
The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the
documentation. As long as you have `mkdocs` installed (see above), this should work fine:
@@ -363,7 +328,7 @@ To build your own version with Firebase, you must:
* And change `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
* Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/play/release/*.apk)
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
./gradlew assemblePlayRelease
# To build a bundle .aab (app/play/release/*.aab)
@@ -429,7 +394,7 @@ steps:
### XCode setup
1. Follow step 4 of [Add Firebase to your Apple project](https://firebase.google.com/docs/ios/setup) to install the
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

View File

@@ -2,9 +2,9 @@
<!-- This file was generated by scripts/emoji-convert.sh -->
You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
[tagging and emojis page](publish.md#tags-emojis).
[tagging and emojis page](../publish/#tags-emojis).
<table class="remove-md-box emoji-table"><tr>

View File

@@ -135,21 +135,6 @@ You can send a message during a workflow run with curl. Here is an example sendi
${{ secrets.NTFY_URL }}
```
## Changedetection.io
ntfy is an excellent choice for getting notifications when a website has a change sent to your mobile (or desktop),
[changedetection.io](https://changedetection.io) or on GitHub ([dgtlmoon/changedetection.io](https://github.com/dgtlmoon/changedetection.io))
uses [apprise](https://github.com/caronc/apprise) library for notification integrations.
To add any ntfy(s) notification to a website change simply add the [ntfy style URL](https://github.com/caronc/apprise/wiki/Notify_ntfy)
to the notification list.
For example `ntfy://{topic}` or `ntfy://{user}:{password}@{host}:{port}/{topics}`
In your changedetection.io installation, click `Edit` > `Notifications` on a single website watch (or group) then add
the special ntfy Apprise Notification URL to the Notification List.
![ntfy alerts on website change](static/img/cdio-setup.jpg)
## Watchtower (shoutrrr)
You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
@@ -162,23 +147,14 @@ services:
image: containrrr/watchtower
environment:
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_SKIP_TITLE=True
- WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
```
The environment variable `WATCHTOWER_NOTIFICATION_SKIP_TITLE` is required to prevent Watchtower from [replacing the `title` query parameter](https://containrrr.dev/watchtower/notifications/#settings). If omitted, the provided notification title will not be used.
Or, if you only want to send notifications using shoutrrr:
```
shoutrrr send -u "ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
```
Authentication tokens are also supported via the generic webhook and authorization header using this url format (replace the domain, topic and token with your own):
```
generic+https://DOMAIN/TOPIC?@authorization=Bearer+TOKEN`
```
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
<!-- Sonarr v4 is in beta as of May 2023, should be updated to remove v3 reference when stable -->

View File

@@ -76,29 +76,7 @@ However, if you still want to disable it, you can do so with the `web-root: disa
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.
## If topic names are public, could I not just brute force them?
If you don't have [ACLs set up](config.md#access-control), the topic name is your password, it says so everywhere. If you
choose a easy-to-guess/dumb topic name, people will be able to guess it. If you choose a randomly generated topic name,
the topic is as good as a good password.
As for brute forcing: It's not possible to brute force a ntfy server for very long, as you'll get quickly rate limited.
In the default configuration, you'll be able to do 60 requests as a burst, and then 1 request per 10 seconds. Assuming you
choose a random 10 digit topic name using only A-Z, a-z, 0-9, _ and -, there are 64^10 possible topic names. Even if you
could do hundreds of requests per seconds (which you cannot), it would take many years to brute force a topic name.
For ntfy.sh, there's even a fail2ban in place which will ban your IP pretty quickly.
## 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.
## Can I email you? Can I DM you on Discord/Matrix?
While I love chatting on [Discord](https://discord.gg/cT7ECsZj9w), [Matrix](https://matrix.to/#/#ntfy-space:matrix.org),
[Lemmy](https://discuss.ntfy.sh/c/ntfy), or [GitHub](https://github.com/binwiederhier/ntfy/issues), I generally
**do not respond to emails about ntfy or direct messages** about ntfy, unless you are paying for a
[ntfy Pro](https://ntfy.sh/#pricing) plan, or you are inquiring about business opportunities.
I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions
in the above-mentioned forums benefits others, since I can link to the discussion at a later point in time, or other users
may be able to help out. I hope you understand.

View File

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

View File

@@ -3,9 +3,9 @@ ntfy lets you **send push notifications to your phone or desktop via scripts fro
or POST requests. I use it to notify myself when scripts fail, or long-running commands complete.
## 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://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>
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

View File

@@ -14,15 +14,14 @@ 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), `~/Library/Application Support/ntfy/client.yml` (for the macOS 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))
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))
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)
for details).
If you like tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU) on YouTube, or
[Alex's Docker-based setup guide](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/). Both are great
resources to get started. _I am not affiliated with Kris or Alex, I just liked their video/post._
If you like video tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU).
It's short and to the point. _I am not affiliated with Kris, I just liked the video._
## Linux binaries
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
@@ -30,37 +29,37 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.tar.gz
tar zxvf ntfy_2.9.0_linux_amd64.tar.gz
sudo cp -a ntfy_2.9.0_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_amd64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_x86_64.tar.gz
tar zxvf ntfy_2.5.0_linux_x86_64.tar.gz
sudo cp -a ntfy_2.5.0_linux_x86_64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.tar.gz
tar zxvf ntfy_2.9.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.9.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.tar.gz
tar zxvf ntfy_2.5.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.5.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.tar.gz
tar zxvf ntfy_2.9.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.9.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.tar.gz
tar zxvf ntfy_2.5.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.5.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.tar.gz
tar zxvf ntfy_2.9.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.9.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.tar.gz
tar zxvf ntfy_2.5.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.5.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -110,7 +109,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -118,7 +117,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -126,7 +125,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -134,7 +133,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -144,36 +143,34 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_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.9.0/ntfy_2.9.0_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
## Arch Linux
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/).
You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download,
build and install ntfy and keep it up to date.
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/). You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download, build and install ntfy and keep it up to date.
```
paru -S ntfysh-bin
```
@@ -195,18 +192,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
## 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.9.0/ntfy_2.9.0_darwin_all.tar.gz),
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_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.9.0/ntfy_2.9.0_darwin_all.tar.gz > ntfy_2.9.0_darwin_all.tar.gz
tar zxvf ntfy_2.9.0_darwin_all.tar.gz
sudo cp -a ntfy_2.9.0_darwin_all/ntfy /usr/local/bin/ntfy
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_macOS_all.tar.gz > ntfy_2.5.0_macOS_all.tar.gz
tar zxvf ntfy_2.5.0_macOS_all.tar.gz
sudo cp -a ntfy_2.5.0_macOS_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.9.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_2.5.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
@@ -224,7 +221,7 @@ brew install ntfy
## 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.9.0/ntfy_2.9.0_windows_amd64.zip),
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_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).
@@ -280,7 +277,7 @@ docker run \
Using docker-compose with non-root user and healthchecks enabled:
```yaml
version: "2.3"
version: "2.1"
services:
ntfy:

View File

@@ -6,7 +6,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
## Official integrations
- [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification
- [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
@@ -17,15 +16,13 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [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.8/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
- [Netdata](https://learn.netdata.cloud/docs/alerts-and-notifications/notifications/agent-alert-notifications/ntfy) ⭐ - Real-time performance monitoring
- [Deployer](https://github.com/deployphp/deployer) ⭐ - PHP deployment tool
- [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
- [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring
- [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring
## Integration via HTTP/SMTP/etc.
@@ -58,9 +55,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [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)
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
- [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go)
- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP)
- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
## CLIs + GUIs
@@ -84,6 +78,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [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)
- [website-watcher](https://github.com/muety/website-watcher) - A small tool to watch websites for changes (with XPath support) (Python)
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
@@ -126,57 +121,9 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig)
- [ntfy-browser](https://github.com/johman10/ntfy-browser) - browser extension to receive notifications without having the page open (TypeScript)
- [ntfy-electron](https://github.com/xdpirate/ntfy-electron) - Electron wrapper for the ntfy web app (JS)
- [systemd-ntfy-poweronoff](https://github.com/stendler/systemd-ntfy-poweronoff) - Systemd services to send notifications on system startup and shutdown (Go)
- [msgdrop](https://github.com/jbrubake/msgdrop) - Send and receive encrypted messages (Bash)
- [vigilant](https://github.com/VerifiedJoseph/vigilant) - Monitor RSS/ATOM and JSON feeds, and send push notifications on new entries (PHP)
- [ansible-role-ntfy-alertmanager](https://github.com/bleetube/ansible-role-ntfy-alertmanager) - Ansible role to install xenrox/ntfy-alertmanager
- [NtfyMe-Blender](https://github.com/NotNanook/NtfyMe-Blender) - Blender addon to send notifications to NtfyMe (Python)
- [ntfy-ios-url-share](https://www.icloud.com/shortcuts/be8a7f49530c45f79733cfe3e41887e6) - An iOS shortcut that lets you share URLs easily and quickly.
- [ntfy-ios-filesharing](https://www.icloud.com/shortcuts/fe948d151b2e4ae08fb2f9d6b27d680b) - An iOS shortcut that lets you share files from your share feed to a topic of your choice.
- [systemd-ntfy](https://hackage.haskell.org/package/systemd-ntfy) - monitor a set of systemd services an send a notification to ntfy.sh whenever their status changes
- [RouterOS Scripts](https://git.eworm.de/cgit/routeros-scripts/about/) - a collection of scripts for MikroTik RouterOS
- [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell)
- [jetspotter](https://github.com/vvanouytsel/jetspotter) - send notifications when planes are spotted near you (Go)
- [monitoring_ntfy](https://www.drupal.org/project/monitoring_ntfy) - Drupal monitoring Ntfy.sh integration (PHP/Drupal)
- [Notify](https://flathub.org/apps/com.ranfdev.Notify) - Native GTK4 client for ntfy (Rust)
- [notify-via-ntfy](https://exchange.checkmk.com/p/notify-via-ntfy) - Checkmk plugin to send notifications via ntfy (Python)
- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
## Blog + forum posts
- [ntfy / Emacs Lisp](https://speechcode.com/blog/ntfy/) - speechcode.com - 3/2024
- [Boost Your Productivity with ntfy.sh: The Ultimate Notification Tool for Command-Line Users](https://dev.to/archetypal/boost-your-productivity-with-ntfysh-the-ultimate-notification-tool-for-command-line-users-iil) - dev.to - 3/2024
- [Nextcloud Talk (F-Droid version) notifications using ntfy (ntfy.sh)](https://www.youtube.com/watch?v=0a6PpfN5PD8) - youtube.com - 2/2024
- [ZFS and SMART Warnings via Ntfy](https://rair.dev/zfs-smart-ntfy/) - rair.dev - 2/2024
- [Automating Security Camera Notifications With Home Assistant and Ntfy](https://runtimeterror.dev/automating-camera-notifications-home-assistant-ntfy/) ⭐ - runtimeterror.dev - 2/2024
- [Ntfy: self-hosted notification service](https://medium.com/@williamdonze/ntfy-self-hosted-notification-service-0f3eada6e657) ⭐ - williamdonze.medium.com - 1/2024
- [Lets Supercharge Snowflake Alerts with Cool ntfy Open-source Notifications!](https://sarathi-data-ml-cloud.medium.com/lets-supercharge-snowflake-alerts-with-cool-ntfy-open-source-notifications-296da442c331) - sarathi-data-ml-cloud.medium.com - 1/2024
- [Setting up NTFY with Ngnix-Proxy-Manager, authentication and Ansible notifications](https://random-it-blog.de/rocky-linux/setting-up-ntfy-with-ngnix-proxy-manager-authentication-and-ansible-notifications/) - random-it-blog.de - 12/2023
- [Introducing the Monitoring Ntfy.sh Integration Module: Real-time Notifications for Drupal Monitoring](https://cyberschorsch.dev/drupal/introducing-monitoring-ntfysh-integration-module-real-time-notifications-drupal-monitoring) - cyberschorsch.dev - 11/2023
- [How to install Ntfy.sh on CasaOS using BigBearCasaOS](https://www.youtube.com/watch?v=wSWhtSNwTd8) - youtube.com - 10/2023
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-update-notifications-ntfy/) - rair.dev - 9/2023
- [Easy Push Notifications With ntfy.sh](https://runtimeterror.dev/easy-push-notifications-with-ntfy/) ⭐ - runtimeterror.dev - 9/2023
- [Ntfy: Your Ultimate Push Notification Powerhouse!](https://kkamalesh117.medium.com/ntfy-your-ultimate-push-notification-powerhouse-1968c070f1d1) - kkamalesh117.medium.com - 9/2023
- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023
- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023
- [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023
- [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023
- [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023
- [Send Notifications With Ntfy for New WordPress Posts](https://www.activepieces.com/blog/ntfy-notifications-for-wordpress-new-posts) - activepieces.com - 9/2023
- [Get Ntfy Notifications About New Zendesk Ticket](https://www.activepieces.com/blog/ntfy-notifications-about-new-zendesk-tickets) - activepieces.com - 9/2023
- [Set reminder for recurring events using ntfy & Cron](https://www.youtube.com/watch?v=J3O4aQ-EcYk) - youtube.com - 9/2023
- [ntfy - Installation and full configuration setup](https://www.youtube.com/watch?v=QMy14rGmpFI) - youtube.com - 9/2023
- [How to install Ntfy.sh on Portainer / Docker Compose](https://www.youtube.com/watch?v=utD9GNbAwyg) - youtube.com - 9/2023
- [ntfy - Push-Benachrichtigungen // Push Notifications](https://www.youtube.com/watch?v=LE3vRPPqZOU) - youtube.com - 9/2023
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-upadte-notifications-ntfy/) - rair.dev - 9/2023
- [How to Send Alerts From Raspberry Pi Pico W to a Phone or Tablet](https://www.tomshardware.com/how-to/send-alerts-raspberry-pi-pico-w-to-mobile-device) - tomshardware.com - 8/2023
- [NetworkChunk - how did I NOT know about this?](https://www.youtube.com/watch?v=poDIT2ruQ9M) ⭐ - youtube.com - 8/2023
- [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023
- [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023
- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023
- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023
- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023
- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023
- [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023
- [桌面通知ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023
- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023
@@ -200,7 +147,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [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
- [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 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
- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022
@@ -242,6 +188,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [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
## Alternative 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
@@ -256,7 +203,6 @@ ntfy community. Thanks to everyone running a public server. **You guys rock!**
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
| [ntfy.fossman.de](https://ntfy.fossman.de/) | 🇩🇪 Germany |
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**.

View File

@@ -1,5 +1,5 @@
# Known issues
This is an incomplete list of known issues with the ntfy server, web app, Android app, and iOS app. You can find a complete
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.
@@ -26,18 +26,3 @@ 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
## iOS app seeing "New message", but not real message content
If you see `New message` notifications on iOS, your iPhone can likely not talk to your self-hosted server. Be sure that
your iOS device and your ntfy server are either on the same network, or that your phone can actually reach the server.
Turn on tracing/debugging on the server (via `log-level: trace` or `log-level: debug`, see [troubleshooting](troubleshooting.md)),
and read docs on [iOS instant notifications](https://docs.ntfy.sh/config/#ios-instant-notifications).
## Safari does not play sounds for web push notifications
Safari does not support playing sounds for web push notifications, and treats them all as silent. This will be fixed with
iOS 17 / Safari 17, which will be released later in 2023.
## PWA on iOS sometimes crashes with an IndexedDB error (see [#787](https://github.com/binwiederhier/ntfy/issues/787))
When resuming the installed PWA from the background, it sometimes crashes with an error from IndexedDB/Dexie, due to a
[WebKit bug]( https://bugs.webkit.org/show_bug.cgi?id=197050). A reload will fix it until a permanent fix is found.

View File

@@ -138,7 +138,7 @@ a [title](#message-title), and [tag messages](#tags-emojis) 🥳 🎉. Here's an
Tags = "warning,skull"
}
Body = "Remote access to phils-laptop detected. Act right away."
}
}
Invoke-RestMethod @Request
```
@@ -457,7 +457,6 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
=== "PowerShell"
``` powershell
$Request = @{
Method = 'POST'
URI = "https://ntfy.sh/phil_alerts"
Headers = @{
Priority = "5"
@@ -624,109 +623,6 @@ them with a comma, e.g. `tag1,tag2,tag3`.
as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `tag1,=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
## Markdown formatting
_Supported on:_ :material-firefox:
You can format messages using [Markdown](https://www.markdownguide.org/basic-syntax/) 🤩. That means you can use
**bold text**, *italicized text*, links, images, and more. Supported Markdown features (web app only for now):
- [Emphasis](https://www.markdownguide.org/basic-syntax/#emphasis) such as **bold** (`**bold**`), *italics* (`*italics*`)
- [Links](https://www.markdownguide.org/basic-syntax/#links) (`[some tool](https://ntfy.sh)`)
- [Images](https://www.markdownguide.org/basic-syntax/#images) (`![some image](https://bing.com/logo.png)`)
- [Code blocks](https://www.markdownguide.org/basic-syntax/#code-blocks) (` ```code blocks``` `) and [inline code](https://www.markdownguide.org/basic-syntax/#inline-code) (`` `inline code` ``)
- [Headings](https://www.markdownguide.org/basic-syntax/#headings) (`# headings`, `## headings`, etc.)
- [Lists](https://www.markdownguide.org/basic-syntax/#lists) (`- lists`, `1. lists`, etc.)
- [Blockquotes](https://www.markdownguide.org/basic-syntax/#blockquotes) (`> blockquotes`)
- [Horizontal rules](https://www.markdownguide.org/basic-syntax/#horizontal-rules) (`---`)
By default, messages sent to ntfy are rendered as plain text. To enable Markdown, set the `X-Markdown` header (or any of
its aliases: `Markdown`, or `md`) to `true` (or `1` or `yes`), or set the `Content-Type` header to `text/markdown`.
As of today, **Markdown is only supported in the web app.** Here's an example of how to enable Markdown formatting:
=== "Command line (curl)"
```
curl \
-d "Look ma, **bold text**, *italics*, ..." \
-H "Markdown: yes" \
ntfy.sh/mytopic
```
=== "ntfy CLI"
```
ntfy publish \
--markdown \
mytopic \
"Look ma, **bold text**, *italics*, ..."
```
=== "HTTP"
``` http
POST /mytopic HTTP/1.1
Host: ntfy.sh
Markdown: yes
Look ma, **bold text**, *italics*, ...
```
=== "JavaScript"
``` javascript
fetch('https://ntfy.sh/mytopic', {
method: 'POST', // PUT works too
body: 'Look ma, **bold text**, *italics*, ...',
headers: { 'Markdown': 'yes' }
})
```
=== "Go"
``` go
http.Post("https://ntfy.sh/mytopic", "text/markdown",
strings.NewReader("Look ma, **bold text**, *italics*, ..."))
// or
req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic",
strings.NewReader("Look ma, **bold text**, *italics*, ..."))
req.Header.Set("Markdown", "yes")
http.DefaultClient.Do(req)
```
=== "PowerShell"
``` powershell
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/mytopic"
Body = "Look ma, **bold text**, *italics*, ..."
Headers = @{
Markdown = "yes"
}
}
Invoke-RestMethod @Request
```
=== "Python"
``` python
requests.post("https://ntfy.sh/mytopic",
data="Look ma, **bold text**, *italics*, ..."
headers={ "Markdown": "yes" }))
```
=== "PHP"
``` php-inline
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
'http' => [
'method' => 'POST', // PUT also works
'header' => 'Content-Type: text/markdown', // !
'content' => 'Look ma, **bold text**, *italics*, ...'
]
]));
```
Here's what that looks like in the web app:
<figure markdown>
![markdown](static/img/web-markdown.png){ width=500 }
<figcaption>Markdown formatting in the web app</figcaption>
</figure>
## Scheduled delivery
_Supported on:_ :material-android: :material-apple: :material-firefox:
@@ -738,8 +634,9 @@ Usage is pretty straight forward. You can set the delivery time using the `X-Del
`3h`, `2 days`), or a natural language time string (e.g. `10am`, `8:30pm`, `tomorrow, 3pm`, `Tuesday, 7am`,
[and more](https://github.com/olebedev/when)).
As of today, the minimum delay you can set is **10 seconds** and the maximum delay is **3 days**. This can be configured
with the `message-delay-limit` option).
As of today, the minimum delay you can set is **10 seconds** and the maximum delay is **3 days**. This can currently
not be configured otherwise ([let me know](https://github.com/binwiederhier/ntfy/issues) if you'd like to change
these limits).
For the purposes of [message caching](config.md#message-cache), scheduled messages are kept in the cache until 12 hours
after they were delivered (or whatever the server-side cache duration is set to). For instance, if a message is scheduled
@@ -1033,7 +930,7 @@ is the only required one:
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = ConvertTo-JSON @{
Body = @{
Topic = "mytopic"
Title = "Low disk space alert"
Message = "Disk space is low at 5.1 GB"
@@ -1042,7 +939,7 @@ is the only required one:
FileName = "diskspace.jpg"
Tags = @("warning", "cd")
Click = "https://homecamera.lan/xasds1h2xsSsa/"
Actions = @(
Actions = ConvertTo-JSON @(
@{
Action = "view"
Label = "Admin panel"
@@ -1107,7 +1004,6 @@ all the supported fields:
| `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications |
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) |
| `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-url) |
| `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted |
| `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) |
| `filename` | - | *string* | `file.jpg` | File name of the attachment |
| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery |
@@ -1130,7 +1026,7 @@ As of today, the following actions are supported:
when the action button is tapped (only supported on Android)
* [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped
Here's an example of what a notification with actions can look like:
Here's an example of what that a notification with actions can look like:
<figure markdown>
![notification with actions](static/img/android-screenshot-notification-actions.png){ width=500 }
@@ -1919,10 +1815,10 @@ And the same example using [JSON publishing](#publish-as-json):
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = ConvertTo-Json -Depth 3 @{
Body = @{
Topic = "wifey"
Message = "Your wife requested you send a picture of yourself."
Actions = @(
Actions = ConvertTo-Json -Depth 3 @(
@{
Action = "broadcast"
Label = "Take picture"
@@ -2072,7 +1968,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
'method' => 'POST',
'header' =>
"Content-Type: text/plain\r\n" .
'Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}',
"Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}",
'content' => 'Garage door has been open for 15 minutes. Close it?'
]
]));
@@ -2199,10 +2095,10 @@ And the same example using [JSON publishing](#publish-as-json):
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = ConvertTo-Json -Depth 3 @{
Body = @{
Topic = "myhome"
Message = "Garage door has been open for 15 minutes. Close it?"
Actions = @(
Actions = ConvertTo-Json -Depth 3 @(
@{
Action = "http"
Label = "Close door"
@@ -2287,7 +2183,7 @@ You can define which URL to open when a notification is clicked. This may be use
to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open
the web browser (or the app) and open the website.
To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its alias `Click`).
To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its aliase `Click`).
If you pass a website URL (`http://` or `https://`) the web browser will open. If you pass another URI that can be handled
by another app, the responsible app may open.
@@ -2439,17 +2335,6 @@ Here's an example showing how to upload an image:
http.DefaultClient.Do(req)
```
=== "PowerShell"
``` powershell
$Request = @{
Method = "POST"
Uri = "ntfy.sh/flowers"
InFile = "flower.jpg"
Headers = @{"Filename" = "flower.jpg"}
}
Invoke-RestMethod @Request
```
=== "Python"
``` python
requests.put("https://ntfy.sh/flowers",
@@ -3557,34 +3442,6 @@ ntfy server plays the role of the Push Gateway, as well as the Push Provider. Un
!!! info
This is not a generic Matrix Push Gateway. It only works in combination with UnifiedPush and ntfy.
### Message and Title Templates
Some services let you specify a webhook URL but do not let you modify the webhook body (e.g., Grafana). Instead of using a separate
bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title
which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON).
Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to "yes". Then, include templates
in your message and/or title (no other fields can be filled with a template at this time) by including paths to the
appropriate JSON fields surrounded by `${` and `}`. See an example below.
See [GJSON docs](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for supported JSON path syntax.
[https://gjson.dev/](https://gjson.dev/) is a great resource for testing your templates.
=== "HTTP"
``` http
POST /mytopic HTTP/1.1
Host: ntfy.sh
X-Message: Error message: ${error.desc}
X-Title: ${hostname}: A ${error.level} error has occurred
X-Template: yes
{"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}
```
The example above would send a notification with a title "philipp-pc: A severe error has occurred" and a message "Error message: Disk has run out of space".
For Grafana webhooks, you might find it helpful to use the headers `X-Title: Grafana alert: ${title}` and `X-Message: ${message}`.
Alternatively, you can include the params in the webhook URL. For example, by
appending `?template=yes&title=Grafana alert: ${title}&message=${message}` to the URL.
## Public topics
Obviously all topics on ntfy.sh are public, but there are a few designated topics that are used in examples, and topics
that you can use to try out what [authentication and access control](#authentication) looks like.
@@ -3636,7 +3493,6 @@ table in their canonical form.
| `X-Actions` | `Actions`, `Action` | JSON array or short format of [user actions](#action-buttons) |
| `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) |
| `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment |
| `X-Markdown` | `Markdown`, `md` | Enable [Markdown formatting](#markdown-formatting) in the notification body |
| `X-Icon` | `Icon` | URL to use as notification [icon](#icons) |
| `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client |
| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) |
@@ -3646,4 +3502,3 @@ table in their canonical form.
| `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps |
| `X-Poll-ID` | `Poll-ID` | Internal parameter, used for [iOS push notifications](config.md#ios-instant-notifications) |
| `Authorization` | - | If supported by the server, you can [login to access](#authentication) protected topics |
| `Content-Type` | - | If set to `text/markdown`, [Markdown formatting](#markdown-formatting) is enabled |

View File

@@ -2,140 +2,6 @@
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.9.0
Released Mar 7, 2024
A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer message delays** in scheduled delivery messages. The web app also now supports pasting images from the clipboard. Other than that, only a few bug fixes and documentation updates, and a teeny tiny breaking change 😬.
!!! info
⚠️ **Breaking change**: The `Rate-Topics` header was removed due to a [DoS issue](https://github.com/binwiederhier/ntfy/issues/1048). This only affects installations with `visitor-subscriber-rate-limiting: true`, which is not the default and likely very rarely used. Normally I'd never remove a feature, but this is a security issue, and likely affects almost nobody.
**Features:**
* Support for larger message delays with `message-delay-limit` (see [message limits](config.md#message-limits), [#1050](https://github.com/binwiederhier/ntfy/pull/1050)/[#1019](https://github.com/binwiederhier/ntfy/issues/1019), thanks to [@MrChadMWood](https://github.com/MrChadMWood) for reporting)
* Support for larger message body sizes with `message-size-limit` (use at your own risk, see [message limits](config.md#message-limits), [#836](https://github.com/binwiederhier/ntfy/pull/836)/[#1050](https://github.com/binwiederhier/ntfy/pull/1050), thanks to [@zhzy0077](https://github.com/zhzy0077) for implementing this, and to [@nkjshlsqja7331](https://github.com/nkjshlsqja7331) for reporting)
* Web app: You can now paste images into the message bar or publish dialog ([#963](https://github.com/binwiederhier/ntfy/pull/963)/[#572](https://github.com/binwiederhier/ntfy/issues/572), thanks to [@cmj2002](https://github.com/cmj2002) for implementing, and [@rounakdatta](https://github.com/rounakdatta) for reporting)
**Bug fixes + maintenance:**
* ⚠️ Remove `Rate-Topics` header due to DoS security issue if `visitor-subscriber-rate-limiting: true` ([#1048](https://github.com/binwiederhier/ntfy/issues/1048))
**Documentation:**
* Remove `mkdocs-simple-hooks` ([#1016](https://github.com/binwiederhier/ntfy/pull/1016), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht))
* Update Watchtower example ([#1014](https://github.com/binwiederhier/ntfy/pull/1014), thanks to [@lennart-m](https://github.com/lennart-m))
* Fix dead links ([#1022](https://github.com/binwiederhier/ntfy/pull/1022), thanks to [@DerRockWolf](https://github.com/DerRockWolf))
* PowerShell file upload example ([#1004](https://github.com/binwiederhier/ntfy/pull/1004), thanks to [@YMan84](https://github.com/YMan84))
## ntfy iOS app v1.3
Released Nov 26, 2023
This release (hopefully) fixes the issues with the iOS UI not updating properly when new notifications arrive, as well as notifications not being received (anymore) after previously working. Both issues have been annoying and known bugs for a long time, and I hope that they are finally fixed.
Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and to the anonymous donor for sponsoring these fixes.
**Bug fixes:**
* UI not updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267)/[#402](https://github.com/binwiederhier/ntfy/issues/402), thanks to [@tcaputi](https://github.com/tcaputi))
## ntfy server v2.8.0
Released November 19, 2023
This release brings a handful of random bug fixes: two unrelated access control list fixes, a fix around web app crashes for languages with underscores in the language code (e.g. `zh_Hant`, `zh_Hans`, `pt_BR`, ...), a workaround for the `Priority` header (often used in Cloudflare setups), and support among others support for HTML-only emails (finally), web app crash fixes
**Bug fixes + maintenance:**
* Support for HTML-only emails ([#690](https://github.com/binwiederhier/ntfy/issues/690)/[#693](https://github.com/binwiederhier/ntfy/pull/693), thanks to [@teastrainer](https://github.com/teastrainer) and [@CrazyWolf13](https://github.com/CrazyWolf13) for reporting)
* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting)
* Fix ACL issue with order of read/write rules ([#914](https://github.com/binwiederhier/ntfy/issues/914)/[#917](https://github.com/binwiederhier/ntfy/pull/917), thanks to [@sandman7920](https://github.com/sandman7920))
* Re-add `tzdata` to Docker images for amd64 image ([#894](https://github.com/binwiederhier/ntfy/issues/894), [#307](https://github.com/binwiederhier/ntfy/pull/307))
* Add special logic to ignore `Priority` header if it resembles an RFC 9218 value ([#851](https://github.com/binwiederhier/ntfy/pull/851)/[#895](https://github.com/binwiederhier/ntfy/pull/895), thanks to [@gusdleon](https://github.com/gusdleon), see also [#351](https://github.com/binwiederhier/ntfy/issues/351), [#353](https://github.com/binwiederhier/ntfy/issues/353), [#461](https://github.com/binwiederhier/ntfy/issues/461))
* PWA: hide install prompt on macOS 14 Safari ([#899](https://github.com/binwiederhier/ntfy/pull/899), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
* Fix web app crash in Edge for languages with underline in locale ([#922](https://github.com/binwiederhier/ntfy/pull/922)/[#912](https://github.com/binwiederhier/ntfy/issues/912)/[#852](https://github.com/binwiederhier/ntfy/issues/852), thanks to [@imkero](https://github.com/imkero))
**Additional languages:**
* Finnish (thanks to [@Seppo](https://hosted.weblate.org/user/Seppo/))
## ntfy server v2.7.0
Released August 17, 2023
This release ships Markdown support for the web app (not in the Android app yet), and adds support for
right-to-left languages (RTL) in the web app. It also fixes a few issues around date/time formatting,
internationalization support, a CLI auth bug.
Furthermore, it fixes a security issue around access tokens getting erroneously deleted for other users
in a specific scenario. This was a denial-of-service-type security issue, since it **effectively allowed a
single user to deny access to all other users of a ntfy instance**. Please note that while tokens were
erroneously deleted, **nobody but the token owner ever had access to it.** Please refer to [the ticket](https://github.com/binwiederhier/ntfy/issues/838)
for details. **Please upgrade your ntfy instance if you run a multi-user system.**
**Features:**
* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Security:** ⚠️
* Fixes issue with access tokens getting deleted ([#838](https://github.com/binwiederhier/ntfy/issues/838))
**Bug fixes + maintenance:**
* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
**Documentation:**
* Update docs for Apache config ([#819](https://github.com/binwiederhier/ntfy/pull/819), thanks to [@nisbet-hubbard](https://github.com/nisbet-hubbard))
## ntfy server v2.6.2
Released June 30, 2023
With this release, the ntfy web app now contains a **[progressive web app](subscribe/pwa.md) (PWA)
with Web Push support**, which means you'll be able to **install the ntfy web app on your desktop or phone** similar
to a native app (__even on iOS!__ 🥳). Installing the PWA gives ntfy web its own launcher, a standalone window,
push notifications, and an app badge with the unread notification count. Note that for self-hosted servers,
[Web Push](config.md#web-push) must be configured.
On top of that, this release also brings **dark mode** 🧛🌙 to the web app.
🙏 A huge thanks for this release goes to [@nimbleghost](https://github.com/nimbleghost), for basically implementing the
Web Push / PWA and dark mode feature by himself. I'm really grateful for your contributions.
❤️ If you like ntfy, **please consider sponsoring us** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
if you use promo code `MYTOPIC`). ntfy will always remain open source.
**Features:**
* The web app now supports Web Push, and is installable as a [progressive web app (PWA)](https://docs.ntfy.sh/subscribe/pwa/) on Chrome, Edge, Android, and iOS ([#751](https://github.com/binwiederhier/ntfy/pull/751), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Support for dark mode in the web app ([#206](https://github.com/binwiederhier/ntfy/issues/206), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Bug fixes:**
* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
* Newly created access tokens are now lowercase only to fully support `<topic>+<token>@<domain>` email syntax ([#773](https://github.com/binwiederhier/ntfy/issues/773), thanks to gingervitiz for reporting)
* The .1 release fixes a few visual issues with dark mode, and other web app updates ([#791](https://github.com/binwiederhier/ntfy/pull/791), [#793](https://github.com/binwiederhier/ntfy/pull/793), [#792](https://github.com/binwiederhier/ntfy/pull/792), thanks to [@nimbleghost](https://github.com/nimbleghost))
* The .2 release fixes issues with the service worker in Firefox and adds automatic service worker updates ([#795](https://github.com/binwiederhier/ntfy/pull/795), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Maintenance:**
* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Changes in tarball/zip naming:**
Due to a [change in GoReleaser](https://goreleaser.com/deprecations/#archivesreplacements), some of the binary release
archives now have slightly different names. My apologies if this causes issues in the downstream projects that use ntfy:
- `ntfy_v${VERSION}_windows_x86_64.zip` -> `ntfy_v${VERSION}_windows_amd64.zip`
- `ntfy_v${VERSION}_linux_x86_64.tar.gz` -> `ntfy_v${VERSION}_linux_amd64.tar.gz`
- `ntfy_v${VERSION}_macOS_all.tar.gz` -> `ntfy_v${VERSION}_darwin_all.tar.gz`
## ntfy server v2.5.0
Released May 18, 2023
@@ -165,7 +31,7 @@ if you use promo code `MYTOPIC`). ntfy will always remain open source.
## ntfy server v2.4.0
Released Apr 26, 2023
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds support to encode the `X-Title`,
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds suport to encode the `X-Title`,
`X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website.
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
@@ -1328,7 +1194,7 @@ Released Dec 28, 2021
**Features & bug fixes:**
* [Publish messages via e-mail](publish.md#e-mail-publishing) #66
* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
* Fixing the Santa bug #65
@@ -1338,12 +1204,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet
### ntfy server v2.9.1 (UNRELEASED)
**Features:**
* You can now include a message and/or title template that will be filled with values from a JSON body, great for services that let you specify a webhook URL but do not let you change the webhook body (such as Grafana). ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing)
### ntfy Android app v1.16.1 (UNRELEASED)
**Features:**
@@ -1359,3 +1219,18 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**Additional languages:**
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
### ntfy server v2.6.0 (UNRELEASED)
**Bug fixes:**
* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
**Maintenance:**
* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

BIN
docs/static/img/web-pin.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -190,10 +190,9 @@ format. Keepalive messages are sent as empty lines.
## WebSockets
You may also subscribe to topics via [WebSockets](https://en.wikipedia.org/wiki/WebSocket), which is also widely
supported in many languages. Most notably, WebSockets are natively supported in JavaScript. You may also want to
check out the [full example on GitHub](https://github.com/binwiederhier/ntfy/tree/main/examples/web-example-websocket).
On the command line, I recommend [websocat](https://github.com/vi/websocat), a fantastic tool similar to `socat`
or `curl`, but specifically for WebSockets.
supported in many languages. Most notably, WebSockets are natively supported in JavaScript. On the command line,
I recommend [websocat](https://github.com/vi/websocat), a fantastic tool similar to `socat` or `curl`, but specifically
for WebSockets.
The WebSockets endpoint is available at `<topic>/ws` and returns messages as JSON objects similar to the
[JSON stream endpoint](#subscribe-as-json-stream).

View File

@@ -10,7 +10,7 @@ to topics via the ntfy CLI. The CLI is included in the same `ntfy` binary that c
## Install + configure
To install the ntfy CLI, simply **follow the steps outlined on the [install page](../install.md)**. The ntfy server and
client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client
by creating `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user). You
by creating `~/.config/ntfy/client.yml` (for the non-root user), or `/etc/ntfy/client.yml` (for the root user). You
can find a [skeleton config](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub.
If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**,

View File

@@ -12,9 +12,6 @@ You can get the Android app from both [Google Play](https://play.google.com/stor
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).
Alternatively, you may also want to consider using the **[progressive web app (PWA)](pwa.md)** instead of the native app.
The PWA is a website that you can add to your home screen, and it will behave just like a native app.
## Overview
A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them.

View File

@@ -1,69 +0,0 @@
# Using the progressive web app (PWA)
While ntfy doesn't have a native desktop app, it is built as a [progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) (PWA)
and thus can be **installed on both desktop and mobile devices**.
This gives it its own launcher (e.g. shortcut on Windows, app on macOS, launcher shortcut on Linux, home screen icon on iOS, and
launcher icon on Android), a standalone window, push notifications, and an app badge with the unread notification count.
Web app installation is **supported on** (see [compatibility table](https://caniuse.com/web-app-manifest) for details):
- **Chrome:** Android, Windows, Linux, macOS
- **Safari:** iOS 16.4+, macOS 14+
- **Firefox:** Android, as well as on Windows/Linux [via an extension](https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/)
- **Edge:** Windows
Note that for self-hosted servers, [Web Push](../config.md#web-push) must be configured for the PWA to work.
## Installation
### Chrome on Desktop
To install and register the web app via Chrome, click the "install app" icon. After installation, you can find the app in your
app drawer:
<div id="pwa-screenshots-chrome-safari-desktop" class="screenshots">
<a href="../../static/img/pwa-install.png"><img src="../../static/img/pwa-install.png"/></a>
<a href="../../static/img/pwa.png"><img src="../../static/img/pwa.png"/></a>
<a href="../../static/img/pwa-badge.png"><img src="../../static/img/pwa-badge.png"/></a>
</div>
### Safari on macOS
To install and register the web app via Safari, click on the Share menu and click Add to Dock. You need to be on macOS Sonoma (14) or higher.
<div id="pwa-screenshots-safari-desktop" class="screenshots">
<a href="../../static/img/pwa-install-macos-safari-add-to-dock.png"><img src="../../static/img/pwa-install-macos-safari-add-to-dock.png"/></a>
</div>
### Chrome/Firefox on Android
For Chrome on Android, either click the "Add to Home Screen" banner at the bottom of the screen, or select "Install app"
in the menu, and then click "Install" in the popup menu. After installation, you can find the app in your app drawer,
and on your home screen.
<div id="pwa-screenshots-chrome-android" class="screenshots">
<a href="../../static/img/pwa-install-chrome-android.jpg"><img src="../../static/img/pwa-install-chrome-android.jpg"/></a>
<a href="../../static/img/pwa-install-chrome-android-menu.jpg"><img src="../../static/img/pwa-install-chrome-android-menu.jpg"/></a>
<a href="../../static/img/pwa-install-chrome-android-popup.jpg"><img src="../../static/img/pwa-install-chrome-android-popup.jpg"/></a>
</div>
For Firefox, select "Install" in the menu, and then click "Add" to add an icon to your home screen:
<div id="pwa-screenshots-firefox-android" class="screenshots">
<a href="../../static/img/pwa-install-firefox-android-menu.jpg"><img src="../../static/img/pwa-install-firefox-android-menu.jpg"/></a>
<a href="../../static/img/pwa-install-firefox-android-popup.jpg"><img src="../../static/img/pwa-install-firefox-android-popup.jpg"/></a>
</div>
### Safari on iOS
On iOS Safari, tap on the Share menu, then tap "Add to Home Screen":
<div id="pwa-screenshots-safari-ios" class="screenshots">
<a href="../../static/img/pwa-install-safari-ios-button.jpg"><img src="../../static/img/pwa-install-safari-ios-button.jpg"/></a>
<a href="../../static/img/pwa-install-safari-ios-menu.jpg"><img src="../../static/img/pwa-install-safari-ios-menu.jpg"/></a>
<a href="../../static/img/pwa-install-safari-ios-add-icon.jpg"><img src="../../static/img/pwa-install-safari-ios-add-icon.jpg"/></a>
</div>
## Background notifications
Background notifications via web push are enabled by default and cannot be turned off when the app is installed, as notifications would
not be delivered reliably otherwise. You can mute topics you don't want to receive notifications for.
On desktop, you generally need either your browser or the web app open to receive notifications, though the ntfy tab doesn't need to be
open. On mobile, you don't need to have the web app open to receive notifications. Look at the [web docs](./web.md#background-notifications)
for a detailed breakdown.

View File

@@ -1,75 +1,27 @@
# Subscribe from the web app
The web app lets you subscribe and publish messages to ntfy topics. For ntfy.sh, the web app is available at [ntfy.sh/app](https://ntfy.sh/app).
To subscribe, simply type in the topic name and click the *Subscribe* button. **After subscribing, messages published to the topic
will appear in the web app, and pop up as a notification.**
# Subscribe from the Web UI
You can use the Web UI to subscribe to topics as well. If you do, and you keep the website open, **notifications will
pop up as desktop notifications**. Simply type in the topic name and click the *Subscribe* button. The browser will
keep a connection open and listen for incoming notifications.
<div id="subscribe-screenshots" class="screenshots">
<a href="../../static/img/web-subscribe.png"><img src="../../static/img/web-subscribe.png"/></a>
</div>
## Publish messages
To learn how to send messages, check out the [publishing page](../publish.md).
<div id="web-screenshots" class="screenshots">
<a href="../../static/img/web-detail.png"><img src="../../static/img/web-detail.png"/></a>
<a href="../../static/img/web-notification.png"><img src="../../static/img/web-notification.png"/></a>
<a href="../../static/img/web-subscribe.png"><img src="../../static/img/web-subscribe.png"/></a>
</div>
## Topic reservations
To keep receiving desktop notifications from ntfy, you need to keep the website open. What I do, and what I highly recommend,
is to pin the tab so that it's always open, but sort of out of the way:
<figure markdown>
![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>
## Notification features and browser support
- Emoji tags are supported in all browsers
- [Click](../publish.md#click-action) actions are supported in all browsers
- Only Chrome, Edge, and Opera support displaying view and http [actions](../publish.md#action-buttons) in notifications.
Their presentation is platform specific.
Note that HTTP actions are performed using fetch and thus are limited to the [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
rules, which means that any URL you include needs to respond to a [preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request)
with headers allowing the origin of the ntfy web app (`Access-Control-Allow-Origin: https://ntfy.sh`) or `*`.
- Only Chrome, Edge, and Opera support displaying [images](../publish.md#attachments) in notifications.
Look at the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API#browser_compatibility)
for more info.
## Background notifications
While subscribing, you have the option to enable background notifications on supported browsers (see "Settings" tab).
Note: If you add the web app to your homescreen (as a progressive web app, more info in the [installed web app](pwa.md)
docs), you cannot turn these off, as notifications would not be delivered reliably otherwise. You can mute topics you don't want to receive
notifications for.
**If background notifications are off:** This requires an active ntfy tab to be open to receive notifications.
These are typically instantaneous, and will appear as a system notification. If you don't see these, check that your browser
is allowed to show notifications (for example in System Settings on macOS). If you don't want to enable background notifications,
**pinning the ntfy tab on your browser** is a good solution to leave it running.
**If background notifications are on:** This uses the [Web Push API](https://caniuse.com/push-api). You don't need an active
ntfy tab open, but in some cases you may need to keep your browser open. Background notifications are only supported on the
same server hosting the web app. You cannot use another server, but can instead subscribe on the other server itself.
If the ntfy app is not opened for more than a week, background notifications will be paused. You can resume them
by opening the app again, and will get a warning notification before they are paused.
| Browser | Platform | Browser Running | Browser Not Running | Restrictions |
|---------|----------|-----------------|---------------------|---------------------------------------------------------|
| Chrome | Desktop | ✅ | ❌ | |
| Firefox | Desktop | ✅ | ❌ | |
| Edge | Desktop | ✅ | ❌ | |
| Opera | Desktop | ✅ | ❌ | |
| Safari | Desktop | ✅ | ✅ | requires Safari 16.1, macOS 13 Ventura |
| Chrome | Android | ✅ | ✅ | |
| Firefox | Android | ✅ | ✅ | |
| Safari | iOS | ⚠️ | ⚠️ | requires iOS 16.4, only when app is added to homescreen |
(Browsers below 1% usage not shown, look at the [Push API](https://caniuse.com/push-api) for more info)

View File

@@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ntfy.sh: WebSocket Example</title>
<meta name="robots" content="noindex, nofollow" />
<style>
body { font-size: 1.2em; line-height: 130%; }
#events { font-family: monospace; }
</style>
</head>
<body>
<h1>ntfy.sh: WebSocket Example</h1>
<p>
This is an example showing how to use <a href="https://ntfy.sh">ntfy.sh</a> with
<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket">WebSocket</a>.<br/>
This example doesn't need a server. You can just save the HTML page and run it from anywhere.
</p>
<button id="publishButton">Send test notification</button>
<p><b>Log:</b></p>
<div id="events"></div>
<script type="text/javascript">
const publishURL = `https://ntfy.sh/example`;
const subscribeURL = `wss://ntfy.sh/example/ws`;
const events = document.getElementById('events');
const websocket = new WebSocket(subscribeURL);
// Publish button
document.getElementById("publishButton").onclick = () => {
fetch(publishURL, {
method: 'POST', // works with PUT as well, though that sends an OPTIONS request too!
body: `It is ${new Date().toString()}. This is a test.`
})
};
// Incoming events
websocket.onopen = () => {
let event = document.createElement('div');
event.innerHTML = `WebSocket connected to ${subscribeURL}`;
events.appendChild(event);
};
websocket.onerror = (e) => {
let event = document.createElement('div');
event.innerHTML = `WebSocket error: Failed to connect to ${subscribeURL}`;
events.appendChild(event);
};
websocket.onmessage = (e) => {
let event = document.createElement('div');
event.innerHTML = e.data;
events.appendChild(event);
};
</script>
</body>
</html>

113
go.mod
View File

@@ -1,93 +1,74 @@
module heckel.io/ntfy/v2
module heckel.io/ntfy
go 1.21
toolchain go1.21.3
go 1.18
require (
cloud.google.com/go/firestore v1.15.0 // indirect
cloud.google.com/go/storage v1.39.1 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/emersion/go-smtp v0.18.0
github.com/gabriel-vasile/mimetype v1.4.3
github.com/gorilla/websocket v1.5.1
github.com/mattn/go-sqlite3 v1.14.22
github.com/olebedev/when v1.0.0
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.27.1
golang.org/x/crypto v0.21.0
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0
golang.org/x/term v0.18.0
golang.org/x/time v0.5.0
google.golang.org/api v0.170.0
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
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.3
golang.org/x/crypto v0.9.0
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.2.0
golang.org/x/term v0.8.0
golang.org/x/time v0.3.0
google.golang.org/api v0.122.0
gopkg.in/yaml.v2 v2.4.0
)
replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pin version due to breaking changes, see #839
require github.com/pkg/errors v0.9.1 // indirect
require (
firebase.google.com/go/v4 v4.13.0
github.com/SherClockHolmes/webpush-go v1.3.0
github.com/microcosm-cc/bluemonday v1.0.26
github.com/prometheus/client_golang v1.19.0
github.com/stripe/stripe-go/v74 v74.30.0
github.com/tidwall/gjson v1.17.1
firebase.google.com/go/v4 v4.11.0
github.com/prometheus/client_golang v1.15.1
github.com/stripe/stripe-go/v74 v74.18.0
)
require (
cloud.google.com/go v0.112.1 // indirect
cloud.google.com/go/compute v1.25.1 // indirect
cloud.google.com/go v0.110.2 // indirect
cloud.google.com/go/compute v1.19.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.7 // indirect
cloud.google.com/go/longrunning v0.5.6 // indirect
cloud.google.com/go/iam v1.0.1 // indirect
cloud.google.com/go/longrunning v0.4.2 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/aymerick/douceur v0.2.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-20231106173351-e73c9f7bad43 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.3 // 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/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.50.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.43.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/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/appengine/v2 v2.0.5 // indirect
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/appengine/v2 v2.0.3 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

256
go.sum
View File

@@ -1,64 +1,62 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds=
cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI=
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.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE=
cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
firebase.google.com/go/v4 v4.13.0 h1:meFz9nvDNh/FDyrEykoAzSfComcQbmnQSjoHrePRqeI=
firebase.google.com/go/v4 v4.13.0/go.mod h1:e1/gaR6EnbQfsmTnAMx1hnz+ninJIrrr/RAh59Tpfn8=
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 v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU=
cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8=
cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE=
cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ=
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.11.0 h1:szjBoiF33A2FavRLIDZjW1mw+OsW/XAtHoYNIqWOjRk=
firebase.google.com/go/v4 v4.11.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
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.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
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/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/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.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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-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-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
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/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-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI=
github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
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/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.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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=
@@ -68,18 +66,21 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
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/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/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=
@@ -87,154 +88,131 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w=
github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
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/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.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us=
github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc=
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/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
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/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/stripe/stripe-go/v74 v74.18.0 h1:ImSIoaVkTUozHxa21AhwHYBjwc8fVSJJJB1Q7oaXzIw=
github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
github.com/urfave/cli/v2 v2.25.3/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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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-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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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/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=
@@ -242,35 +220,35 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es=
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
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.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/appengine/v2 v2.0.5 h1:4C+F3Cd3L2nWEfSmFEZDPjQvDwL8T0YCeZBysZifP3k=
google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
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.3 h1:AyY/mipuqiyCIAqOevfmu5fMDc5/9P/QggWfCQYdkSA=
google.golang.org/appengine/v2 v2.0.3/go.mod h1:2Z0TTdcXxnHdXzmp8drrmOExUDM2WQgyT33c6JDUlJM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 h1:PgNlNSx2Nq2/j4juYzQBG0/Zdr+WP4z5N01Vk4VYBCY=
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237/go.mod h1:9sVD8c25Af3p0rGs7S7LLsxWKFiJt/65LdSyqXBkX/Y=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
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.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
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=
@@ -282,12 +260,12 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -3,7 +3,7 @@ package log
import (
"encoding/json"
"fmt"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
"log"
"os"
"sort"

View File

@@ -3,7 +3,7 @@ package main
import (
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/cmd"
"heckel.io/ntfy/cmd"
"os"
"runtime"
)

View File

@@ -64,40 +64,39 @@ markdown_extensions:
- attr_list
- md_in_html
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
hooks:
- docs/hooks.py
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
plugins:
- search
- minify:
minify_html: true
- mkdocs-simple-hooks:
hooks:
on_post_build: "docs.hooks:copy_fonts"
nav:
- "Getting started": index.md
- "Publishing":
- "Sending messages": publish.md
- "Subscribing":
- "From your phone": subscribe/phone.md
- "From the Web app": subscribe/web.md
- "From the Desktop": subscribe/pwa.md
- "From the CLI": subscribe/cli.md
- "Using the API": subscribe/api.md
- "Self-hosting":
- "Installation": install.md
- "Configuration": config.md
- "Other things":
- "FAQs": faq.md
- "Examples": examples.md
- "Integrations + projects": integrations.md
- "Release notes": releases.md
- "Emojis 🥳 🎉": emojis.md
- "Troubleshooting": troubleshooting.md
- "Known issues": known-issues.md
- "Deprecation notices": deprecations.md
- "Development": develop.md
- "Privacy policy": privacy.md
- "Getting started": index.md
- "Publishing":
- "Sending messages": publish.md
- "Subscribing":
- "From your phone": subscribe/phone.md
- "From the Web app": subscribe/web.md
- "From the CLI": subscribe/cli.md
- "Using the API": subscribe/api.md
- "Self-hosting":
- "Installation": install.md
- "Configuration": config.md
- "Other things":
- "FAQs": faq.md
- "Examples": examples.md
- "Integrations + projects": integrations.md
- "Release notes": releases.md
- "Emojis 🥳 🎉": emojis.md
- "Troubleshooting": troubleshooting.md
- "Known issues": known-issues.md
- "Deprecation notices": deprecations.md
- "Development": develop.md
- "Privacy policy": privacy.md

View File

@@ -1,3 +1,4 @@
# The documentation uses 'mkdocs', which is written in Python
mkdocs-material
mkdocs-minify-plugin
mkdocs-simple-hooks

View File

@@ -25,9 +25,9 @@ elif [[ "$1" == *.md ]]; then
<!-- This file was generated by scripts/emoji-convert.sh -->
You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
[tagging and emojis page](publish.md#tags-emojis).
[tagging and emojis page](../publish/#tags-emojis).
<table class=\"remove-md-box emoji-table\"><tr>
" > "$1"

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
"regexp"
"strings"
"unicode/utf8"

View File

@@ -1,41 +1,33 @@
package server
import (
"heckel.io/ntfy/user"
"io/fs"
"net/netip"
"time"
"heckel.io/ntfy/v2/user"
)
// Defines default config settings (excluding limits, see below)
const (
DefaultListenHTTP = ":80"
DefaultCacheDuration = 12 * time.Hour
DefaultCacheBatchTimeout = time.Duration(0)
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
DefaultManagerInterval = time.Minute
DefaultDelayedSenderInterval = 10 * time.Second
DefaultMessageDelayMin = 10 * time.Second
DefaultMessageDelayMax = 3 * 24 * time.Hour
DefaultMinDelay = 10 * time.Second
DefaultMaxDelay = 3 * 24 * time.Hour
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // ~control topic (Android), not too frequently to save battery
DefaultFirebasePollInterval = 20 * time.Minute // ~poll topic (iOS), max. 2-3 times per hour (see docs)
DefaultFirebaseQuotaExceededPenaltyDuration = 10 * time.Minute // Time that over-users are locked out of Firebase if it returns "quota exceeded"
DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed
)
// Defines default Web Push settings
const (
DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour
DefaultWebPushExpiryDuration = 9 * 24 * time.Hour
)
// Defines all global and per-visitor limits
// - message size limit: the max number of bytes for a message
// - total topic limit: max number of topics overall
// - various attachment limits
const (
DefaultMessageSizeLimit = 4096 // Bytes; note that FCM/APNS have a limit of ~4 KB for the entire message
DefaultMessageLengthLimit = 4096 // Bytes
DefaultTotalTopicLimit = 15000
DefaultAttachmentTotalSizeLimit = int64(5 * 1024 * 1024 * 1024) // 5 GB
DefaultAttachmentFileSizeLimit = int64(15 * 1024 * 1024) // 15 MB
@@ -123,9 +115,9 @@ type Config struct {
MetricsEnable bool
MetricsListenHTTP string
ProfileListenHTTP string
MessageDelayMin time.Duration
MessageDelayMax time.Duration
MessageSizeLimit int
MessageLimit int
MinDelay time.Duration
MaxDelay time.Duration
TotalTopicLimit int
TotalAttachmentSizeLimit int64
VisitorSubscriptionLimit int
@@ -154,13 +146,6 @@ type Config struct {
EnableMetrics bool
AccessControlAllowOrigin string // CORS header field to restrict access from web clients
Version string // injected by App
WebPushPrivateKey string
WebPushPublicKey string
WebPushFile string
WebPushEmailAddress string
WebPushStartupQueries string
WebPushExpiryDuration time.Duration
WebPushExpiryWarningDuration time.Duration
}
// NewConfig instantiates a default new server config
@@ -212,9 +197,9 @@ func NewConfig() *Config {
TwilioPhoneNumber: "",
TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests
TwilioVerifyService: "",
MessageSizeLimit: DefaultMessageSizeLimit,
MessageDelayMin: DefaultMessageDelayMin,
MessageDelayMax: DefaultMessageDelayMax,
MessageLimit: DefaultMessageLengthLimit,
MinDelay: DefaultMinDelay,
MaxDelay: DefaultMaxDelay,
TotalTopicLimit: DefaultTotalTopicLimit,
TotalAttachmentSizeLimit: 0,
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
@@ -242,11 +227,5 @@ func NewConfig() *Config {
EnableReservations: false,
AccessControlAllowOrigin: "*",
Version: "",
WebPushPrivateKey: "",
WebPushPublicKey: "",
WebPushFile: "",
WebPushEmailAddress: "",
WebPushExpiryDuration: DefaultWebPushExpiryDuration,
WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration,
}
}

View File

@@ -2,7 +2,7 @@ package server_test
import (
"github.com/stretchr/testify/assert"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/server"
"testing"
)

View File

@@ -3,7 +3,7 @@ package server
import (
"encoding/json"
"fmt"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/log"
"net/http"
)
@@ -114,11 +114,6 @@ var (
errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil}
errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil}
errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "delayed call notifications are not supported", "", nil}
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil}
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil}
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
errHTTPBadRequestTemplatedMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "", nil}
errHTTPBadRequestTemplatedMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "", nil}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
@@ -143,6 +138,5 @@ var (
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil}
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil}
errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil}
errHTTPInternalErrorWebPushUnableToPublish = &errHTTP{50004, http.StatusInternalServerError, "internal server error: unable to publish web push message", "", nil}
errHTTPInsufficientStorageUnifiedPush = &errHTTP{50701, http.StatusInsufficientStorage, "cannot publish to UnifiedPush topic without previously active subscriber", "", nil}
)

View File

@@ -3,8 +3,8 @@ package server
import (
"errors"
"fmt"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"io"
"os"
"path/filepath"

View File

@@ -4,7 +4,7 @@ import (
"bytes"
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
"os"
"strings"
"testing"

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"github.com/emersion/go-smtp"
"github.com/gorilla/websocket"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"net/http"
"strings"
"unicode/utf8"
@@ -29,7 +29,6 @@ const (
tagResetter = "resetter"
tagWebsocket = "websocket"
tagMatrix = "matrix"
tagWebPush = "webpush"
)
var (

View File

@@ -10,8 +10,8 @@ import (
"time"
_ "github.com/mattn/go-sqlite3" // SQLite driver
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
)
var (
@@ -45,7 +45,6 @@ const (
attachment_deleted INT NOT NULL,
sender TEXT NOT NULL,
user TEXT NOT NULL,
content_type TEXT NOT NULL,
encoding TEXT NOT NULL,
published INT NOT NULL
);
@@ -64,43 +63,43 @@ const (
COMMIT;
`
insertMessageQuery = `
INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
deleteMessageQuery = `DELETE FROM messages WHERE mid = ?`
updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
selectMessagesByIDQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
FROM messages
WHERE mid = ?
`
selectMessagesSinceTimeQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
FROM messages
WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
FROM messages
WHERE topic = ? AND time >= ?
ORDER BY time, id
`
selectMessagesSinceIDQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
FROM messages
WHERE topic = ? AND id > ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
FROM messages
WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY time, id
`
selectMessagesDueQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
FROM messages
WHERE time <= ? AND published = 0
ORDER BY time, id
@@ -122,7 +121,7 @@ const (
// Schema management queries
const (
currentSchemaVersion = 12
currentSchemaVersion = 11
createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
@@ -241,11 +240,6 @@ const (
);
INSERT INTO stats (key, value) VALUES ('messages', 0);
`
// 11 -> 12
migrate11To12AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN content_type TEXT NOT NULL DEFAULT('');
`
)
var (
@@ -261,7 +255,6 @@ var (
8: migrateFrom8,
9: migrateFrom9,
10: migrateFrom10,
11: migrateFrom11,
}
)
@@ -277,7 +270,7 @@ func newSqliteCache(filename, startupQueries string, cacheDuration time.Duration
if err != nil {
return nil, err
}
if err := setupMessagesDB(db, startupQueries, cacheDuration); err != nil {
if err := setupDB(db, startupQueries, cacheDuration); err != nil {
return nil, err
}
var queue *util.BatchingQueue[*message]
@@ -391,7 +384,6 @@ func (c *messageCache) addMessages(ms []*message) error {
attachmentDeleted, // Always zero
sender,
m.User,
m.ContentType,
m.Encoding,
published,
)
@@ -664,7 +656,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
func readMessage(rows *sql.Rows) (*message, error) {
var timestamp, expires, attachmentSize, attachmentExpires int64
var priority int
var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string
var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, encoding string
err := rows.Scan(
&id,
&timestamp,
@@ -684,7 +676,6 @@ func readMessage(rows *sql.Rows) (*message, error) {
&attachmentURL,
&sender,
&user,
&contentType,
&encoding,
)
if err != nil {
@@ -715,23 +706,22 @@ func readMessage(rows *sql.Rows) (*message, error) {
}
}
return &message{
ID: id,
Time: timestamp,
Expires: expires,
Event: messageEvent,
Topic: topic,
Message: msg,
Title: title,
Priority: priority,
Tags: tags,
Click: click,
Icon: icon,
Actions: actions,
Attachment: att,
Sender: senderIP, // Must parse assuming database must be correct
User: user,
ContentType: contentType,
Encoding: encoding,
ID: id,
Time: timestamp,
Expires: expires,
Event: messageEvent,
Topic: topic,
Message: msg,
Title: title,
Priority: priority,
Tags: tags,
Click: click,
Icon: icon,
Actions: actions,
Attachment: att,
Sender: senderIP, // Must parse assuming database must be correct
User: user,
Encoding: encoding,
}, nil
}
@@ -759,7 +749,7 @@ func (c *messageCache) Close() error {
return c.db.Close()
}
func setupMessagesDB(db *sql.DB, startupQueries string, cacheDuration time.Duration) error {
func setupDB(db *sql.DB, startupQueries string, cacheDuration time.Duration) error {
// Run startup queries
if startupQueries != "" {
if _, err := db.Exec(startupQueries); err != nil {
@@ -939,7 +929,7 @@ func migrateFrom9(db *sql.DB, cacheDuration time.Duration) error {
return tx.Commit()
}
func migrateFrom10(db *sql.DB, _ time.Duration) error {
func migrateFrom10(db *sql.DB, cacheDuration time.Duration) error {
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 10 to 11")
tx, err := db.Begin()
if err != nil {
@@ -954,19 +944,3 @@ func migrateFrom10(db *sql.DB, _ time.Duration) error {
}
return tx.Commit()
}
func migrateFrom11(db *sql.DB, _ time.Duration) error {
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 11 to 12")
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(migrate11To12AlterMessagesTableQuery); err != nil {
return err
}
if _, err := tx.Exec(updateSchemaVersion, 12); err != nil {
return err
}
return tx.Commit()
}

View File

@@ -9,6 +9,13 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/emersion/go-smtp"
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/sync/errgroup"
"heckel.io/ntfy/log"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"io"
"net"
"net/http"
@@ -25,15 +32,6 @@ import (
"sync"
"time"
"unicode/utf8"
"github.com/emersion/go-smtp"
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/tidwall/gjson"
"golang.org/x/sync/errgroup"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
)
// Server is the main server, providing the UI and API for ntfy
@@ -54,7 +52,6 @@ type Server struct {
messagesHistory []int64 // Last n values of the messages counter, used to determine rate
userManager *user.Manager // Might be nil!
messageCache *messageCache // Database that stores the messages
webPush *webPushStore // Database that stores web push subscriptions
fileCache *fileCache // File system based cache that stores attachments
stripe stripeAPI // Stripe API, can be replaced with a mock
priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!)
@@ -79,15 +76,11 @@ var (
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
webConfigPath = "/config.js"
webManifestPath = "/manifest.webmanifest"
webRootHTMLPath = "/app.html"
webServiceWorkerPath = "/sw.js"
accountPath = "/account"
matrixPushPath = "/_matrix/push/v1/notify"
metricsPath = "/metrics"
apiHealthPath = "/v1/health"
apiStatsPath = "/v1/stats"
apiWebPushPath = "/v1/webpush"
apiTiersPath = "/v1/tiers"
apiUsersPath = "/v1/users"
apiUsersAccessPath = "/v1/users/access"
@@ -110,8 +103,6 @@ var (
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
urlRegex = regexp.MustCompile(`^https?://`)
phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`)
templateVarRegex = regexp.MustCompile(`\${([^}]+)}`)
templateVarFormat = "${%s}"
//go:embed site
webFs embed.FS
@@ -126,12 +117,12 @@ var (
const (
firebaseControlTopic = "~control" // See Android if changed
firebasePollTopic = "~poll" // See iOS if changed (DISABLED for now)
firebasePollTopic = "~poll" // See iOS if changed
emptyMessageBody = "triggered" // Used if message body is empty
newMessageBody = "New message" // Used in poll requests as generic message
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
jsonBodyBytesLimit = 32768 // Max number of bytes for a request bodys (unless MessageLimit is higher)
jsonBodyBytesLimit = 16384 // Max number of bytes for a JSON request body
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
messagesHistoryMax = 10 // Number of message count values to keep in memory
@@ -160,13 +151,6 @@ func New(conf *Config) (*Server, error) {
if err != nil {
return nil, err
}
var webPush *webPushStore
if conf.WebPushPublicKey != "" {
webPush, err = newWebPushStore(conf.WebPushFile, conf.WebPushStartupQueries)
if err != nil {
return nil, err
}
}
topics, err := messageCache.Topics()
if err != nil {
return nil, err
@@ -206,7 +190,6 @@ func New(conf *Config) (*Server, error) {
s := &Server{
config: conf,
messageCache: messageCache,
webPush: webPush,
fileCache: fileCache,
firebaseClient: firebaseClient,
smtpSender: mailer,
@@ -359,9 +342,6 @@ func (s *Server) closeDatabases() {
s.userManager.Close()
}
s.messageCache.Close()
if s.webPush != nil {
s.webPush.Close()
}
}
// handle is the main entry point for all HTTP requests
@@ -436,8 +416,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.handleHealth(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == webManifestPath {
return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath {
return s.ensureAdmin(s.handleUsersGet)(w, r, v)
} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
@@ -492,10 +470,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberAdd)))(w, r, v)
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath {
return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberDelete)))(w, r, v)
} else if r.Method == http.MethodPost && apiWebPushPath == r.URL.Path {
return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushUpdate))(w, r, v)
} else if r.Method == http.MethodDelete && apiWebPushPath == r.URL.Path {
return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushDelete))(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath {
return s.handleStats(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath {
@@ -504,7 +478,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.handleMatrixDiscovery(w)
} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
return s.handleMetrics(w, r, v)
} else if r.Method == http.MethodGet && (staticRegex.MatchString(r.URL.Path) || r.URL.Path == webServiceWorkerPath || r.URL.Path == webRootHTMLPath) {
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
return s.ensureWebEnabled(s.handleStatic)(w, r, v)
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
@@ -578,9 +552,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
EnableCalls: s.config.TwilioAccount != "",
EnableEmails: s.config.SMTPSenderFrom != "",
EnableReservations: s.config.EnableReservations,
EnableWebPush: s.config.WebPushPublicKey != "",
BillingContact: s.config.BillingContact,
WebPushPublicKey: s.config.WebPushPublicKey,
DisallowedTopics: s.config.DisallowedTopics,
}
b, err := json.MarshalIndent(response, "", " ")
@@ -592,25 +564,6 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
return err
}
// handleWebManifest serves the web app manifest for the progressive web app (PWA)
func (s *Server) handleWebManifest(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
response := &webManifestResponse{
Name: "ntfy web",
Description: "ntfy lets you send push notifications via scripts from any computer or phone",
ShortName: "ntfy",
Scope: "/",
StartURL: s.config.WebRoot,
Display: "standalone",
BackgroundColor: "#ffffff",
ThemeColor: "#317f6f",
Icons: []*webManifestIcon{
{SRC: "/static/images/pwa-192x192.png", Sizes: "192x192", Type: "image/png"},
{SRC: "/static/images/pwa-512x512.png", Sizes: "512x512", Type: "image/png"},
},
}
return s.writeJSONWithContentType(w, response, "application/manifest+json")
}
// handleMetrics returns Prometheus metrics. This endpoint is only called if enable-metrics is set,
// and listen-metrics-http is not set.
func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visitor) error {
@@ -676,7 +629,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
// - avoid abuse (e.g. 1 uploader, 1k downloaders)
// - and also uses the higher bandwidth limits of a paying user
m, err := s.messageCache.Message(messageID)
if errors.Is(err, errMessageNotFound) {
if err == errMessageNotFound {
if s.config.CacheBatchTimeout > 0 {
// Strange edge case: If we immediately after upload request the file (the web app does this for images),
// and messages are persisted asynchronously, retry fetching from the database
@@ -736,18 +689,18 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
if err != nil {
return nil, err
}
body, err := util.Peek(r.Body, s.config.MessageSizeLimit)
body, err := util.Peek(r.Body, s.config.MessageLimit)
if err != nil {
return nil, err
}
m := newDefaultMessage(t.ID, "")
cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m)
cache, firebase, email, call, unifiedpush, e := s.parsePublishParams(r, m)
if e != nil {
return nil, e.With(t)
}
if unifiedpush && s.config.VisitorSubscriberRateLimiting && t.RateVisitor() == nil {
// UnifiedPush clients must subscribe before publishing to allow proper subscriber-based rate limiting.
// The 5xx response is because some app servers (in particular Mastodon) will remove
// UnifiedPush clients must subscribe before publishing to allow proper subscriber-based rate limiting (see
// Rate-Topics header). The 5xx response is because some app servers (in particular Mastodon) will remove
// the subscription as invalid if any 400-499 code (except 429/408) is returned.
// See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46
return nil, errHTTPInsufficientStorageUnifiedPush.With(t)
@@ -772,7 +725,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
if cache {
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
}
if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil {
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
return nil, err
}
if m.Message == "" {
@@ -810,9 +763,6 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream
go s.forwardPollRequest(v, m)
}
if s.config.WebPushPublicKey != "" {
go s.publishToWebPushEndpoints(v, m)
}
} else {
logvrm(v, r, m).Tag(tagPublish).Debug("Message delayed, will process later")
}
@@ -875,7 +825,7 @@ func (s *Server) sendToFirebase(v *visitor, m *message) {
logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
if err := s.firebaseClient.Send(v, m); err != nil {
minc(metricFirebasePublishedFailure)
if errors.Is(err, errFirebaseTemporarilyBanned) {
if err == errFirebaseTemporarilyBanned {
logvm(v, m).Tag(tagFirebase).Err(err).Debug("Unable to publish to Firebase: %v", err.Error())
} else {
logvm(v, m).Tag(tagFirebase).Err(err).Warn("Unable to publish to Firebase: %v", err.Error())
@@ -927,7 +877,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
}
}
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template bool, unifiedpush bool, err *errHTTP) {
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, unifiedpush bool, err *errHTTP) {
cache = readBoolParam(r, true, "x-cache", "cache")
firebase = readBoolParam(r, true, "x-firebase", "firebase")
m.Title = readParam(r, "x-title", "title", "t")
@@ -943,7 +893,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
}
if attach != "" {
if !urlRegex.MatchString(attach) {
return false, false, "", "", false, false, errHTTPBadRequestAttachmentURLInvalid
return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid
}
m.Attachment.URL = attach
if m.Attachment.Name == "" {
@@ -961,19 +911,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
}
if icon != "" {
if !urlRegex.MatchString(icon) {
return false, false, "", "", false, false, errHTTPBadRequestIconURLInvalid
return false, false, "", "", false, errHTTPBadRequestIconURLInvalid
}
m.Icon = icon
}
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
if s.smtpSender == nil && email != "" {
return false, false, "", "", false, false, errHTTPBadRequestEmailDisabled
return false, false, "", "", false, errHTTPBadRequestEmailDisabled
}
call = readParam(r, "x-call", "call")
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled
return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid
return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid
}
messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
if messageStr != "" {
@@ -982,27 +932,27 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
var e error
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
if e != nil {
return false, false, "", "", false, false, errHTTPBadRequestPriorityInvalid
return false, false, "", "", false, errHTTPBadRequestPriorityInvalid
}
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
if delayStr != "" {
if !cache {
return false, false, "", "", false, false, errHTTPBadRequestDelayNoCache
return false, false, "", "", false, errHTTPBadRequestDelayNoCache
}
if email != "" {
return false, false, "", "", false, false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
}
if call != "" {
return false, false, "", "", false, false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
return false, false, "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
}
delay, err := util.ParseFutureTime(delayStr, time.Now())
if err != nil {
return false, false, "", "", false, false, errHTTPBadRequestDelayCannotParse
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
return false, false, "", "", false, false, errHTTPBadRequestDelayTooSmall
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
return false, false, "", "", false, false, errHTTPBadRequestDelayTooLarge
return false, false, "", "", false, errHTTPBadRequestDelayCannotParse
} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
return false, false, "", "", false, errHTTPBadRequestDelayTooSmall
} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
return false, false, "", "", false, errHTTPBadRequestDelayTooLarge
}
m.Time = delay.Unix()
}
@@ -1010,14 +960,9 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
if actionsStr != "" {
m.Actions, e = parseActions(actionsStr)
if e != nil {
return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
}
}
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
if markdown || strings.ToLower(contentType) == "text/markdown" {
m.ContentType = "text/markdown"
}
template = readBoolParam(r, false, "x-template", "template", "tpl")
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
if unifiedpush {
firebase = false
@@ -1029,7 +974,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
cache = false
email = ""
}
return cache, firebase, email, call, template, unifiedpush, nil
return cache, firebase, email, call, unifiedpush, nil
}
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
@@ -1037,18 +982,16 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
// If a message is flagged as poll request, the body does not matter and is discarded
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
// If UnifiedPush is enabled, encode as base64 if body is binary, and do not trim
// If body is binary, encode as base64, if not do not encode
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
// Body must be a message, because we attached an external URL
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
// Body must be attachment, because we passed a filename
// 5. curl -H "Template: yes" -T file.txt ntfy.sh/mytopic
// If templating is enabled, read up to 32k and treat message body as JSON
// 6. curl -T file.txt ntfy.sh/mytopic
// 5. curl -T file.txt ntfy.sh/mytopic
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
// 7. curl -T file.txt ntfy.sh/mytopic
// In all other cases, mostly if file.txt is > message limit, treat it as an attachment
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template, unifiedpush bool) error {
// 6. curl -T file.txt ntfy.sh/mytopic
// If file.txt is > message limit, treat it as an attachment
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error {
if m.Event == pollRequestEvent { // Case 1
return s.handleBodyDiscard(body)
} else if unifiedpush {
@@ -1057,12 +1000,10 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body
return s.handleBodyAsTextMessage(m, body) // Case 3
} else if m.Attachment != nil && m.Attachment.Name != "" {
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
} else if template {
return s.handleBodyAsTemplatedTextMessage(m, body) // Case 5
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
return s.handleBodyAsTextMessage(m, body) // Case 6
return s.handleBodyAsTextMessage(m, body) // Case 5
}
return s.handleBodyAsAttachment(r, v, m, body) // Case 7
return s.handleBodyAsAttachment(r, v, m, body) // Case 6
}
func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error {
@@ -1094,35 +1035,6 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser
return nil
}
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error {
body, err := util.Peek(body, jsonBodyBytesLimit)
if err != nil {
return err
} else if body.LimitReached {
return errHTTPEntityTooLargeJSONBody
}
peekedBody := strings.TrimSpace(string(body.PeekedBytes))
if !gjson.Valid(peekedBody) {
return errHTTPBadRequestTemplatedMessageNotJSON
}
m.Message = replaceGJSONTemplate(m.Message, peekedBody)
m.Title = replaceGJSONTemplate(m.Title, peekedBody)
if len(m.Message) > s.config.MessageSizeLimit {
return errHTTPBadRequestTemplatedMessageTooLarge
}
return nil
}
func replaceGJSONTemplate(template string, source string) string {
matches := templateVarRegex.FindAllStringSubmatch(template, -1)
for _, m := range matches {
if result := gjson.Get(source, m[1]); result.Exists() {
template = strings.ReplaceAll(template, fmt.Sprintf(templateVarFormat, m[1]), result.String())
}
}
return template
}
func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error {
if s.fileCache == nil || s.config.BaseURL == "" || s.config.AttachmentCacheDir == "" {
return errHTTPBadRequestAttachmentsDisallowed.With(m)
@@ -1165,7 +1077,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
util.NewFixedLimiter(vinfo.Stats.AttachmentTotalSizeRemaining),
}
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...)
if errors.Is(err, util.ErrLimitReached) {
if err == util.ErrLimitReached {
return errHTTPEntityTooLargeAttachment.With(m)
} else if err != nil {
return err
@@ -1219,7 +1131,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
if err != nil {
return err
}
poll, since, scheduled, filters, err := parseSubscribeParams(r)
poll, since, scheduled, filters, rateTopics, err := parseSubscribeParams(r)
if err != nil {
return err
}
@@ -1249,7 +1161,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
}
return nil
}
if err := s.maybeSetRateVisitors(r, v, topics); err != nil {
if err := s.maybeSetRateVisitors(r, v, topics, rateTopics); err != nil {
return err
}
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
@@ -1315,7 +1227,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
if err != nil {
return err
}
poll, since, scheduled, filters, err := parseSubscribeParams(r)
poll, since, scheduled, filters, rateTopics, err := parseSubscribeParams(r)
if err != nil {
return err
}
@@ -1401,7 +1313,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
}
return conn.WriteJSON(msg)
}
if err := s.maybeSetRateVisitors(r, v, topics); err != nil {
if err := s.maybeSetRateVisitors(r, v, topics, rateTopics); err != nil {
return err
}
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
@@ -1434,7 +1346,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
return err
}
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) {
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, rateTopics []string, err error) {
poll = readBoolParam(r, false, "x-poll", "poll", "po")
scheduled = readBoolParam(r, false, "x-scheduled", "scheduled", "sched")
since, err = parseSince(r, poll)
@@ -1445,6 +1357,7 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
if err != nil {
return
}
rateTopics = readCommaSeparatedParam(r, "x-rate-topics", "rate-topics")
return
}
@@ -1456,8 +1369,9 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
// - or the topic is reserved, and v.user is the owner
// - or the topic is not reserved, and v.user has write access
//
// This only applies to UnifiedPush topics ("up...").
func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*topic) error {
// Note: This TEMPORARILY also registers all topics starting with "up" (= UnifiedPush). This is to ease the transition
// until the Android app will send the "Rate-Topics" header.
func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*topic, rateTopics []string) error {
// Bail out if not enabled
if !s.config.VisitorSubscriberRateLimiting {
return nil
@@ -1466,7 +1380,7 @@ func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*top
// Make a list of topics that we'll actually set the RateVisitor on
eligibleRateTopics := make([]*topic, 0)
for _, t := range topics {
if strings.HasPrefix(t.ID, unifiedPushTopicPrefix) && len(t.ID) == unifiedPushTopicLength {
if (strings.HasPrefix(t.ID, unifiedPushTopicPrefix) && len(t.ID) == unifiedPushTopicLength) || util.Contains(rateTopics, t.ID) {
eligibleRateTopics = append(eligibleRateTopics, t)
}
}
@@ -1778,9 +1692,6 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
if s.config.UpstreamBaseURL != "" {
go s.forwardPollRequest(v, m)
}
if s.config.WebPushPublicKey != "" {
go s.publishToWebPushEndpoints(v, m)
}
if err := s.messageCache.MarkPublished(m); err != nil {
return err
}
@@ -1791,7 +1702,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
// before passing it on to the next handler. This is meant to be used in combination with handlePublish.
func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageSizeLimit*2, false) // 2x to account for JSON format overhead
m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageLimit*2, false) // 2x to account for JSON format overhead
if err != nil {
return err
}
@@ -1824,9 +1735,6 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
if m.Icon != "" {
r.Header.Set("X-Icon", m.Icon)
}
if m.Markdown {
r.Header.Set("X-Markdown", "yes")
}
if len(m.Actions) > 0 {
actionsStr, err := json.Marshal(m.Actions)
if err != nil {
@@ -1849,7 +1757,7 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
func (s *Server) transformMatrixJSON(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
newRequest, err := newRequestFromMatrixJSON(r, s.config.BaseURL, s.config.MessageSizeLimit)
newRequest, err := newRequestFromMatrixJSON(r, s.config.BaseURL, s.config.MessageLimit)
if err != nil {
logvr(v, r).Tag(tagMatrix).Err(err).Debug("Invalid Matrix request")
if e, ok := err.(*errMatrixPushkeyRejected); ok {
@@ -2004,11 +1912,7 @@ func (s *Server) visitor(ip netip.Addr, user *user.User) *visitor {
}
func (s *Server) writeJSON(w http.ResponseWriter, v any) error {
return s.writeJSONWithContentType(w, v, "application/json")
}
func (s *Server) writeJSONWithContentType(w http.ResponseWriter, v any, contentType string) error {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
if err := json.NewEncoder(w).Encode(v); err != nil {
return err

View File

@@ -144,27 +144,6 @@
# smtp-server-domain:
# smtp-server-addr-prefix:
# Web Push support (background notifications for browsers)
#
# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users
# can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push
# endpoint, which will then forward it to the browser.
#
# You must configure web-push-public/private key, web-push-file, and web-push-email-address below to enable Web Push.
# Run "ntfy webpush keys" to generate the keys.
#
# - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
# - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
# - web-push-email-address is the admin email address send to the push provider, e.g. `sysadmin@example.com`
# - web-push-startup-queries is an optional list of queries to run on startup`
#
# web-push-public-key:
# web-push-private-key:
# web-push-file:
# web-push-email-address:
# web-push-startup-queries:
# If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header.
#
# - twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
@@ -236,16 +215,6 @@
# upstream-base-url:
# upstream-access-token:
# Configures message-specific limits
#
# - message-size-limit defines the max size of a message body. Please note message sizes >4K are NOT RECOMMENDED,
# and largely untested. If FCM and/or APNS is used, the limit should stay 4K, because their limits are around that size.
# If you increase this size limit regardless, FCM and APNS will NOT work for large messages.
# - message-delay-limit defines the max delay of a message when using the "Delay" header.
#
# message-size-limit: "4k"
# message-delay-limit: "3d"
# Rate limiting: Total number of topics before the server rejects new topics.
#
# global-topic-limit: 15000
@@ -287,14 +256,15 @@
# Rate limiting: Enable subscriber-based rate limiting (mostly used for UnifiedPush)
#
# If subscriber-based rate limiting is enabled, messages published on UnifiedPush topics** (topics starting with "up")
# will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.
# 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 subscribing to UnifiedPush topics via HTTP stream, or websockets, will be automatically registered as
# a "rate visitor", i.e. the visitor whose rate limits will be used when publishing on this topic. Note that setting the rate visitor
# requires **read-write permission** on the topic.
# 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: Setting the rate visitor requires READ-WRITE permission on the topic.
#
# If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if
# UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if
# no "rate visitor" has been previously registered. This is to avoid burning the publisher's "visitor-message-daily-limit".
#
# visitor-subscriber-rate-limiting: false
@@ -351,10 +321,6 @@
# - "field -> level" to match any value, e.g. "time_taken_ms -> debug"
# Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging.
#
# Check your permissions:
# If you are running ntfy with systemd, make sure this log file is owned by the
# ntfy user and group by running: chown ntfy.ntfy <filename>.
#
# Example (good for production):
# log-level: info
# log-format: json

View File

@@ -2,9 +2,9 @@ package server
import (
"encoding/json"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/log"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"net/http"
"net/netip"
"strings"
@@ -170,11 +170,6 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *
if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil {
return errHTTPBadRequestIncorrectPasswordConfirmation
}
if s.webPush != nil && u.ID != "" {
if err := s.webPush.RemoveSubscriptionsByUserID(u.ID); err != nil {
logvr(v, r).Err(err).Warn("Error removing web push subscriptions for %s", u.Name)
}
}
if u.Billing.StripeSubscriptionID != "" {
logvr(v, r).Tag(tagStripe).Info("Canceling billing subscription for user %s", u.Name)
if _, err := s.stripe.CancelSubscription(u.Billing.StripeSubscriptionID); err != nil {

View File

@@ -3,9 +3,9 @@ package server
import (
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/log"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"io"
"net/netip"
"path/filepath"
@@ -718,11 +718,11 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "starter",
MessageSizeLimit: 10,
MessageLimit: 10,
}))
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "pro",
MessageSizeLimit: 20,
MessageLimit: 20,
}))
require.Nil(t, s.userManager.ChangeTier("phil", "starter"))

View File

@@ -1,8 +1,7 @@
package server
import (
"errors"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/user"
"net/http"
)
@@ -46,7 +45,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
return errHTTPBadRequest.Wrap("username invalid, or password missing")
}
u, err := s.userManager.User(req.Username)
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
if err != nil && err != user.ErrUserNotFound {
return err
} else if u != nil {
return errHTTPConflictUserExists
@@ -54,7 +53,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
var tier *user.Tier
if req.Tier != "" {
tier, err = s.userManager.Tier(req.Tier)
if errors.Is(err, user.ErrTierNotFound) {
if err == user.ErrTierNotFound {
return errHTTPBadRequestTierInvalid
} else if err != nil {
return err
@@ -77,7 +76,7 @@ func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *vi
return err
}
u, err := s.userManager.User(req.Username)
if errors.Is(err, user.ErrUserNotFound) {
if err == user.ErrUserNotFound {
return errHTTPBadRequestUserNotFound
} else if err != nil {
return err
@@ -99,7 +98,7 @@ func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *vi
return err
}
_, err = s.userManager.User(req.Username)
if errors.Is(err, user.ErrUserNotFound) {
if err == user.ErrUserNotFound {
return errHTTPBadRequestUserNotFound
} else if err != nil {
return err

View File

@@ -2,8 +2,8 @@ package server
import (
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"sync/atomic"
"testing"
"time"

View File

@@ -8,8 +8,8 @@ import (
"firebase.google.com/go/v4/messaging"
"fmt"
"google.golang.org/api/option"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"strings"
)
@@ -144,18 +144,17 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro
}
if allowForward {
data = map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
"priority": fmt.Sprintf("%d", m.Priority),
"tags": strings.Join(m.Tags, ","),
"click": m.Click,
"icon": m.Icon,
"title": m.Title,
"message": m.Message,
"content_type": m.ContentType,
"encoding": m.Encoding,
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
"priority": fmt.Sprintf("%d", m.Priority),
"tags": strings.Join(m.Tags, ","),
"click": m.Click,
"icon": m.Icon,
"title": m.Title,
"message": m.Message,
"encoding": m.Encoding,
}
if len(m.Actions) > 0 {
actions, err := json.Marshal(m.Actions)

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/user"
"net/netip"
"strings"
"sync"
@@ -182,7 +182,6 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
"title": "some title",
"message": "this is a message",
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
"content_type": "",
"encoding": "",
"attachment_name": "some file.jpg",
"attachment_type": "image/jpeg",
@@ -204,7 +203,6 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
"title": "some title",
"message": "this is a message",
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
"content_type": "",
"encoding": "",
"attachment_name": "some file.jpg",
"attachment_type": "image/jpeg",

View File

@@ -1,8 +1,8 @@
package server
import (
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"strings"
)
@@ -15,7 +15,6 @@ func (s *Server) execManager() {
s.pruneTokens()
s.pruneAttachments()
s.pruneMessages()
s.pruneAndNotifyWebPushSubscriptions()
// Message count per topic
var messagesCached int

View File

@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
"io"
"net/http"
"strings"

View File

@@ -3,7 +3,7 @@ package server
import (
"net/http"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
)
type contextKey int
@@ -58,15 +58,6 @@ func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
}
}
func (s *Server) ensureWebPushEnabled(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if s.config.WebRoot == "" || s.config.WebPushPublicKey == "" {
return errHTTPNotFound
}
return next(w, r, v)
}
}
func (s *Server) ensureUserManager(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if s.userManager == nil {

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