Compare commits

...

48 Commits

Author SHA1 Message Date
Philipp Heckel
b2c2bd1e4b Remove "poll" alias for X-Poll-ID 2022-05-28 22:06:46 -04:00
Philipp Heckel
b003d79ae4 Bump version 2022-05-28 22:02:13 -04:00
Philipp Heckel
a52b024807 Update npm 2022-05-28 20:24:19 -04:00
Philipp Heckel
12b83828bd Docs 2022-05-28 20:20:46 -04:00
Philipp Heckel
96bb357435 Polish the poll_request stuff 2022-05-27 20:30:20 -04:00
Philipp Heckel
6a43c1a126 WIP: iOS poll_request forwarder 2022-05-27 07:55:57 -04:00
Philipp Heckel
4dabc56952 Subscribe filter for querying by ID 2022-05-26 18:52:55 -04:00
Philipp Heckel
5e510a19a1 Update deps 2022-05-26 16:50:36 -04:00
Philipp Heckel
b627a327d1 Add Italian, release notes 2022-05-26 16:38:09 -04:00
Philipp Heckel
0b38efd761 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-05-26 16:27:02 -04:00
Philipp Heckel
983dec801a Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-05-26 16:26:58 -04:00
Philipp Heckel
01eeb71b9d Update the docs 2022-05-26 16:22:47 -04:00
Adriel Sand
6ba1d7b2a5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.4% (188 of 189 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2022-05-26 15:15:10 +02:00
Philipp Heckel
ff202a042b Update deps 2022-05-25 21:41:14 -04:00
Philipp Heckel
af76a2606d Support for Firebase ~poll keepalive topic that wakes up iOS devices every 20 minutes 2022-05-25 21:39:46 -04:00
Philipp Heckel
98b56c2f06 Changelog 2022-05-24 22:30:12 -04:00
Philipp Heckel
b6afa2fd49 Changelog 2022-05-24 20:47:53 -04:00
Michelangelo Camaioni
e1c07228e5 Translated using Weblate (Italian)
Currently translated at 100.0% (189 of 189 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/
2022-05-24 13:15:08 +02:00
Philipp Heckel
a949748d91 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-05-23 10:54:09 -04:00
Michelangelo Camaioni
125fcd85bb Added translation using Weblate (Italian) 2022-05-23 11:18:53 +02:00
Philipp Heckel
2abd6a57ee Support emails without Content-Type, closes #265 2022-05-21 20:20:44 -04:00
Philipp Heckel
35a691a1bc updated release notes 2022-05-21 12:10:45 -04:00
Philipp Heckel
2bf6b8bb28 Docs and bump version 2022-05-21 11:45:11 -04:00
Philipp C. Heckel
cb82e2690c Merge pull request #264 from Fallenbagel/patch-2
Add webhook example for Jellyseerr/Overseerr
2022-05-21 11:10:18 -04:00
Philipp Heckel
ab1f9220a3 Release log 2022-05-21 10:13:41 -04:00
Philipp Heckel
4c5d40e4c9 Fix make targets to actually work on macOS 2022-05-21 10:08:33 -04:00
Philipp Heckel
c33065151e Switch to build tags "noserver" instead of using _linux suffix 2022-05-21 09:46:49 -04:00
Philipp Heckel
ab01d0f04e Merge branch 'macos-server-ORIGIN' into macos-server 2022-05-21 09:35:23 -04:00
Philipp Heckel
42c3c6eb29 Re-add simple target to be able to build on macOS 2022-05-21 09:34:53 -04:00
Philipp Heckel
da7a1f656f Merge branch 'main' into macos-server-ORIGIN 2022-05-21 08:58:59 -04:00
Henrique Pires
63719ca0a0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.9% (187 of 189 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2022-05-21 01:17:08 +02:00
Philipp Heckel
cd27d47f4b APNs data 2022-05-20 15:59:58 -04:00
Philipp Heckel
60c5ccf34b Merge branch 'main' of github.com:binwiederhier/ntfy into macos-server 2022-05-20 15:59:12 -04:00
Fallenbagel
d819de2626 Add webhook example for Jellyseerr/Overseerr
Add webhook json payload example for jellyseerr/overseerr.
2022-05-20 04:33:40 +05:00
Shoshin Akamine
79cb082879 Translated using Weblate (Japanese)
Currently translated at 100.0% (189 of 189 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/
2022-05-19 19:16:07 +02:00
Rogelio Dominguez
632bf8d0b6 Translated using Weblate (Spanish)
Currently translated at 100.0% (189 of 189 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-05-19 19:16:06 +02:00
Philipp Heckel
5e1d1698ff Changelog 2022-05-17 09:58:56 -04:00
Philipp Heckel
396497fff7 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-05-17 09:19:46 -04:00
Philipp C. Heckel
94bfe029d5 Merge pull request #262 from MayeulC/patch-1
Docs/config: fix typo in private server example
2022-05-17 09:19:15 -04:00
Mayeul Cantan
7f736eb93e Docs/config: fix typo in private server example
The `/etc/ntfy/server.yml` example was invalid yaml previously.
2022-05-17 09:54:34 +02:00
Philipp Heckel
414e283b46 Update develop instructions for Android 2022-05-16 14:53:51 -04:00
Philipp Heckel
a52e628f7b Changelog 2022-05-16 13:59:48 -04:00
Philipp Heckel
3eea97109e Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-05-16 13:58:49 -04:00
Philipp C. Heckel
1950fc518f Merge pull request #257 from oddlama/disable-makefile-paralellism
Force MAKEFLAGS to --jobs=1 to ensure dependencies are executed sequentially and in correct order
2022-05-16 13:58:36 -04:00
oddlama
6c2aa0c3c2 Force MAKEFLAGS to --jobs=1 to ensure dependencies are executed
sequentially and in-order.

If this is not set, make -j2 web or higher job counts will
cause the build to fail as some dependencies are not expressed
directly on the dependent tasks but as a dependency list
on a parent task.

Alternatively one could add the required dependencies for each
task separately, but that would factually sequentiallize the
build, so there's no real difference except this approach
fixes all dependency chains globally.
2022-05-16 15:41:23 +02:00
Philipp Heckel
891257cce8 Merge branch 'main' into macos-server 2022-05-13 14:59:35 -04:00
Philipp Heckel
a2825880bc macOS server support, this is just to support iOS development, not for prod 2022-05-11 13:47:41 -04:00
Philipp Heckel
577cd0fcea ios 2022-05-11 13:29:23 -04:00
35 changed files with 2416 additions and 1471 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,7 @@
dist/
build/
.idea/
.vscode/
*.swp
server/docs/
server/site/
@@ -9,3 +10,4 @@ playground/
secrets/
*.iml
node_modules/
.DS_Store

View File

@@ -59,6 +59,7 @@ builds:
binary: ntfy
env:
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
tags: [noserver] # don't include server files
ldflags:
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [windows]
@@ -71,6 +72,7 @@ builds:
binary: ntfy
env:
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
tags: [noserver] # don't include server files
ldflags:
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [darwin]

122
Makefile
View File

@@ -1,66 +1,72 @@
MAKEFLAGS := --jobs=1
VERSION := $(shell git describe --tag)
.PHONY:
help:
@echo "Typical commands (more see below):"
@echo " make build - Build web app, documentation and server/client (sloowwww)"
@echo " make cli-linux-amd64 - Build server/client binary (amd64, no web app or docs)"
@echo " make install-linux-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)"
@echo " make web - Build the web app"
@echo " make docs - Build the documentation"
@echo " make check - Run all tests, vetting/formatting checks and linters"
@echo " make build - Build web app, documentation and server/client (sloowwww)"
@echo " make cli-linux-amd64 - Build server/client binary (amd64, no web app or docs)"
@echo " make install-linux-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)"
@echo " make web - Build the web app"
@echo " make docs - Build the documentation"
@echo " make check - Run all tests, vetting/formatting checks and linters"
@echo
@echo "Build everything:"
@echo " make build - Build web app, documentation and server/client"
@echo " make clean - Clean build/dist folders"
@echo " make build - Build web app, documentation and server/client"
@echo " make clean - Clean build/dist folders"
@echo
@echo "Build server & client (not release version):"
@echo " make cli - Build server & client (all architectures)"
@echo " make cli-linux-amd64 - Build server & client (Linux, amd64 only)"
@echo " make cli-linux-armv6 - Build server & client (Linux, armv6 only)"
@echo " make cli-linux-armv7 - Build server & client (Linux, armv7 only)"
@echo " make cli-linux-arm64 - Build server & client (Linux, arm64 only)"
@echo " make cli-windows-amd64 - Build client (Windows, amd64 only)"
@echo " make cli-darwin-amd64 - Build client (macOS, amd64 only)"
@echo "Build server & client (using GoReleaser, not release version):"
@echo " make cli - Build server & client (all architectures)"
@echo " make cli-linux-amd64 - Build server & client (Linux, amd64 only)"
@echo " make cli-linux-armv6 - Build server & client (Linux, armv6 only)"
@echo " make cli-linux-armv7 - Build server & client (Linux, armv7 only)"
@echo " make cli-linux-arm64 - Build server & client (Linux, arm64 only)"
@echo " make cli-windows-amd64 - Build client (Windows, amd64 only)"
@echo " make cli-darwin-all - Build client (macOS, arm64+amd64 universal binary)"
@echo
@echo "Build server & client (without GoReleaser):"
@echo " make cli-linux-server - Build client & server (no GoReleaser, current arch, Linux)"
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
@echo
@echo "Build web app:"
@echo " make web - Build the web app"
@echo " make web-deps - Install web app dependencies (npm install the universe)"
@echo " make web-build - Actually build the web app"
@echo " make web - Build the web app"
@echo " make web-deps - Install web app dependencies (npm install the universe)"
@echo " make web-build - Actually build the web app"
@echo
@echo "Build documentation:"
@echo " make docs - Build the documentation"
@echo " make docs-deps - Install Python dependencies (pip3 install)"
@echo " make docs-build - Actually build the documentation"
@echo " make docs - Build the documentation"
@echo " make docs-deps - Install Python dependencies (pip3 install)"
@echo " make docs-build - Actually build the documentation"
@echo
@echo "Test/check:"
@echo " make test - Run tests"
@echo " make race - Run tests with -race flag"
@echo " make coverage - Run tests and show coverage"
@echo " make coverage-html - Run tests and show coverage (as HTML)"
@echo " make coverage-upload - Upload coverage results to codecov.io"
@echo " make test - Run tests"
@echo " make race - Run tests with -race flag"
@echo " make coverage - Run tests and show coverage"
@echo " make coverage-html - Run tests and show coverage (as HTML)"
@echo " make coverage-upload - Upload coverage results to codecov.io"
@echo
@echo "Lint/format:"
@echo " make fmt - Run 'go fmt'"
@echo " make fmt-check - Run 'go fmt', but don't change anything"
@echo " make vet - Run 'go vet'"
@echo " make lint - Run 'golint'"
@echo " make staticcheck - Run 'staticcheck'"
@echo " make fmt - Run 'go fmt'"
@echo " make fmt-check - Run 'go fmt', but don't change anything"
@echo " make vet - Run 'go vet'"
@echo " make lint - Run 'golint'"
@echo " make staticcheck - Run 'staticcheck'"
@echo
@echo "Releasing:"
@echo " make release - Create a release"
@echo " make release-snapshot - Create a test release"
@echo " make release - Create a release"
@echo " make release-snapshot - Create a test release"
@echo
@echo "Install locally (requires sudo):"
@echo " make install-linux-amd64 - Copy amd64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-linux-armv6 - Copy armv6 binary from dist/ to /usr/bin/ntfy"
@echo " make install-linux-armv7 - Copy armv7 binary from dist/ to /usr/bin/ntfy"
@echo " make install-linux-arm64 - Copy arm64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-linux-deb-amd64 - Install .deb from dist/ (amd64 only)"
@echo " make install-linux-deb-armv6 - Install .deb from dist/ (armv6 only)"
@echo " make install-linux-deb-armv7 - Install .deb from dist/ (armv7 only)"
@echo " make install-linux-deb-arm64 - Install .deb from dist/ (arm64 only)"
@echo " make install-linux-amd64 - Copy amd64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-linux-armv6 - Copy armv6 binary from dist/ to /usr/bin/ntfy"
@echo " make install-linux-armv7 - Copy armv7 binary from dist/ to /usr/bin/ntfy"
@echo " make install-linux-arm64 - Copy arm64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-linux-deb-amd64 - Install .deb from dist/ (amd64 only)"
@echo " make install-linux-deb-armv6 - Install .deb from dist/ (armv6 only)"
@echo " make install-linux-deb-armv7 - Install .deb from dist/ (armv7 only)"
@echo " make install-linux-deb-arm64 - Install .deb from dist/ (arm64 only)"
# Building everything
@@ -71,7 +77,7 @@ clean: .PHONY
build: web docs cli
update: web-deps-update cli-deps-update docs-deps-update
docker pull alpine
# Documentation
@@ -131,6 +137,36 @@ cli-windows-amd64: cli-deps-static-sites
cli-darwin-all: cli-deps-static-sites
goreleaser build --snapshot --rm-dist --debug --id ntfy_darwin_all
cli-linux-server: cli-deps-static-sites
# This is a target to build the CLI (including the server) manually.
# Use this for development, if you really don't want to install GoReleaser ...
mkdir -p dist/ntfy_linux_server server/docs
CGO_ENABLED=1 go build \
-o dist/ntfy_linux_server/ntfy \
-tags sqlite_omit_load_extension,osusergo,netgo \
-ldflags \
"-linkmode=external -extldflags=-static -s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
cli-darwin-server: cli-deps-static-sites
# This is a target to build the CLI (including the server) manually.
# Use this for macOS/iOS development, so you have a local server to test with.
mkdir -p dist/ntfy_darwin_server server/docs
CGO_ENABLED=1 go build \
-o dist/ntfy_darwin_server/ntfy \
-tags sqlite_omit_load_extension,osusergo,netgo \
-ldflags \
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
cli-client: cli-deps-static-sites
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
# Use this for development, if you really don't want to install GoReleaser ...
mkdir -p dist/ntfy_client server/docs
CGO_ENABLED=0 go build \
-o dist/ntfy_client/ntfy \
-tags noserver \
-ldflags \
"-X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc
cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64

View File

@@ -1,3 +1,5 @@
//go:build !noserver
package cmd
import (

View File

@@ -1,3 +1,5 @@
//go:build !noserver
package cmd
import (
@@ -39,6 +41,7 @@ var flagsServe = []cli.Flag{
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.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
@@ -100,6 +103,7 @@ func execServe(c *cli.Context) error {
keepaliveInterval := c.Duration("keepalive-interval")
managerInterval := c.Duration("manager-interval")
webRoot := c.String("web-root")
upstreamBaseURL := c.String("upstream-base-url")
smtpSenderAddr := c.String("smtp-sender-addr")
smtpSenderUser := c.String("smtp-sender-user")
smtpSenderPass := c.String("smtp-sender-pass")
@@ -145,6 +149,10 @@ func execServe(c *cli.Context) error {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
} else if !util.InStringList([]string{"app", "home", "disable"}, webRoot) {
return errors.New("if set, web-root must be 'home' or 'app'")
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
return errors.New("if set, upstream-base-url must start with http:// or https://")
} else if upstreamBaseURL != "" && baseURL == "" {
return errors.New("if upstream-base-url is set, base-url must also be set")
}
webRootIsApp := webRoot == "app"
@@ -213,6 +221,7 @@ func execServe(c *cli.Context) error {
conf.KeepaliveInterval = keepaliveInterval
conf.ManagerInterval = managerInterval
conf.WebRootIsApp = webRootIsApp
conf.UpstreamBaseURL = upstreamBaseURL
conf.SMTPSenderAddr = smtpSenderAddr
conf.SMTPSenderUser = smtpSenderUser
conf.SMTPSenderPass = smtpSenderPass

View File

@@ -1,3 +1,5 @@
//go:build !noserver
package cmd
import (

View File

@@ -227,7 +227,7 @@ The easiest way to configure a private instance is to set `auth-default-access`
=== "/etc/ntfy/server.yml"
``` yaml
auth-file "/var/lib/ntfy/user.db"
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
```
@@ -618,6 +618,35 @@ Example:
firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
```
## iOS instant notifications
Unlike Android, iOS heavily restricts background processing, which sadly makes it impossible to implement instant
push notifications without a central server.
To still support instant notifications on iOS through your self-hosted ntfy server, you have to forward so called `poll_request`
messages to the main ntfy.sh server (or any upstream server that's APNS/Firebase connected, if you build your own iOS app),
which will then forward it to Firebase/APNS.
To configure it, simply set `upstream-base-url` like so:
``` yaml
upstream-base-url: "https://ntfy.sh"
```
If set, all incoming messages will publish a poll request to the configured upstream server, containing
the message ID of the original message, instructing the iOS app to poll this server for the actual message contents.
If `upstream-base-url` is not set, notifications will still eventually get to your device, but delivery can take hours,
depending on the state of th phone. If you are using your phone, it shouldn't take more than 20-30 minutes though.
In case you're curious, here's an example of the entire flow:
- In the iOS app, you subscribe to `https://ntfy.example.com/mytopic`
- The app subscribes to the Firebase topic `6de73be8dfb7d69e...` (the SHA256 of the topic URL)
- When you publish a message to `https://ntfy.example.com/mytopic`, your ntfy server will publish a
poll request to `https://ntfy.sh/6de73be8dfb7d69e...` (passing the message ID in the `X-Poll-ID` header)
- The ntfy.sh server publishes the message to Firebase, which forwards it to APNS, which forwards it to your iOS device
- Your iOS device receives the poll request, and fetches the actual message from your server, and then displays it
## Rate limiting
!!! info
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.

View File

@@ -162,13 +162,14 @@ To build only the `ntfy` binary **without the web app or documentation**, use th
``` shell
$ make
Build server & client (not release version):
make cli - Build server & client (all architectures)
make cli-linux-amd64 - Build server & client (Linux, amd64 only)
make cli-linux-armv6 - Build server & client (Linux, armv6 only)
make cli-linux-armv7 - Build server & client (Linux, armv7 only)
make cli-linux-arm64 - Build server & client (Linux, arm64 only)
make cli-windows-amd64 - Build client (Windows, amd64 only)
Build server & client (using GoReleaser, not release version):
make cli - Build server & client (all architectures)
make cli-linux-amd64 - Build server & client (Linux, amd64 only)
make cli-linux-armv6 - Build server & client (Linux, armv6 only)
make cli-linux-armv7 - Build server & client (Linux, armv7 only)
make cli-linux-arm64 - Build server & client (Linux, arm64 only)
make cli-windows-amd64 - Build client (Windows, amd64 only)
make cli-darwin-all - Build client (macOS, arm64+amd64 universal binary)
```
So if you're on an amd64/x86_64-based machine, you may just want to run `make cli-linux-amd64` during testing. On a modern
@@ -199,8 +200,10 @@ server/server.go:85:13: pattern docs: no matching files found
This is because we use `go:embed` to embed the documentation and web app, so the Go code expects files to be
present at `server/docs` and `server/site`. If they are not, you'll see the above error. The `cli-deps-static-sites`
target creates dummy files that ensures that you'll be able to build.
target creates dummy files that ensure that you'll be able to build.
While not officially supported (or released), you can build and run the server **on macOS** as well. Simply run
`make cli-darwin-server` to build a binary, or `go run main.go serve` (see above) to run it.
### Build the web app
The sources for the web app live in `web/`. As long as you have `npm` installed (see above), building the web app
@@ -284,9 +287,13 @@ Then either follow the steps for building with or without Firebase.
I do build the ntfy Android app using IntelliJ IDEA (Android Studio), so I don't know if these Gradle commands will
work without issues. Please give me feedback if it does/doesn't work for you.
Without Firebase, you may want to still change the default `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
Without Firebase, you may want to still change the default `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
if you're self-hosting the server. Then run:
```
# Remove Google dependencies (FCM)
sed -i -e '/google-services/d' build.gradle
sed -i -e '/google-services/d' app/build.gradle
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
./gradlew assembleFdroidRelease
@@ -303,7 +310,7 @@ To build your own version with Firebase, you must:
* Create a Firebase/FCM account
* Place your account file at `app/google-services.json`
* And change `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
* 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/*.apk)
@@ -312,3 +319,9 @@ To build your own version with Firebase, you must:
# To build a bundle .aab (app/play/release/*.aab)
./gradlew bundlePlayRelease
```
## iOS app
The ntfy iOS app source code is available [on GitHub](https://github.com/binwiederhier/ntfy-ios).
!!! info
I haven't had time to move the build instructions here. Please check out the repository instead.

View File

@@ -4,6 +4,11 @@ There are a million ways to use ntfy, but here are some inspirations. I try to c
<a href="https://github.com/binwiederhier/ntfy/tree/main/examples">examples on GitHub</a>, so be sure to check
those out, too.
!!! info
Many of these examples were contributed by ntfy users. If you have other examples of how you use ntfy, please
[create a pull request](https://github.com/binwiederhier/ntfy/pulls), and I'll happily include it. Also note, that
I cannot guarantee that all of these examples are functional. Many of them I have not tried myself.
## A long process is done: backups, copying data, pipelines, ...
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
@@ -98,7 +103,8 @@ One of my co-workers uses the following Ansible task to let him know when things
```
## Watchtower notifications (shoutrrr)
You can use `shoutrrr` generic webhook support to send watchtower notifications to your ntfy topic.
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
Example docker-compose.yml:
```yml
@@ -122,7 +128,6 @@ GitHub have been hopeless. In case it ever becomes available, I want to know imm
``` cron
# Check github/ntfy user
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
~
```
## Download notifications (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd)
@@ -132,11 +137,10 @@ Some simple bash scripts to achieve this are kindly provided in [nickexyz's repo
## Node-RED
You can use the HTTP request node to send messages with [Node-RED](https://nodered.org), some examples:
<details>
<summary>Example: Send a message (click to expand)</summary>
```
``` json
[
{
"id": "c956e688cc74ad8e",
@@ -225,7 +229,7 @@ You can use the HTTP request node to send messages with [Node-RED](https://noder
<details>
<summary>Example: Send a picture (click to expand)</summary>
```
``` json
[
{
"id": "d135a13eadeb9d6d",
@@ -341,8 +345,8 @@ You can use the HTTP request node to send messages with [Node-RED](https://noder
## Gatus service health check
An example for a custom alert with <a href="https://github.com/TwiN/gatus">Gatus</a>
```
An example for a custom alert with [Gatus](https://github.com/TwiN/gatus):
``` yaml
alerting:
custom:
url: "https://ntfy.sh"
@@ -366,3 +370,18 @@ alerting:
TRIGGERED: "warning"
RESOLVED: "white_check_mark"
```
## Jellyseerr/Overseerr webhook
Here is an example for [jellyseerr](https://github.com/Fallenbagel/jellyseerr)/[overseerr](https://overseerr.dev/) webhook
JSON payload. Remember to change the `https://requests.example.com` to your jellyseerr/overseerr URL.
``` json
{
"topic": "requests",
"title": "{{event}}",
"message": "{{subject}}\n{{message}}\n\nRequested by: {{requestedBy_username}}\n\nStatus: {{media_status}}\nRequest Id: {{request_id}}",
"priority": 4,
"attach": "{{image}}",
"click": "https://requests.example.com/{{media_type}}/{{media_tmdbid}}"
}
```

View File

@@ -5,7 +5,7 @@ or POST requests. I use it to notify myself when scripts fail, or long-running c
## Step 1: Get the app
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
<a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="../../static/img/badge-appstore.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
@@ -83,7 +83,7 @@ This will create a notification that looks like this:
</figure>
That's it. You're all set. Go play and read the rest of the docs. I highly recommend reading at least the page on
[publishing messages](publish.md), as well as the detailed page on the [Android app](subscribe/phone.md).
[publishing messages](publish.md), as well as the detailed page on the [Android/iOS app](subscribe/phone.md).
Here's another video showing the entire process:

View File

@@ -26,37 +26,37 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_x86_64.tar.gz
tar zxvf ntfy_1.22.0_linux_x86_64.tar.gz
sudo cp -a ntfy_1.22.0_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.22.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_x86_64.tar.gz
tar zxvf ntfy_1.24.0_linux_x86_64.tar.gz
sudo cp -a ntfy_1.24.0_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_armv6.tar.gz
tar zxvf ntfy_1.22.0_linux_armv6.tar.gz
sudo cp -a ntfy_1.22.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.22.0_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.tar.gz
tar zxvf ntfy_1.24.0_linux_armv6.tar.gz
sudo cp -a ntfy_1.24.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_armv7.tar.gz
tar zxvf ntfy_1.22.0_linux_armv7.tar.gz
sudo cp -a ntfy_1.22.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.22.0_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.tar.gz
tar zxvf ntfy_1.24.0_linux_armv7.tar.gz
sudo cp -a ntfy_1.24.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_arm64.tar.gz
tar zxvf ntfy_1.22.0_linux_arm64.tar.gz
sudo cp -a ntfy_1.22.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.22.0_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.tar.gz
tar zxvf ntfy_1.24.0_linux_arm64.tar.gz
sudo cp -a ntfy_1.24.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -103,7 +103,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -111,7 +111,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -119,7 +119,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -127,7 +127,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -137,28 +137,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.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/v1.22.0/ntfy_1.22.0_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_1.22.0_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -184,21 +184,23 @@ If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For a
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_v1.22.0_macOS_all.tar.gz > ntfy_v1.22.0_macOS_all.tar.gz
tar zxvf ntfy_v1.22.0_macOS_all.tar.gz
sudo cp -a ntfy_v1.22.0_macOS_all/ntfy /usr/local/bin/ntfy
curl https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_v1.24.0_macOS_all.tar.gz > ntfy_v1.24.0_macOS_all.tar.gz
tar zxvf ntfy_v1.24.0_macOS_all.tar.gz
sudo cp -a ntfy_v1.24.0_macOS_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_v1.22.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_v1.24.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
!!! info
If there is a desire to install ntfy via [Homebrew](https://brew.sh/), please create a
[GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know.
[GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know. Also, you can build and run the
ntfy server on macOS as well, though I don't officially support that. Check out the [build instructions](develop.md)
for details.
## Windows
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.22.0/ntfy_v1.22.0-next_windows_x86_64.zip),
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_v1.24.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).

View File

@@ -296,6 +296,8 @@ an [external image attachment](#attach-file-from-a-url) and [email publishing](#
</figure>
## Message title
_Supported on:_ :material-android: :material-apple: :material-firefox:
The notification title is typically set to the topic short URL (e.g. `ntfy.sh/mytopic`). To override the title,
you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
@@ -372,7 +374,9 @@ you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
</figure>
## Message priority
All messages have a priority, which defines how urgently your phone notifies you. You can set custom
_Supported on:_ :material-android: :material-apple: :material-firefox:
All messages have a priority, which defines how urgently your phone notifies you. On Android, you can set custom
notification sounds and vibration patterns on your phone to map to these priorities (see [Android config](subscribe/phone.md)).
The following priorities exist:
@@ -460,6 +464,8 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
</figure>
## Tags & emojis 🥳 🎉
_Supported on:_ :material-android: :material-apple: :material-firefox:
You can tag messages with emojis and other relevant strings:
* **Emojis**: If a tag matches an [emoji short code](emojis.md), it'll be converted to an emoji and prepended
@@ -579,6 +585,8 @@ them with a comma, e.g. `tag1,tag2,tag3`.
</figure>
## Scheduled delivery
_Supported on:_ :material-android: :material-apple: :material-firefox:
You can delay the delivery of messages and let ntfy send them at a later date. This can be used to send yourself
reminders or even to execute commands at a later date (if your subscriber acts on messages).
@@ -679,6 +687,8 @@ Here are a few examples (assuming today's date is **12/10/2021, 9am, Eastern Tim
</tr></table>
## Webhooks (publish via GET)
_Supported on:_ :material-android: :material-apple: :material-firefox:
In addition to using PUT/POST, you can also send to topics via simple HTTP GET requests. This makes it easy to use
a ntfy topic as a [webhook](https://en.wikipedia.org/wiki/Webhook), or if your client has limited HTTP support (e.g.
like the [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) Android app).
@@ -782,6 +792,8 @@ Here's an example with a custom message, tags and a priority:
```
## Publish as JSON
_Supported on:_ :material-android: :material-apple: :material-firefox:
For some integrations with other tools (e.g. [Jellyfin](https://jellyfin.org/), [overseerr](https://overseerr.dev/)),
adding custom headers to HTTP requests may be tricky or impossible, so ntfy also allows publishing the entire message
as JSON in the request body.
@@ -943,6 +955,8 @@ all the supported fields:
| `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications |
## Action buttons
_Supported on:_ :material-android: :material-apple: :material-firefox:
You can add action buttons to notifications to allow yourself to react to a notification directly. This is incredibly
useful and has countless applications.
@@ -953,7 +967,7 @@ As of today, the following actions are supported:
* [`view`](#open-websiteapp): Opens a website or app when the action button is tapped
* [`broadcast`](#send-android-broadcast): Sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
when the action button is tapped
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 that a notification with actions can look like:
@@ -1276,6 +1290,8 @@ The required/optional fields for each action depend on the type of the action it
for details.
### Open website/app
_Supported on:_ :material-android: :material-apple: :material-firefox:
The `view` action **opens a website or app when the action button is tapped**, e.g. a browser, a Google Maps location, or
even a deep link into Twitter or a show ntfy topic. How exactly the action is handled depends on how Android and your
desktop browser treat the links. Normally it'll just open a link in the browser.
@@ -1515,6 +1531,8 @@ The `view` action supports the following fields:
| `clear` | - | *boolean* | `false` | `true` | Clear notification after action button is tapped |
### Send Android broadcast
_Supported on:_ :material-android:
The `broadcast` action **sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
when the action button is tapped**. This allows integration into automation apps such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm), which basically means
@@ -1779,6 +1797,8 @@ The `broadcast` action supports the following fields:
| `clear` | - | *boolean* | `false` | `true` | Clear notification after action button is tapped |
### Send HTTP request
_Supported on:_ :material-android: :material-apple: :material-firefox:
The `http` action **sends a HTTP request when the action button is tapped**. You can use this to trigger REST APIs
for whatever systems you have, e.g. opening the garage door, or turning on/off lights.
@@ -1791,14 +1811,14 @@ Here's an example using the [`X-Actions` header](#using-a-header):
```
curl \
-d "Garage door has been open for 15 minutes. Close it?" \
-H "Actions: http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" \
-H "Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" \
ntfy.sh/myhome
```
=== "ntfy CLI"
```
ntfy publish \
--actions="http, Cloor 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\"}" \
myhome \
"Garage door has been open for 15 minutes. Close it?"
```
@@ -1807,7 +1827,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
``` http
POST /myhome HTTP/1.1
Host: ntfy.sh
Actions: http, Cloor 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"}
Garage door has been open for 15 minutes. Close it?
```
@@ -1818,7 +1838,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
method: 'POST',
body: 'Garage door has been open for 15 minutes. Close it?',
headers: {
'Actions': 'http, Cloor 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\"}'
}
})
```
@@ -1826,14 +1846,14 @@ Here's an example using the [`X-Actions` header](#using-a-header):
=== "Go"
``` go
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("Garage door has been open for 15 minutes. Close it?"))
req.Header.Set("Actions", "http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}")
req.Header.Set("Actions", "http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}")
http.DefaultClient.Do(req)
```
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/myhome"
$headers = @{ Actions="http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" }
$headers = @{ Actions="http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" }
$body = "Garage door has been open for 15 minutes. Close it?"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
```
@@ -1842,7 +1862,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
``` python
requests.post("https://ntfy.sh/myhome",
data="Garage door has been open for 15 minutes. Close it?",
headers={ "Actions": "http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" })
headers={ "Actions": "http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" })
```
=== "PHP"
@@ -1852,7 +1872,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
'method' => 'POST',
'header' =>
"Content-Type: text/plain\r\n" .
"Actions: http, Cloor 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?'
]
]));
@@ -2055,6 +2075,8 @@ The `http` action supports the following fields:
| `clear` | - | *boolean* | `false` | `true` | Clear notification after HTTP request succeeds. If the request fails, the notification is not cleared. |
## Click action
_Supported on:_ :material-android: :material-apple: :material-firefox:
You can define which URL to open when a notification is clicked. This may be useful if your notification is related
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.
@@ -2143,6 +2165,8 @@ Here's an example that will open Reddit when the notification is clicked:
```
## Attachments
_Supported on:_ :material-android: :material-firefox:
You can **send images and other files to your phone** as attachments to a notification. The attachments are then downloaded
onto your phone (depending on size and setting automatically), and can be used from the Downloads folder.
@@ -2315,6 +2339,8 @@ Here's an example showing how to attach an APK file:
</figure>
## E-mail notifications
_Supported on:_ :material-android: :material-apple: :material-firefox:
You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that
you'd like to persist longer, or to blast-notify yourself on all possible channels.
@@ -2425,6 +2451,8 @@ Here's what that looks like in Google Mail:
</figure>
## E-mail publishing
_Supported on:_ :material-android: :material-apple: :material-firefox:
You can publish messages to a topic via e-mail, i.e. by sending an email to a specific address. For instance, you can
publish a message to the topic `sometopic` by sending an e-mail to `ntfy-sometopic@ntfy.sh`. This is useful for e-mail
based integrations such as for statuspage.io (though these days most services also support webhooks and HTTP calls).
@@ -2754,4 +2782,5 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
| `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 |

View File

@@ -4,26 +4,94 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
<!--
## ntfy server v1.23.0 (UNRELEASED)
## ntfy iOS app v1.1 (UNRELEASED)
**Features:**
* [Message priority](https://ntfy.sh/docs/publish/#message-priority) support (no ticket)
* [Tags/emojis](https://ntfy.sh/docs/publish/#tags-emojis) support (no ticket)
* [Action buttons](https://ntfy.sh/docs/publish/#action-buttons) support (no ticket)
* [Click action](https://ntfy.sh/docs/publish/#click-action) support (no ticket)
* Open topic when notification clicked (no ticket)
* Notification now makes a sound and vibrates (no ticket)
* Cancel notifications when navigating to topic (no ticket)
* iOS 14.0 support (no ticket, [PR#1](https://github.com/binwiederhier/ntfy-ios/pull/1), thanks to [@callum-99](https://github.com/callum-99))
**Bugs:**
* iOS UI not always updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267))
-->
## ntfy server v1.24.0
Released May 28, 2022
**Features:**
* Regularly send Firebase keepalive messages to ~poll topic to support self-hosted servers (no ticket)
* Add subscribe filter to query exact messages by ID (no ticket)
* Support for `poll_request` messages to support [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) for self-hosted servers (no ticket)
**Bugs:**
* Support emails without `Content-Type` ([#265](https://github.com/binwiederhier/ntfy/issues/265), thanks to [@dmbonsall](https://github.com/dmbonsall))
**Additional translations:**
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
## ntfy Android app v1.14.0 (UNRELEASED)
**Additional translations:**
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
## ntfy iOS app v1.0
Released May 25, 2022
This is the first version of the ntfy iOS app. It supports only ntfy.sh (no selfhosted servers) and only messages + title
(no priority, tags, attachments, ...). I'll rapidly add (hopefully) most of the other ntfy features, and then I'll focus
on self-hosted servers.
The app is now available in the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
**Tickets:**
* iOS app ([#4](https://github.com/binwiederhier/ntfy/issues/4), see also: [TestFlight summary](https://github.com/binwiederhier/ntfy/issues/4#issuecomment-1133767150))
**Thanks:**
* Thank you to all the testers who tried out the app. You guys gave me the confidence that it's ready to release (albeit with
some known issues which will be addressed in follow-up releases).
## ntfy server v1.23.0
Released May 21, 2022
This release ships a CLI for Windows and macOS, as well as the ability to disable the web app entirely. On top of that,
it adds support for APNs, the iOS messaging service. This is needed for the (soon to be released) iOS app.
**Features:**
* [Windows](https://ntfy.sh/docs/install/#windows) and [macOS](https://ntfy.sh/docs/install/#macos) builds for the [ntfy CLI](https://ntfy.sh/docs/subscribe/cli/) ([#112](https://github.com/binwiederhier/ntfy/issues/112))
* Ability to disable the web app entirely ([#238](https://github.com/binwiederhier/ntfy/issues/238)/[#249](https://github.com/binwiederhier/ntfy/pull/249), thanks to [@Curid](https://github.com/Curid))
* Add APNs config to Firebase messages to support [iOS app](https://github.com/binwiederhier/ntfy/issues/4) ([#247](https://github.com/binwiederhier/ntfy/pull/247), thanks to [@Copephobia](https://github.com/Copephobia))
**Bugs:**
* Support underscores in server.yml config options ([#255](https://github.com/binwiederhier/ntfy/issues/255), thanks to [@ajdelgado](https://github.com/ajdelgado))
* Force MAKEFLAGS to --jobs=1 in `Makefile` ([#257](https://github.com/binwiederhier/ntfy/pull/257), thanks to [@oddlama](https://github.com/oddlama))
**Documentation:**
* Typo in install instructions ([#252](https://github.com/binwiederhier/ntfy/pull/252)/[#251](https://github.com/binwiederhier/ntfy/issues/251), thanks to [@oddlama](https://github.com/oddlama))
* Fix typo in private server example ([#262](https://github.com/binwiederhier/ntfy/pull/262), thanks to [@MayeulC](https://github.com/MayeulC))
* [Examples](examples.md) for [jellyseerr](https://github.com/Fallenbagel/jellyseerr)/[overseerr](https://overseerr.dev/) ([#264](https://github.com/binwiederhier/ntfy/pull/264), thanks to [@Fallenbagel](https://github.com/Fallenbagel))
**Additional translations:**
* Portuguese/Brazil (thanks to [@tiagotriques](https://hosted.weblate.org/user/tiagotriques/))
* Portuguese/Brazil (thanks to [@tiagotriques](https://hosted.weblate.org/user/tiagotriques/) and [@pireshenrique22](https://hosted.weblate.org/user/pireshenrique22/))
-->
Thank you to the many translators, who helped translate the new strings so quickly. I am humbled and amazed by your help.
## ntfy Android app v1.13.0
Released May 11, 2022

View File

@@ -267,7 +267,7 @@ curl -s "ntfy.sh/mytopic/json?poll=1&sched=1"
```
### Filter messages
You can filter which messages are returned based on the well-known message fields `message`, `title`, `priority` and
You can filter which messages are returned based on the well-known message fields `id`, `message`, `title`, `priority` and
`tags`. Here's an example that only returns messages of high or urgent priority that contains the both tags
"zfs-error" and "error". Note that the `priority` filter is a logical OR and the `tags` filter is a logical AND.
@@ -280,12 +280,13 @@ $ curl "ntfy.sh/alerts/json?priority=high&tags=zfs-error"
Available filters (all case-insensitive):
| Filter variable | Alias | Example | Description |
|-----------------|---------------------------|------------------------------------|-------------------------------------------------------------------------|
| `message` | `X-Message`, `m` | `ntfy.sh/mytopic?message=lalala` | Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | `ntfy.sh/mytopic?title=some+title` | Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) |
| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated) |
| Filter variable | Alias | Example | Description |
|-----------------|---------------------------|-----------------------------------------------|-------------------------------------------------------------------------|
| `id` | `X-ID` | `ntfy.sh/mytopic/json?poll=1&id=pbkiz8SD7ZxG` | Only return messages that match this exact message ID |
| `message` | `X-Message`, `m` | `ntfy.sh/mytopic/json?message=lalala` | Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | `ntfy.sh/mytopic/json?title=some+title` | Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic/json?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) |
| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?/jsontags=error,alert` | Only return messages that match *all listed tags* (comma-separated) |
### Subscribe to multiple topics
It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics
@@ -315,18 +316,19 @@ format of the message. It's very straight forward:
**Message**:
| Field | Required | Type | Example | Description |
|--------------|----------|---------------------------------------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
| `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
| `message` | - | *string* | `Some message` | Message body; always present in `message` events |
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
| Field | Required | Type | Example | Description |
|--------------|----------|---------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
| `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
| `message` | - | *string* | `Some message` | Message body; always present in `message` events |
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
| `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification |
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
**Attachment** (part of the message, see [attachments](../publish.md#attachments) for details):
@@ -416,6 +418,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
| `poll` | `X-Poll`, `po` | Return cached messages and close connection |
| `since` | `X-Since`, `si` | Return cached messages since timestamp, duration or message ID |
| `scheduled` | `X-Scheduled`, `sched` | Include scheduled/delayed messages in message list |
| `id` | `X-ID` | Filter: Only return messages that match this exact message ID |
| `message` | `X-Message`, `m` | Filter: Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | Filter: Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | Filter: Only return messages that match *any priority listed* (comma-separated) |

View File

@@ -1,14 +1,16 @@
# Subscribe from your phone
You can use the [ntfy Android App](https://play.google.com/store/apps/details?id=io.heckel.ntfy) to receive
notifications directly on your phone. Just like the server, this app is also [open source](https://github.com/binwiederhier/ntfy-android).
Since I don't have an iPhone or a Mac, I didn't make an iOS app yet. I'd be awesome if [someone else could help out](https://github.com/binwiederhier/ntfy/issues/4).
You can use the ntfy [Android App](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [iOS app](https://apps.apple.com/us/app/ntfy/id1625396347)
to receive notifications directly on your phone. Just like the server, this app is also open source, and the code is available
on GitHub ([Android](https://github.com/binwiederhier/ntfy-android), [iOS](https://github.com/binwiederhier/ntfy-ios)). Feel free to
contribute, or [build your own](../develop.md).
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="../../static/img/badge-appstore.png"></a>
You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that
the F-Droid flavor does not use Firebase.
the 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).
## Overview
A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
@@ -31,6 +33,8 @@ If those screenshots are still not enough, here's a video:
</figure>
## Message priority
_Supported on:_ :material-android: :material-apple:
When you [publish messages](../publish.md#message-priority) to a topic, you can **define a priority**. This priority defines
how urgently Android will notify you about the notification, and whether they make a sound and/or vibrate.
@@ -59,6 +63,8 @@ setting, and other settings such as popover or notification dot:
</figure>
## Instant delivery
_Supported on:_ :material-android:
Instant delivery allows you to receive messages on your phone instantly, **even when your phone is in doze mode**, i.e.
when the screen turns off, and you leave it on the desk for a while. This is achieved with a foreground service, which
you'll see as a permanent notification that looks like this:
@@ -89,6 +95,8 @@ The ntfy Android app uses Firebase only for the main host `ntfy.sh`, and only in
It won't use Firebase for any self-hosted servers, and not at all in the the F-Droid flavor.
## Share to topic
_Supported on:_ :material-android:
You can share files to a topic using Android's "Share" feature. This works in almost any app that supports sharing files
or text, and it's useful for sending yourself links, files or other things. The feature remembers a few of the last topics
you shared content to and lists them at the bottom.
@@ -101,6 +109,8 @@ The feature is pretty self-explanatory, and one picture says more than a thousan
</div>
## ntfy:// links
_Supported on:_ :material-android:
The ntfy Android app supports deep linking directly to topics. This is useful when integrating with [automation apps](#automation-apps)
such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm),
or to simply directly link to a topic from a mobile website.
@@ -119,6 +129,8 @@ or to simply directly link to a topic from a mobile website.
## Integrations
### UnifiedPush
_Supported on:_ :material-android:
[UnifiedPush](https://unifiedpush.org) is a standard for receiving push notifications without using the Google-owned
[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) service. It puts push notifications
in the control of the user. ntfy can act as a **UnifiedPush distributor**, forwarding messages to apps that support it.
@@ -134,6 +146,8 @@ to handle messages. Here's an example with [FluffyChat](https://fluffychat.im/):
</div>
### Automation apps
_Supported on:_ :material-android:
The ntfy Android app integrates nicely with automation apps such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm). Using Android intents, you can
**react to incoming messages**, as well as **send messages**.
@@ -210,9 +224,3 @@ The following intent extras are supported when for the intent with the `io.hecke
| `message` ❤️ | ✔ | *String* | `Some message` | Message body; **you must set this** |
| `tags` | - | *String* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) |
| `priority` | - | *String or Int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
## iPhone/iOS
I almost feel devious for putting the *Download on the App Store* button on this page. Currently, there is no iOS app
for ntfy, but it's in the works. You can track the status on GitHub.
<a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="../../static/img/badge-appstore.png"></a>

24
go.mod
View File

@@ -4,7 +4,7 @@ go 1.17
require (
cloud.google.com/go/firestore v1.6.1 // indirect
cloud.google.com/go/storage v1.22.0 // indirect
cloud.google.com/go/storage v1.22.1 // indirect
firebase.google.com/go v3.13.0+incompatible
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
@@ -14,20 +14,20 @@ require (
github.com/mattn/go-sqlite3 v1.14.13
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.6.0
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
github.com/urfave/cli/v2 v2.8.1
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 // indirect
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
golang.org/x/time v0.0.0-20220411224347-583f2d630306
google.golang.org/api v0.79.0
google.golang.org/api v0.81.0
gopkg.in/yaml.v2 v2.4.0
)
require github.com/pkg/errors v0.9.1 // indirect
require (
cloud.google.com/go v0.101.1 // indirect
cloud.google.com/go v0.102.0 // indirect
cloud.google.com/go/compute v1.6.1 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
@@ -36,17 +36,19 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
github.com/googleapis/go-type-adapters v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20220516133312-45b265872317 // indirect
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a // indirect
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 // indirect
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
google.golang.org/grpc v1.46.2 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect

67
go.sum
View File

@@ -27,8 +27,8 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.101.1 h1:3+/0TAm9JD/PyhkrDWQWi2L197h3euCsM+H+J4iYTR8=
cloud.google.com/go v0.101.1/go.mod h1:55HwjsGW4CHD3JrNuMdZtSDsgTs0CuCB/bBTugD+7AA=
cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8=
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -56,8 +56,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.22.0 h1:NUV0NNp9nkBuW66BFRLuMgldN60C57ET3dhbwLIYio8=
cloud.google.com/go/storage v1.22.0/go.mod h1:GbaLEoMqbVm6sx3Z0R++gSiBlgMv6yUi2q1DeGFKQgE=
cloud.google.com/go/storage v1.22.1 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg=
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
@@ -168,9 +168,8 @@ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPg
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -187,12 +186,13 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
@@ -213,8 +213,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
@@ -237,8 +235,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.6.0 h1:yj2Drkflh8X/zUrkWlWlUjZYHyWN7WMmpVxyxXIUyv8=
github.com/urfave/cli/v2 v2.6.0/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs=
github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
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.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -258,12 +258,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 h1:Tgea0cVUD0ivh5ADBX4WwuI12DUd2to3nCYe2eayMIw=
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c=
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -340,10 +336,10 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220516133312-45b265872317 h1:r49syLG49NYZTBMIAay/ng07NB4DW3GzH7LylUq15UM=
golang.org/x/net v0.0.0-20220516133312-45b265872317/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -363,8 +359,9 @@ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -375,7 +372,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -435,14 +431,13 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY=
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -513,8 +508,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -552,10 +548,10 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.79.0 h1:vaOcm0WdXvhGkci9a0+CcQVZqSRjN8ksSBlWv99f8Pg=
google.golang.org/api v0.79.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8=
google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -634,15 +630,17 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 h1:q1kiSVscqoDeqTF27eQ2NnLLDmqF0I373qQNXYMy0fo=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -671,7 +669,6 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=

View File

@@ -61,6 +61,9 @@ markdown_extensions:
custom_checkbox: true
- attr_list
- md_in_html
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
plugins:
- search

View File

@@ -13,7 +13,8 @@ const (
DefaultAtSenderInterval = 10 * time.Second
DefaultMinDelay = 10 * time.Second
DefaultMaxDelay = 3 * 24 * time.Hour
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery
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)
)
// Defines all global and per-visitor limits
@@ -67,6 +68,8 @@ type Config struct {
WebRootIsApp bool
AtSenderInterval time.Duration
FirebaseKeepaliveInterval time.Duration
FirebasePollInterval time.Duration
UpstreamBaseURL string
SMTPSenderAddr string
SMTPSenderUser string
SMTPSenderPass string
@@ -117,6 +120,7 @@ func NewConfig() *Config {
MaxDelay: DefaultMaxDelay,
AtSenderInterval: DefaultAtSenderInterval,
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
FirebasePollInterval: DefaultFirebasePollInterval,
TotalTopicLimit: DefaultTotalTopicLimit,
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit,

View File

@@ -3,6 +3,7 @@ package server
import (
"bytes"
"context"
"crypto/sha256"
"embed"
"encoding/base64"
"encoding/json"
@@ -91,7 +92,9 @@ var (
const (
firebaseControlTopic = "~control" // See Android if changed
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"
)
@@ -421,6 +424,9 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
if err != nil {
return err
}
if m.PollID != "" {
m = newPollRequestMessage(t.ID, m.PollID)
}
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
return err
}
@@ -434,18 +440,13 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
}
}
if s.firebase != nil && firebase && !delayed {
go func() {
if err := s.firebase(m); err != nil {
log.Printf("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error())
}
}()
go s.sendToFirebase(v, m)
}
if s.mailer != nil && email != "" && !delayed {
go func() {
if err := s.mailer.Send(v.ip, email, m); err != nil {
log.Printf("[%s] MAIL - Unable to send email: %v", v.ip, err.Error())
}
}()
go s.sendEmail(v, m, email)
}
if s.config.UpstreamBaseURL != "" {
go s.forwardPollRequest(v, m)
}
if cache {
if err := s.messageCache.AddMessage(m); err != nil {
@@ -463,6 +464,38 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
return nil
}
func (s *Server) sendToFirebase(v *visitor, m *message) {
if err := s.firebase(m); err != nil {
log.Printf("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error())
}
}
func (s *Server) sendEmail(v *visitor, m *message, email string) {
if err := s.mailer.Send(v.ip, email, m); err != nil {
log.Printf("[%s] MAIL - Unable to send email: %v", v.ip, err.Error())
}
}
func (s *Server) forwardPollRequest(v *visitor, m *message) {
topicURL := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic)
topicHash := fmt.Sprintf("%x", sha256.Sum256([]byte(topicURL)))
forwardURL := fmt.Sprintf("%s/%s", s.config.UpstreamBaseURL, topicHash)
req, err := http.NewRequest("POST", forwardURL, strings.NewReader(""))
if err != nil {
log.Printf("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error())
return
}
req.Header.Set("X-Poll-ID", m.ID)
response, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error())
return
} else if response.StatusCode != http.StatusOK {
log.Printf("[%s] FWD - Unable to forward poll request, unexpected status: %d", v.ip, response.StatusCode)
return
}
}
func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) {
cache = readBoolParam(r, true, "x-cache", "cache")
firebase = readBoolParam(r, true, "x-firebase", "firebase")
@@ -548,32 +581,42 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
firebase = false
unifiedpush = true
}
m.PollID = readParam(r, "x-poll-id", "poll-id")
if m.PollID != "" {
unifiedpush = false
cache = false
email = ""
}
return cache, firebase, email, unifiedpush, nil
}
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
//
// 1. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
// 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 body is binary, encode as base64, if not do not encode
// 2. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
// Body must be a message, because we attached an external URL
// 3. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
// Body must be attachment, because we passed a filename
// 4. curl -T file.txt ntfy.sh/mytopic
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
// 5. curl -T file.txt ntfy.sh/mytopic
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
// 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 unifiedpush {
return s.handleBodyAsMessageAutoDetect(m, body) // Case 1
if m.Event == pollRequestEvent { // Case 1
return nil
} else if unifiedpush {
return s.handleBodyAsMessageAutoDetect(m, body) // Case 2
} else if m.Attachment != nil && m.Attachment.URL != "" {
return s.handleBodyAsTextMessage(m, body) // Case 2
return s.handleBodyAsTextMessage(m, body) // Case 3
} else if m.Attachment != nil && m.Attachment.Name != "" {
return s.handleBodyAsAttachment(r, v, m, body) // Case 3
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
return s.handleBodyAsTextMessage(m, body) // Case 4
return s.handleBodyAsTextMessage(m, body) // Case 5
}
return s.handleBodyAsAttachment(r, v, m, body) // Case 5
return s.handleBodyAsAttachment(r, v, m, body) // Case 6
}
func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedReadCloser) error {
@@ -1074,7 +1117,11 @@ func (s *Server) runFirebaseKeepaliver() {
select {
case <-time.After(s.config.FirebaseKeepaliveInterval):
if err := s.firebase(newKeepaliveMessage(firebaseControlTopic)); err != nil {
log.Printf("error sending Firebase keepalive message: %s", err.Error())
log.Printf("error sending Firebase keepalive message to %s: %s", firebaseControlTopic, err.Error())
}
case <-time.After(s.config.FirebasePollInterval):
if err := s.firebase(newKeepaliveMessage(firebasePollTopic)); err != nil {
log.Printf("error sending Firebase keepalive message to %s: %s", firebasePollTopic, err.Error())
}
case <-s.closeChan:
return

View File

@@ -135,6 +135,18 @@
#
# web-root: app
# Server URL of a Firebase/APNS-connected ntfy server (likely "https://ntfy.sh").
#
# iOS users:
# If you use the iOS ntfy app, you MUST configure this to receive timely notifications. You'll like want this:
# upstream-base-url: "https://ntfy.sh"
#
# If set, all incoming messages will publish a "poll_request" message to the configured upstream server, containing
# the message ID of the original message, instructing the iOS app to poll this server for the actual message contents.
# This is to prevent the upstream server and Firebase/APNS from being able to read the message.
#
# upstream-base-url:
# Rate limiting: Total number of topics before the server rejects new topics.
#
# global-topic-limit: 15000

View File

@@ -3,37 +3,20 @@ package server
import (
"context"
"encoding/json"
"fmt"
"strings"
firebase "firebase.google.com/go"
"firebase.google.com/go/messaging"
"fmt"
"google.golang.org/api/option"
"heckel.io/ntfy/auth"
"strings"
)
const (
fcmMessageLimit = 4000
fcmMessageLimit = 4000
fcmApnsBodyMessageLimit = 100
)
// maybeTruncateFCMMessage performs best-effort truncation of FCM messages.
// The docs say the limit is 4000 characters, but during testing it wasn't quite clear
// what fields matter; so we're just capping the serialized JSON to 4000 bytes.
func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message {
s, err := json.Marshal(m)
if err != nil {
return m
}
if len(s) > fcmMessageLimit {
over := len(s) - fcmMessageLimit + 16 // = len("truncated":"1",), sigh ...
message, ok := m.Data["message"]
if ok && len(message) > over {
m.Data["truncated"] = "1"
m.Data["message"] = message[:len(message)-over]
}
}
return m
}
func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subscriber, error) {
fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile))
if err != nil {
@@ -53,8 +36,29 @@ func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subsc
}, nil
}
// toFirebaseMessage converts a message to a Firebase message.
//
// Normal messages ("message"):
// - For Android, we can receive data messages from Firebase and process them as code, so we just send all fields
// in the "data" attribute. In the Android app, we then turn those into a notification and display it.
// - On iOS, we are not allowed to receive data-only messages, so we build messages with an "alert" (with title and
// message), and still send the rest of the data along in the "aps" attribute. We can then locally modify the
// message in the Notification Service Extension.
//
// Keepalive messages ("keepalive"):
// - On Android, we subscribe to the "~control" topic, which is used to restart the foreground service (if it died,
// e.g. after an app update). We send these keepalive messages regularly (see Config.FirebaseKeepaliveInterval).
// - On iOS, we subscribe to the "~poll" topic, which is used to poll all topics regularly. This is because iOS
// does not allow any background or scheduled activity at all.
//
// Poll request messages ("poll_request"):
// - Normal messages are turned into poll request messages if anonymous users are not allowed to read the message.
// On Android, this will trigger the app to poll the topic and thereby displaying new messages.
// - If UpstreamBaseURL is set, messages are forwarded as poll requests to an upstream server and then forwarded
// to Firebase here. This is mainly for iOS to support self-hosted servers.
func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, error) {
var data map[string]string // Mostly matches https://ntfy.sh/docs/subscribe/api/#json-message-format
var apnsConfig *messaging.APNSConfig
switch m.Event {
case keepaliveEvent, openEvent:
data = map[string]string{
@@ -63,6 +67,17 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
"event": m.Event,
"topic": m.Topic,
}
apnsConfig = createAPNSBackgroundConfig(data)
case pollRequestEvent:
data = map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
"message": m.Message,
"poll_id": m.PollID,
}
apnsConfig = createAPNSAlertConfig(m, data)
case messageEvent:
allowForward := true
if auther != nil {
@@ -95,6 +110,7 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
data["attachment_url"] = m.Attachment.URL
}
apnsConfig = createAPNSAlertConfig(m, data)
} else {
// If anonymous read for a topic is not allowed, we cannot send the message along
// via Firebase. Instead, we send a "poll_request" message, asking the client to poll.
@@ -104,6 +120,7 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
"event": pollRequestEvent,
"topic": m.Topic,
}
// TODO Handle APNS?
}
}
var androidConfig *messaging.AndroidConfig
@@ -116,5 +133,85 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
Topic: m.Topic,
Data: data,
Android: androidConfig,
APNS: apnsConfig,
}), nil
}
// maybeTruncateFCMMessage performs best-effort truncation of FCM messages.
// The docs say the limit is 4000 characters, but during testing it wasn't quite clear
// what fields matter; so we're just capping the serialized JSON to 4000 bytes.
func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message {
s, err := json.Marshal(m)
if err != nil {
return m
}
if len(s) > fcmMessageLimit {
over := len(s) - fcmMessageLimit + 16 // = len("truncated":"1",), sigh ...
message, ok := m.Data["message"]
if ok && len(message) > over {
m.Data["truncated"] = "1"
m.Data["message"] = message[:len(message)-over]
}
}
return m
}
// createAPNSAlertConfig creates an APNS config for iOS notifications that show up as an alert (only relevant for iOS).
// We must set the Alert struct ("alert"), and we need to set MutableContent ("mutable-content"), so the Notification Service
// Extension in iOS can modify the message.
func createAPNSAlertConfig(m *message, data map[string]string) *messaging.APNSConfig {
apnsData := make(map[string]interface{})
for k, v := range data {
apnsData[k] = v
}
return &messaging.APNSConfig{
Payload: &messaging.APNSPayload{
CustomData: apnsData,
Aps: &messaging.Aps{
MutableContent: true,
Alert: &messaging.ApsAlert{
Title: m.Title,
Body: maybeTruncateAPNSBodyMessage(m.Message),
},
},
},
}
}
// createAPNSBackgroundConfig creates an APNS config for a silent background message (only relevant for iOS). Apple only
// allows us to send 2-3 of these notifications per hour, and delivery not guaranteed. We use this only for the ~poll
// topic, which triggers the iOS app to poll all topics for changes.
//
// See https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app
func createAPNSBackgroundConfig(data map[string]string) *messaging.APNSConfig {
apnsData := make(map[string]interface{})
for k, v := range data {
apnsData[k] = v
}
return &messaging.APNSConfig{
Headers: map[string]string{
"apns-push-type": "background",
"apns-priority": "5",
},
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
ContentAvailable: true,
},
CustomData: apnsData,
},
}
}
// maybeTruncateAPNSBodyMessage truncates the body for APNS.
//
// The "body" of the push notification can contain the entire message, which would count doubly for the overall length
// of the APNS payload. I set a limit of 100 characters before truncating the notification "body" with ellipsis.
// The message would not be changed (unless truncated for being too long). Note: if the payload is too large (>4KB),
// APNS will simply reject / discard the notification, meaning it will never arrive on the iOS device.
func maybeTruncateAPNSBodyMessage(s string) string {
if len(s) >= fcmApnsBodyMessageLimit {
over := len(s) - fcmApnsBodyMessageLimit + 3 // len("...")
return s[:len(s)-over] + "..."
}
return s
}

View File

@@ -32,6 +32,23 @@ func TestToFirebaseMessage_Keepalive(t *testing.T) {
require.Nil(t, err)
require.Equal(t, "mytopic", fbm.Topic)
require.Nil(t, fbm.Android)
require.Equal(t, &messaging.APNSConfig{
Headers: map[string]string{
"apns-push-type": "background",
"apns-priority": "5",
},
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
ContentAvailable: true,
},
CustomData: map[string]interface{}{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
},
},
}, fbm.APNS)
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
@@ -46,6 +63,23 @@ func TestToFirebaseMessage_Open(t *testing.T) {
require.Nil(t, err)
require.Equal(t, "mytopic", fbm.Topic)
require.Nil(t, fbm.Android)
require.Equal(t, &messaging.APNSConfig{
Headers: map[string]string{
"apns-push-type": "background",
"apns-priority": "5",
},
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
ContentAvailable: true,
},
CustomData: map[string]interface{}{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
},
},
}, fbm.APNS)
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
@@ -60,6 +94,25 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
m.Tags = []string{"tag 1", "tag2"}
m.Click = "https://google.com"
m.Title = "some title"
m.Actions = []*action{
{
ID: "123",
Action: "view",
Label: "Open page",
Clear: true,
URL: "https://ntfy.sh",
},
{
ID: "456",
Action: "http",
Label: "Close door",
URL: "https://door.com/close",
Method: "PUT",
Headers: map[string]string{
"really": "yes",
},
},
}
m.Attachment = &attachment{
Name: "some file.jpg",
Type: "image/jpeg",
@@ -74,6 +127,35 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
require.Equal(t, &messaging.AndroidConfig{
Priority: "high",
}, fbm.Android)
require.Equal(t, &messaging.APNSConfig{
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
MutableContent: true,
Alert: &messaging.ApsAlert{
Title: "some title",
Body: "this is a message",
},
},
CustomData: map[string]interface{}{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "message",
"topic": "mytopic",
"priority": "4",
"tags": strings.Join(m.Tags, ","),
"click": "https://google.com",
"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"}}]`,
"encoding": "",
"attachment_name": "some file.jpg",
"attachment_type": "image/jpeg",
"attachment_size": "12345",
"attachment_expires": "98765543",
"attachment_url": "https://example.com/file.jpg",
},
},
}, fbm.APNS)
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
@@ -84,6 +166,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
"click": "https://google.com",
"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"}}]`,
"encoding": "",
"attachment_name": "some file.jpg",
"attachment_type": "image/jpeg",
@@ -112,6 +195,41 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) {
}, fbm.Data)
}
func TestToFirebaseMessage_PollRequest(t *testing.T) {
m := newPollRequestMessage("mytopic", "fOv6k1QbCzo6")
fbm, err := toFirebaseMessage(m, nil)
require.Nil(t, err)
require.Equal(t, "mytopic", fbm.Topic)
require.Nil(t, fbm.Android)
require.Equal(t, &messaging.APNSConfig{
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
MutableContent: true,
Alert: &messaging.ApsAlert{
Title: "",
Body: "New message",
},
},
CustomData: map[string]interface{}{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "poll_request",
"topic": "mytopic",
"message": "New message",
"poll_id": "fOv6k1QbCzo6",
},
},
}, fbm.APNS)
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "poll_request",
"topic": "mytopic",
"message": "New message",
"poll_id": "fOv6k1QbCzo6",
}, fbm.Data)
}
func TestMaybeTruncateFCMMessage(t *testing.T) {
origMessage := strings.Repeat("this is a long string", 300)
origFCMMessage := &messaging.Message{

View File

@@ -159,37 +159,47 @@ func (s *smtpSession) withFailCount(fn func() error) error {
}
func readMailBody(msg *mail.Message) (string, error) {
if msg.Header.Get("Content-Type") == "" {
return readPlainTextMailBody(msg)
}
contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
if err != nil {
return "", err
}
if contentType == "text/plain" {
body, err := io.ReadAll(msg.Body)
return readPlainTextMailBody(msg)
} else if strings.HasPrefix(contentType, "multipart/") {
return readMultipartMailBody(msg, params)
}
return "", errUnsupportedContentType
}
func readPlainTextMailBody(msg *mail.Message) (string, error) {
body, err := io.ReadAll(msg.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func readMultipartMailBody(msg *mail.Message, params map[string]string) (string, error) {
mr := multipart.NewReader(msg.Body, params["boundary"])
for {
part, err := mr.NextPart()
if err != nil { // may be io.EOF
return "", err
}
partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
if err != nil {
return "", err
}
if partContentType != "text/plain" {
continue
}
body, err := io.ReadAll(part)
if err != nil {
return "", err
}
return string(body), nil
}
if strings.HasPrefix(contentType, "multipart/") {
mr := multipart.NewReader(msg.Body, params["boundary"])
for {
part, err := mr.NextPart()
if err != nil { // may be io.EOF
return "", err
}
partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
if err != nil {
return "", err
}
if partContentType != "text/plain" {
continue
}
body, err := io.ReadAll(part)
if err != nil {
return "", err
}
return string(body), nil
}
}
return "", errUnsupportedContentType
}

View File

@@ -94,6 +94,24 @@ what's up
require.Nil(t, session.Data(strings.NewReader(email)))
}
func TestSmtpBackend_Plaintext_No_ContentType(t *testing.T) {
email := `Subject: Very short mail
what's up
`
conf, backend := newTestBackend(t, func(m *message) error {
require.Equal(t, "mytopic", m.Topic)
require.Equal(t, "Very short mail", m.Title)
require.Equal(t, "what's up", m.Message)
return nil
})
conf.SMTPServerAddrPrefix = ""
session, _ := backend.AnonymousLogin(nil)
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
require.Nil(t, session.Data(strings.NewReader(email)))
}
func TestSmtpBackend_Plaintext_EncodedSubject(t *testing.T) {
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
Subject: =?UTF-8?B?VGhyZWUgc2FudGFzIPCfjoXwn46F8J+OhQ==?=

View File

@@ -24,13 +24,14 @@ type message struct {
Time int64 `json:"time"` // Unix time in seconds
Event string `json:"event"` // One of the above
Topic string `json:"topic"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
Priority int `json:"priority,omitempty"`
Tags []string `json:"tags,omitempty"`
Click string `json:"click,omitempty"`
Actions []*action `json:"actions,omitempty"`
Attachment *attachment `json:"attachment,omitempty"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
PollID string `json:"poll_id,omitempty"`
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
}
@@ -84,14 +85,11 @@ type messageEncoder func(msg *message) (string, error)
// newMessage creates a new message with the current timestamp
func newMessage(event, topic, msg string) *message {
return &message{
ID: util.RandomString(messageIDLength),
Time: time.Now().Unix(),
Event: event,
Topic: topic,
Priority: 0,
Tags: nil,
Title: "",
Message: msg,
ID: util.RandomString(messageIDLength),
Time: time.Now().Unix(),
Event: event,
Topic: topic,
Message: msg,
}
}
@@ -110,6 +108,13 @@ func newDefaultMessage(topic, msg string) *message {
return newMessage(messageEvent, topic, msg)
}
// newPollRequestMessage is a convenience method to create a poll request message
func newPollRequestMessage(topic, pollID string) *message {
m := newMessage(pollRequestEvent, topic, newMessageBody)
m.PollID = pollID
return m
}
func validMessageID(s string) bool {
return util.ValidRandomString(s, messageIDLength)
}
@@ -153,6 +158,7 @@ var (
)
type queryFilter struct {
ID string
Message string
Title string
Tags []string
@@ -160,6 +166,7 @@ type queryFilter struct {
}
func parseQueryFilters(r *http.Request) (*queryFilter, error) {
idFilter := readParam(r, "x-id", "id")
messageFilter := readParam(r, "x-message", "message", "m")
titleFilter := readParam(r, "x-title", "title", "t")
tagsFilter := util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
@@ -172,6 +179,7 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) {
priorityFilter = append(priorityFilter, priority)
}
return &queryFilter{
ID: idFilter,
Message: messageFilter,
Title: titleFilter,
Tags: tagsFilter,
@@ -182,11 +190,11 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) {
func (q *queryFilter) Pass(msg *message) bool {
if msg.Event != messageEvent {
return true // filters only apply to messages
}
if q.Message != "" && msg.Message != q.Message {
} else if q.ID != "" && msg.ID != q.ID {
return false
}
if q.Title != "" && msg.Title != q.Title {
} else if q.Message != "" && msg.Message != q.Message {
return false
} else if q.Title != "" && msg.Title != q.Title {
return false
}
messagePriority := msg.Priority

2504
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -152,5 +152,40 @@
"prefs_notifications_delete_after_one_week_description": "Las notificaciones se eliminan automáticamente después de una semana",
"priority_low": "baja",
"notifications_actions_not_supported": "Acción no soportada en la aplicación web",
"notifications_actions_http_request_title": "Enviar HTTP {{method}} a {{url}}"
"notifications_actions_http_request_title": "Enviar HTTP {{method}} a {{url}}",
"error_boundary_unsupported_indexeddb_description": "La aplicación web ntfy necesita IndexedDB para funcionar y su navegador no soporta IndexedDB en modo de navegación privada. <br/> <br/> Si bien esto es desafortunado, tampoco tiene mucho sentido usar la aplicación web ntfy en modo de navegación privada de todos modos, porque todo está almacenado en el almacenamiento del navegador. Puede leer más sobre esto <githubLink>en este issue de GitHub</githubLink>, o hablar con nosotros en <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink>.",
"action_bar_show_menu": "Mostrar menú",
"action_bar_logo_alt": "logo de ntfy",
"action_bar_toggle_action_menu": "Abrir/cerrar el menú de acción",
"message_bar_show_dialog": "Mostrar diálogo de publicación",
"message_bar_publish": "Publicar mensaje",
"nav_button_muted": "Notificaciones silenciadas",
"nav_button_connecting": "conectando",
"notifications_list": "Lista de notificaciones",
"notifications_list_item": "Notificación",
"notifications_mark_read": "Marcar como leído",
"notifications_delete": "Eliminar",
"notifications_priority_x": "Prioridad {{priority}}",
"notifications_new_indicator": "Nueva notificación",
"notifications_attachment_image": "Imagen adjunta",
"notifications_attachment_file_image": "archivo de imagen",
"notifications_attachment_file_video": "archivo de video",
"notifications_attachment_file_audio": "archivo de audio",
"notifications_attachment_file_app": "Archivo de aplicación de Android",
"notifications_attachment_file_document": "otro documento",
"action_bar_toggle_mute": "Silenciar/reactivar notificaciones",
"publish_dialog_emoji_picker_show": "Elige un emoji",
"publish_dialog_topic_reset": "Restablecer tópico",
"publish_dialog_click_reset": "Eliminar URL de clic",
"publish_dialog_email_reset": "Eliminar el reenvío de correo electrónico",
"publish_dialog_attach_reset": "Eliminar la URL del archivo adjunto",
"publish_dialog_delay_reset": "Eliminar entrega retrasada",
"publish_dialog_attached_file_remove": "Eliminar el archivo adjunto",
"emoji_picker_search_clear": "Limpiar búsqueda",
"subscribe_dialog_subscribe_base_url_label": "URL del servicio",
"prefs_notifications_sound_play": "Reproducir el sonido seleccionado",
"prefs_users_table": "Tabla de usuarios",
"prefs_users_edit_button": "Editar usuario",
"prefs_users_delete_button": "Eliminar usuario",
"error_boundary_unsupported_indexeddb_title": "Navegación privada no soportada"
}

View File

@@ -0,0 +1,191 @@
{
"action_bar_logo_alt": "logo ntfy",
"action_bar_settings": "Impostazioni",
"action_bar_clear_notifications": "Cancella tutte le notifiche",
"action_bar_unsubscribe": "Annulla l'iscrizione",
"action_bar_toggle_action_menu": "Apri/chiudi il menu delle azioni",
"message_bar_type_message": "Digita un messaggio qui",
"message_bar_error_publishing": "Errore durante la pubblicazione della notifica",
"message_bar_show_dialog": "Mostra la finestra di dialogo di pubblicazione",
"message_bar_publish": "Pubblica messaggio",
"nav_topics_title": "Topic a cui si è iscritti",
"nav_button_all_notifications": "Tutte le notifiche",
"nav_button_settings": "Impostazioni",
"nav_button_publish_message": "Pubblica notifica",
"nav_button_subscribe": "Iscriviti al topic",
"nav_button_muted": "Notifiche disattivate",
"nav_button_connecting": "connessione",
"alert_grant_title": "Le notifiche sono disabilitate",
"alert_grant_button": "Concedi ora",
"notifications_list": "Elenco notifiche",
"notifications_list_item": "Notifiche",
"notifications_mark_read": "Segna come letto",
"notifications_delete": "Elimina",
"notifications_copied_to_clipboard": "Copiato negli appunti",
"notifications_tags": "Tags",
"notifications_priority_x": "Priorità {{priority}}",
"notifications_new_indicator": "Nuova notifica",
"notifications_attachment_image": "Immagine allegata",
"notifications_attachment_copy_url_title": "Copia l'URL dell'allegato negli appunti",
"notifications_attachment_copy_url_button": "Copia URL",
"notifications_attachment_open_title": "Vai a {{url}}",
"notifications_attachment_open_button": "Apri allegato",
"notifications_attachment_link_expires": "Il collegamento scade il {{date}}",
"notifications_attachment_link_expired": "link per il download scaduto",
"notifications_attachment_file_image": "file immagine",
"notifications_attachment_file_video": "file video",
"action_bar_toggle_mute": "Abilita/disabilita le notifiche",
"notifications_attachment_file_document": "altro documento",
"notifications_click_copy_url_button": "Copia link",
"notifications_click_open_button": "Apri link",
"notifications_actions_open_url_title": "Vai a {{url}}",
"notifications_actions_not_supported": "Azione non supportata nell'app Web",
"notifications_none_for_topic_title": "Non hai ancora ricevuto alcuna notifica per questo topic.",
"notifications_none_for_topic_description": "Per inviare notifiche a questo argomento, è sufficiente PUT o POST all'URL del topic.",
"notifications_none_for_any_title": "Non hai ricevuto alcuna notifica.",
"notifications_no_subscriptions_title": "Sembra che tu non abbia ancora abbonamenti.",
"notifications_example": "Esempio",
"notifications_more_details": "Per ulteriori informazioni, consulta il <websiteLink>sito web</websiteLink> o <docsLink>documentazione</docsLink>.",
"notifications_loading": "Caricamento notifiche in corso…",
"publish_dialog_title_topic": "Pubblica su {{topic}}",
"publish_dialog_title_no_topic": "Pubblica notifica",
"publish_dialog_progress_uploading": "Caricamento in corso…",
"publish_dialog_progress_uploading_detail": "Caricamento {{loaded}}/{{total}} ({{percent}}%)…",
"publish_dialog_message_published": "Notifica pubblicata",
"publish_dialog_attachment_limits_file_and_quota_reached": "supera {{fileSizeLimit}} limite di file e quota, {{remainingBytes}} rimanenti",
"publish_dialog_attachment_limits_file_reached": "supera di {{fileSizeLimit}} il limite dei file",
"publish_dialog_attachment_limits_quota_reached": "supera la quota, {{remainingBytes}} rimanenti",
"publish_dialog_emoji_picker_show": "Scegli emoji",
"publish_dialog_priority_min": "Min. priorità",
"publish_dialog_priority_low": "Bassa priorità",
"publish_dialog_priority_default": "Priorità predefinita",
"publish_dialog_priority_high": "Priorità alta",
"publish_dialog_priority_max": "Max. priorità",
"publish_dialog_base_url_label": "URL del servizio",
"publish_dialog_base_url_placeholder": "URL del servizio, ad es. https://esempio.com",
"publish_dialog_topic_label": "Nome topic",
"publish_dialog_topic_placeholder": "Nome topic, ad es. avvisi_di_phil",
"publish_dialog_topic_reset": "Reset topic",
"publish_dialog_title_label": "Titolo",
"publish_dialog_title_placeholder": "Titolo della notifica, ad es. Avviso di spazio su disco",
"publish_dialog_message_label": "Messaggio",
"publish_dialog_message_placeholder": "Digita un messaggio qui",
"publish_dialog_tags_label": "Tags",
"publish_dialog_priority_label": "Priorità",
"publish_dialog_click_label": "Clicca URL",
"publish_dialog_click_reset": "Rimuovi l'URL del clic",
"publish_dialog_email_label": "Email",
"publish_dialog_email_placeholder": "Indirizzo a cui inoltrare la notifica, ad es. phil@example.com",
"publish_dialog_email_reset": "Rimuovi inoltro email",
"publish_dialog_attach_label": "URL Allegato",
"publish_dialog_attach_reset": "Rimuovi l'URL dell'allegato",
"publish_dialog_filename_label": "Nome del file",
"publish_dialog_filename_placeholder": "Nome file allegato",
"publish_dialog_delay_placeholder": "Consegna ritardata, ad es. {{unixTimestamp}}, {{relativeTime}} o \"{{naturalLanguage}}\" (solo in inglese)",
"publish_dialog_delay_reset": "Rimuovere la consegna ritardata",
"publish_dialog_other_features": "Altre funzionalità:",
"publish_dialog_chip_click_label": "Fare clic su URL",
"publish_dialog_chip_email_label": "Inoltra a e-mail",
"publish_dialog_chip_attach_url_label": "Allega il file tramite URL",
"publish_dialog_chip_attach_file_label": "Allega file locale",
"publish_dialog_chip_delay_label": "Ritardo nella consegna",
"publish_dialog_button_cancel_sending": "Annulla l'invio",
"publish_dialog_button_cancel": "Annulla",
"publish_dialog_button_send": "Invia",
"publish_dialog_checkbox_publish_another": "Pubblica un altro",
"publish_dialog_attached_file_title": "File allegato:",
"publish_dialog_attached_file_remove": "Rimuovi il file allegato",
"publish_dialog_drop_file_here": "Trascina il file qui",
"emoji_picker_search_clear": "Cancella ricerca",
"subscribe_dialog_subscribe_title": "Iscriviti al topic",
"subscribe_dialog_subscribe_topic_placeholder": "Nome dell'argomento, ad es. avvisi_di_phil",
"subscribe_dialog_subscribe_base_url_label": "URL del servizio",
"subscribe_dialog_subscribe_button_cancel": "Annulla",
"subscribe_dialog_login_title": "Accesso richiesto",
"subscribe_dialog_login_username_label": "Nome utente, ad es. phil",
"subscribe_dialog_login_button_login": "Login",
"subscribe_dialog_error_user_anonymous": "anonimo",
"prefs_notifications_sound_title": "Suono di notifica",
"prefs_notifications_sound_description_some": "Le notifiche riproducono il suono {{sound}} quando arrivano",
"prefs_notifications_sound_no_sound": "Nessun suono",
"prefs_notifications_min_priority_description_any": "Visualizzazione di tutte le notifiche, indipendentemente dalla priorità",
"prefs_notifications_min_priority_description_max": "Mostra notifiche se la priorità è 5 (max)",
"prefs_notifications_min_priority_any": "Qualsiasi priorità",
"prefs_notifications_min_priority_low_and_higher": "Priorità bassa e superiore",
"prefs_notifications_min_priority_high_and_higher": "Priorità alta e superiore",
"prefs_notifications_min_priority_max_only": "Solo priorità massima",
"prefs_notifications_delete_after_never": "Mai",
"prefs_notifications_delete_after_three_hours": "Dopo tre ore",
"prefs_notifications_delete_after_one_day": "Dopo un giorno",
"prefs_notifications_delete_after_never_description": "Le notifiche non vengono mai eliminate automaticamente",
"prefs_notifications_delete_after_one_day_description": "Le notifiche vengono eliminate automaticamente dopo un giorno",
"prefs_notifications_delete_after_one_week_description": "Le notifiche vengono eliminate automaticamente dopo una settimana",
"prefs_notifications_delete_after_one_month_description": "Le notifiche vengono eliminate automaticamente dopo un mese",
"prefs_users_title": "Gestisci gli utenti",
"prefs_users_description": "Aggiungi/rimuovi utenti per i tuoi topic protetti qui. Tieni presente che nome utente e password sono memorizzati nella memoria locale del browser.",
"prefs_users_table": "Tabella utenti",
"prefs_users_add_button": "Aggiungi utente",
"prefs_users_edit_button": "Modifica utente",
"prefs_users_delete_button": "Elimina utente",
"prefs_users_table_user_header": "Utente",
"prefs_users_table_base_url_header": "URL del servizio",
"prefs_users_dialog_title_add": "Aggiungi utente",
"prefs_users_dialog_title_edit": "Modifica utente",
"prefs_users_dialog_base_url_label": "URL del servizio, ad es. https://ntfy.sh",
"prefs_users_dialog_username_label": "Nome utente, ad es. phil",
"prefs_users_dialog_password_label": "Password",
"prefs_users_dialog_button_cancel": "Annulla",
"prefs_users_dialog_button_add": "Aggiungere",
"prefs_users_dialog_button_save": "Salva",
"prefs_appearance_title": "Aspetto",
"prefs_appearance_language_title": "Lingua",
"priority_min": "min",
"priority_low": "basso",
"priority_default": "predefinito",
"priority_high": "alto",
"priority_max": "max",
"error_boundary_title": "Oh no, ntfy è andato in crash",
"error_boundary_description": "Questo ovviamente non dovrebbe accadere. Mi dispiace molto per questo.<br/>Se hai un minuto, per favore <githubLink>segnala su GitHub</githubLink>, o faccelo sapere tramite <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink> .",
"error_boundary_button_copy_stack_trace": "Copia traccia dello stack",
"error_boundary_stack_trace": "Traccia dello stack",
"error_boundary_gathering_info": "Raccogli più informazioni…",
"error_boundary_unsupported_indexeddb_title": "Navigazione privata non supportata",
"action_bar_show_menu": "Mostra menu",
"action_bar_send_test_notification": "Inviare una notifica di prova",
"alert_not_supported_description": "Le notifiche non sono supportate nel tuo browser.",
"nav_button_documentation": "Documentazione",
"notifications_actions_http_request_title": "Invia HTTP {{method}} a {{url}}",
"alert_grant_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop.",
"alert_not_supported_title": "Notifiche non supportate",
"notifications_attachment_file_app": "file app Android",
"notifications_no_subscriptions_description": "Fai clic sul link \"{{linktext}}\" per creare o iscriverti a un topic. Successivamente, puoi inviare messaggi tramite PUT o POST e riceverai le notifiche qui.",
"notifications_attachment_file_audio": "file audio",
"notifications_none_for_any_description": "Per inviare notifiche a un topic, è sufficiente PUT o POST all'URL del topic. Ecco un esempio utilizzando uno dei tuoi topic.",
"notifications_click_copy_url_title": "Copia l'URL del collegamento negli appunti",
"prefs_notifications_sound_description_none": "Le notifiche non emettono alcun suono quando arrivano",
"publish_dialog_delay_label": "Ritardo",
"publish_dialog_tags_placeholder": "Elenco di tag separato da virgole, ad es. avviso, backup-srv1",
"publish_dialog_click_placeholder": "URL che viene aperto quando si fa clic sulla notifica",
"publish_dialog_attach_placeholder": "Allega file tramite URL, ad es. https://f-droid.org/F-Droid.apk",
"publish_dialog_chip_topic_label": "Cambia topic",
"publish_dialog_details_examples_description": "Per esempi e una descrizione dettagliata di tutte le funzioni di invio, fare riferimento alla <docsLink>documentazione</docsLink>.",
"publish_dialog_attached_file_filename_placeholder": "Nome file allegato",
"emoji_picker_search_placeholder": "Cerca emoji",
"subscribe_dialog_subscribe_description": "Gli argomenti potrebbero non essere protetti da password, quindi scegli un nome che non sia facile da indovinare. Una volta iscritto, puoi inviare le notifiche tramite PUT/POST.",
"subscribe_dialog_subscribe_use_another_label": "Usa un altro server",
"subscribe_dialog_login_password_label": "Password",
"subscribe_dialog_subscribe_button_subscribe": "Iscriviti",
"prefs_notifications_sound_play": "Riproduci il suono selezionato",
"prefs_notifications_min_priority_title": "Priorità minima",
"subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci username e password per iscriverti.",
"subscribe_dialog_login_button_back": "Indietro",
"subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato",
"prefs_notifications_title": "Notifiche",
"prefs_notifications_delete_after_title": "Elimina le notifiche",
"prefs_notifications_min_priority_default_and_higher": "Priorità predefinita e superiore",
"prefs_notifications_min_priority_description_x_or_higher": "Mostra le notifiche se la priorità è {{number}} ({{name}}) o superiore",
"prefs_notifications_delete_after_one_week": "Dopo una settimana",
"prefs_notifications_delete_after_one_month": "Dopo un mese",
"prefs_notifications_delete_after_three_hours_description": "Le notifiche vengono eliminate automaticamente dopo tre ore",
"error_boundary_unsupported_indexeddb_description": "L'app web ntfy ha bisogno di IndexedDB per funzionare e il tuo browser non supporta IndexedDB in modalità di navigazione privata.<br/><br/>Anche se questo è un peccato, non ha molto senso usare il web ntfy app in modalità di navigazione privata comunque, perché tutto è archiviato nella memoria del browser. Puoi leggere di più a riguardo <githubLink>in questo numero di GitHub</githubLink> o parlarci su <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink>."
}

View File

@@ -49,7 +49,7 @@
"publish_dialog_message_label": "メッセージ",
"publish_dialog_email_label": "メール",
"notifications_none_for_any_title": "まだ通知を受信していません。",
"publish_dialog_priority_max": "優先度最高",
"publish_dialog_priority_max": "優先度 最高",
"publish_dialog_button_cancel_sending": "送信をキャンセル",
"publish_dialog_attach_label": "添付URL",
"notifications_none_for_any_description": "トピックに通知を送信するには、トピックURLにPUTまたはPOSTしてください。トピックのひとつを利用した例を示します。",
@@ -60,14 +60,14 @@
"publish_dialog_email_placeholder": "通知を転送するアドレス, 例) phil@example.com",
"notifications_more_details": "詳しい情報は、<websiteLink>ウェブサイト</websiteLink> または <docsLink>ドキュメント</docsLink> を参照してください。",
"publish_dialog_attachment_limits_file_reached": "ファイルサイズ制限 {{fileSizeLimit}} を超えました",
"publish_dialog_priority_min": "優先度最低",
"publish_dialog_priority_low": "優先度低",
"publish_dialog_priority_default": "優先度通常",
"publish_dialog_priority_min": "優先度 最低",
"publish_dialog_priority_low": "優先度 低",
"publish_dialog_priority_default": "優先度 通常",
"publish_dialog_base_url_label": "サービスURL",
"publish_dialog_other_features": "他の機能:",
"notifications_loading": "通知を読み込み中…",
"publish_dialog_attachment_limits_quota_reached": "クォータを超過しました、残り{{remainingBytes}}",
"publish_dialog_priority_high": "優先度高",
"publish_dialog_priority_high": "優先度 高",
"publish_dialog_topic_placeholder": "トピック名の例 phil_alerts",
"publish_dialog_title_placeholder": "通知タイトル 例: ディスクスペース警告",
"publish_dialog_message_placeholder": "メッセージ本文を入力してください",
@@ -152,5 +152,40 @@
"priority_low": "低",
"priority_min": "最低",
"notifications_actions_not_supported": "このアクションはWebアプリではサポートされていません",
"notifications_actions_http_request_title": "{{url}}にHTTP {{method}}を送信"
"notifications_actions_http_request_title": "{{url}}にHTTP {{method}}を送信",
"prefs_users_edit_button": "ユーザーを編集",
"publish_dialog_attached_file_remove": "添付ファイルを削除",
"error_boundary_unsupported_indexeddb_description": "nfty webアプリは動作にIndexedDBを使用しますが、あなたのブラウザはプライベートブラウジングモード時にIndexedDBをサポートしていません。<br/><br/>これは残念なことですが、ntfy webアプリは全ての情報をブラウザストレージに保存して動作するため、プライベートブラウジングモードで利用するのはあまり意味がないかも知れません。詳細については <githubLink>GitHub issue</githubLink>を参照するか、<discordLink>Discord</discordLink>や<matrixLink>Matrix</matrixLink>の議論に参加してください。",
"action_bar_show_menu": "メニューを表示",
"action_bar_logo_alt": "ntfyロゴ",
"action_bar_toggle_mute": "通知をミュート/解除",
"action_bar_toggle_action_menu": "動作メニューを開く/閉じる",
"message_bar_show_dialog": "送信ダイアログを表示",
"message_bar_publish": "メッセージを送信",
"nav_button_muted": "ミュートされた通知",
"nav_button_connecting": "接続中",
"notifications_list": "通知一覧",
"notifications_new_indicator": "新しい通知",
"notifications_list_item": "通知",
"notifications_mark_read": "既読にする",
"notifications_delete": "削除",
"notifications_priority_x": "優先度 {{priority}}",
"notifications_attachment_image": "添付画像",
"notifications_attachment_file_image": "画像ファイル",
"notifications_attachment_file_video": "動画ファイル",
"notifications_attachment_file_audio": "音声ファイル",
"notifications_attachment_file_app": "Androidアプリファイル",
"notifications_attachment_file_document": "その他文書",
"publish_dialog_emoji_picker_show": "絵文字",
"publish_dialog_topic_reset": "トピックをリセット",
"publish_dialog_click_reset": "クリックURLを削除",
"publish_dialog_email_reset": "メール転送を削除",
"publish_dialog_attach_reset": "添付URLを削除",
"publish_dialog_delay_reset": "配信遅延を削除",
"emoji_picker_search_clear": "検索をクリア",
"subscribe_dialog_subscribe_base_url_label": "サーバーURL",
"prefs_notifications_sound_play": "選択されたサウンドを再生",
"prefs_users_table": "ユーザー一覧",
"prefs_users_delete_button": "ユーザーを削除",
"error_boundary_unsupported_indexeddb_title": "プライベートブラウジングはサポートされていません"
}

View File

@@ -153,5 +153,40 @@
"prefs_notifications_sound_description_none": "Notificações não reproduzem nenhum som quando chegam",
"prefs_notifications_sound_description_some": "Notificações reproduzem som {{sound}} quando chegam",
"prefs_notifications_min_priority_description_x_or_higher": "Mostrar notificações se prioridade for {{number}} ({{name}}) ou acima",
"prefs_notifications_delete_after_three_hours_description": "Notificações são automaticamente excluídas após três horas"
"prefs_notifications_delete_after_three_hours_description": "Notificações são automaticamente excluídas após três horas",
"publish_dialog_attach_reset": "Remover URL do anexo",
"publish_dialog_emoji_picker_show": "Escolher emoji",
"publish_dialog_attached_file_remove": "Remover arquivo anexado",
"emoji_picker_search_clear": "Limpar",
"subscribe_dialog_subscribe_base_url_label": "URL de subscrição",
"notifications_list": "Lista de notificações",
"message_bar_show_dialog": "Mostrar caixa de publicação",
"publish_dialog_topic_reset": "Resetar tópico",
"publish_dialog_delay_reset": "Remover entrega adiada da notificação",
"nav_button_connecting": "Conectando",
"publish_dialog_email_reset": "Remover encaminhar email",
"prefs_notifications_sound_play": "Reproduzir som selecionado",
"action_bar_show_menu": "Mostrar menu",
"action_bar_toggle_mute": "Habilita/Desabilita notificações",
"action_bar_toggle_action_menu": "Abrir/fechar menu de ação",
"action_bar_logo_alt": "nfty logo",
"message_bar_publish": "Publicar mensagem",
"nav_button_muted": "Notificações desabilitadas",
"notifications_list_item": "Notificação",
"notifications_mark_read": "Marcar como lido",
"notifications_delete": "Excluir",
"notifications_priority_x": "Prioridade {{priority}}",
"notifications_new_indicator": "Nova notificação",
"notifications_attachment_image": "Imagem anexada",
"notifications_attachment_file_image": "Arquivo de imagem",
"notifications_attachment_file_video": "Arquivo de vídeo",
"notifications_attachment_file_audio": "Arquivo de áudio",
"notifications_attachment_file_app": "Arquivo apk android",
"notifications_attachment_file_document": "Outros documentos",
"publish_dialog_click_reset": "Remover URL clicável",
"prefs_users_table": "Tabela de usuários",
"prefs_users_edit_button": "Editar usuário",
"prefs_users_delete_button": "Excluir usuário",
"error_boundary_unsupported_indexeddb_title": "Navegação anônima não suportada",
"error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.<br/><br/>Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso <githubLink>nesta edição do GitHub</githubLink>, ou falar conosco em <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>."
}

View File

@@ -436,7 +436,7 @@ const Appearance = () => {
const Language = () => {
const { t, i18n } = useTranslation();
const labelId = "prefLanguage";
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇭🇺", "🇧🇷", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇮🇹", "🇭🇺", "🇧🇷", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
const lang = i18n.language ?? "en";
@@ -449,13 +449,14 @@ const Language = () => {
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={lang} onChange={(ev) => i18n.changeLanguage(ev.target.value)} aria-labelledby={labelId}>
<MenuItem value="en">English</MenuItem>
<MenuItem value="id">Bahasa Indonesia</MenuItem>
<MenuItem value="bg">Български</MenuItem>
<MenuItem value="cs">Čeština</MenuItem>
<MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="es">Español</MenuItem>
<MenuItem value="fr">Français</MenuItem>
<MenuItem value="it">Italiano</MenuItem>
<MenuItem value="hu">Magyar</MenuItem>
<MenuItem value="id">Bahasa Indonesia</MenuItem>
<MenuItem value="ja">日本語</MenuItem>
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>