Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2c2bd1e4b | ||
|
|
b003d79ae4 | ||
|
|
a52b024807 | ||
|
|
12b83828bd | ||
|
|
96bb357435 | ||
|
|
6a43c1a126 | ||
|
|
4dabc56952 | ||
|
|
5e510a19a1 | ||
|
|
b627a327d1 | ||
|
|
0b38efd761 | ||
|
|
983dec801a | ||
|
|
01eeb71b9d | ||
|
|
6ba1d7b2a5 | ||
|
|
ff202a042b | ||
|
|
af76a2606d | ||
|
|
98b56c2f06 | ||
|
|
b6afa2fd49 | ||
|
|
e1c07228e5 | ||
|
|
a949748d91 | ||
|
|
125fcd85bb | ||
|
|
2abd6a57ee | ||
|
|
35a691a1bc |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ playground/
|
||||
secrets/
|
||||
*.iml
|
||||
node_modules/
|
||||
.DS_Store
|
||||
|
||||
2
Makefile
2
Makefile
@@ -77,7 +77,7 @@ clean: .PHONY
|
||||
build: web docs cli
|
||||
|
||||
update: web-deps-update cli-deps-update docs-deps-update
|
||||
|
||||
docker pull alpine
|
||||
|
||||
# Documentation
|
||||
|
||||
|
||||
@@ -41,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)"}),
|
||||
@@ -102,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")
|
||||
@@ -147,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"
|
||||
@@ -215,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -319,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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -26,37 +26,37 @@ deb/rpm packages.
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.23.0/ntfy_1.23.0_linux_x86_64.tar.gz
|
||||
tar zxvf ntfy_1.23.0_linux_x86_64.tar.gz
|
||||
sudo cp -a ntfy_1.23.0_linux_x86_64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.23.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.23.0/ntfy_1.23.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_1.23.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_1.23.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.23.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.23.0/ntfy_1.23.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_1.23.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_1.23.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.23.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.23.0/ntfy_1.23.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_1.23.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_1.23.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.23.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.23.0/ntfy_1.23.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.23.0/ntfy_1.23.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.23.0/ntfy_1.23.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.23.0/ntfy_1.23.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.23.0/ntfy_1.23.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.23.0/ntfy_1.23.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.23.0/ntfy_1.23.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.23.0/ntfy_1.23.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,11 +184,11 @@ 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.23.0/ntfy_v1.23.0_macOS_all.tar.gz > ntfy_v1.23.0_macOS_all.tar.gz
|
||||
tar zxvf ntfy_v1.23.0_macOS_all.tar.gz
|
||||
sudo cp -a ntfy_v1.23.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.23.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
|
||||
```
|
||||
|
||||
@@ -200,7 +200,7 @@ ntfy --help
|
||||
|
||||
## 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.23.0/ntfy_v1.23.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).
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -2,9 +2,74 @@
|
||||
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||
|
||||
<!--
|
||||
|
||||
## ntfy 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))
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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>
|
||||
|
||||
18
go.mod
18
go.mod
@@ -14,24 +14,23 @@ 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.7.1
|
||||
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898
|
||||
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.80.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
|
||||
github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
@@ -42,13 +41,14 @@ require (
|
||||
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-20220520000938-2e3eb7b945c2 // 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-20220517211312-f3a8303e98df // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // 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
|
||||
|
||||
40
go.sum
40
go.sum
@@ -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,7 +56,6 @@ 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/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=
|
||||
@@ -71,8 +70,6 @@ github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0 h1:R/qAiUxFT3mNgQaNqJe0IVznjKRNm23ohAIh9lgtlzc=
|
||||
github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0/go.mod h1:v3ZDlfVAL1OrkKHbGSFFK60k0/7hruHPDq2XMs9Gu6U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -171,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=
|
||||
@@ -239,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.7.1 h1:DsAOFeI9T0vmUW4LiGR5mhuCIn5kqGIE4WMU2ytmH00=
|
||||
github.com/urfave/cli/v2 v2.7.1/go.mod h1:TYFbtzt/azQoJOrGH5mDfZtS0jIkl/OeFwlRWPR9KRM=
|
||||
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=
|
||||
@@ -260,8 +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-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0=
|
||||
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/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=
|
||||
@@ -339,8 +337,9 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
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/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
|
||||
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=
|
||||
@@ -360,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=
|
||||
@@ -436,8 +436,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbuf
|
||||
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=
|
||||
@@ -548,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.80.0 h1:IQWaGVCYnsm4MO3hh+WtSXMzMzuyFx/fuR8qkN3A0Qo=
|
||||
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=
|
||||
@@ -630,17 +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/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 h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
|
||||
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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,39 +17,6 @@ const (
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subscriber, error) {
|
||||
fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile))
|
||||
if err != nil {
|
||||
@@ -69,6 +36,26 @@ 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
|
||||
@@ -80,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 {
|
||||
@@ -112,22 +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
|
||||
}
|
||||
apnsData := make(map[string]interface{})
|
||||
for k, v := range data {
|
||||
apnsData[k] = v
|
||||
}
|
||||
apnsConfig = &messaging.APNSConfig{
|
||||
Payload: &messaging.APNSPayload{
|
||||
CustomData: apnsData,
|
||||
Aps: &messaging.Aps{
|
||||
MutableContent: true,
|
||||
Alert: &messaging.ApsAlert{
|
||||
Title: m.Title,
|
||||
Body: maybeTruncateAPNSBodyMessage(m.Message),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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.
|
||||
@@ -137,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
|
||||
@@ -152,3 +136,82 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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==?=
|
||||
|
||||
@@ -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
|
||||
|
||||
902
web/package-lock.json
generated
902
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
191
web/public/static/langs/it.json
Normal file
191
web/public/static/langs/it.json
Normal 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>."
|
||||
}
|
||||
@@ -187,5 +187,6 @@
|
||||
"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_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>."
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user