Continuation of the docs

pull/29/head
Philipp Heckel 2021-12-02 17:27:31 -05:00
parent 29f2196376
commit 7ba268887b
14 changed files with 265 additions and 281 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist/ dist/
.idea/ .idea/
site/ site/
server/docs/
*.iml *.iml

View File

@ -88,7 +88,10 @@ staticcheck: .PHONY
# Building targets # Building targets
build-deps: .PHONY docs: .PHONY
mkdocs build
build-deps: docs
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/v7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; } which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/v7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; } which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }

View File

@ -11,34 +11,39 @@ $ ntfy
2021/11/30 19:59:08 Listening on :80 2021/11/30 19:59:08 Listening on :80
``` ```
You can immediately start [publishing messages](publish/index.md), or subscribe via the [Android app](subscribe/phone.md), You can immediately start [publishing messages](publish.md), or subscribe via the [Android app](subscribe/phone.md),
[the web UI](subscribe/web.md), or simply via [curl or your favorite HTTP client](subscribe/api.md). To configure [the web UI](subscribe/web.md), or simply via [curl or your favorite HTTP client](subscribe/api.md). To configure
the server further, check out the [config options table](#config-options) or simply type `ntfy --help` to the server further, check out the [config options table](#config-options) or simply type `ntfy --help` to
get a list of [command line options](#command-line-options). get a list of [command line options](#command-line-options).
## Config options ## Message cache
Each config options can be set in the config file `/etc/ntfy/config.yml` (e.g. `listen-http: :80`) or as a If desired, ntfy can temporarily keep notifications in an in-memory or an on-disk cache. Caching messages for a short period
CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment of time is important to allow [phones](subscribe/phone.md) and other devices with brittle Internet connections to be able to retrieve
variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). notifications that they may have missed.
| Config option | Env variable | Format | Default | Description | By default, ntfy keeps messages in memory for 12 hours, which means that **cached messages do not survive an application
|---|---|---|---|---| restart**. You can override this behavior using the following config settings:
| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. |
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. |
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. |
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 30s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 5000 | Rate limiting: Total number of topics before the server rejects new topics. |
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 10s | Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h. * `cache-file`: if set, ntfy will store messages in a SQLite based cache (default is empty, which means in-memory cache).
**This is required if you'd like messages to be retained across restarts**.
* `cache-duration`: defines the duration for which messages are stored in the cache (default is `12h`)
Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscribe/api.md#polling), as well as the
[`since=` parameter](subscribe/api.md#since).
## Behind a proxy (TLS, etc.)
!!! warning
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise all visitors are rate limited
as if they are one.
**Rate limiting:** If you are running ntfy behind a proxy (e.g. nginx, HAproxy or Apache), you should set the `behind-proxy`
flag. This will instruct the [rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary
identifier for a visitor, as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will
be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address.
**TLS/SSL:** ntfy supports HTTPS/TLS by setting the `listen-https` config option. However, if you are behind a proxy, it is
recommended that TLS/SSL termination is done by the proxy itself.
## Firebase (FCM) ## Firebase (FCM)
!!! info !!! info
@ -47,7 +52,7 @@ The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) is the Google approved way to send [Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) is the Google approved way to send
push messages to Android devices. FCM is the only method that an Android app can receive messages without having to run a push messages to Android devices. FCM is the only method that an Android app can receive messages without having to run a
[foreground service](https://developer.android.com/guide/components/foreground-services). [foreground service](https://developer.android.com/guide/components/foreground-services).
For the main host [ntfy.sh](https://ntfy.sh), the [ntfy Android App](subscribe/phone.md) uses Firebase to send messages For the main host [ntfy.sh](https://ntfy.sh), the [ntfy Android App](subscribe/phone.md) uses Firebase to send messages
to the device. For other hosts, instant delivery is used and FCM is not involved. to the device. For other hosts, instant delivery is used and FCM is not involved.
@ -67,20 +72,6 @@ Example:
firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json" firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
``` ```
## Behind a proxy (TLS, etc.)
!!! warning
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise all visitors are rate limited
as if they are one.
**Rate limiting:** If you are running ntfy behind a proxy (e.g. nginx, HAproxy or Apache), you should set the `behind-proxy`
flag. This will instruct the [rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary
identifier for a visitor, as opposed to the remote IP address. If the `behing-proxy` flag is not set, all visitors will
be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address.
**TLS/SSL:** ntfy supports HTTPS/TLS by setting the `listen-https` config option. However, if you are behind a proxy, it is
recommended that TLS/SSL termination is done by the proxy itself.
## Rate limiting ## Rate limiting
!!! info !!! info
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
@ -108,6 +99,30 @@ request every 10s (defined by `visitor-request-limit-replenish`)
During normal usage, you shouldn't encounter this limit at all, and even if you burst a few requests shortly (e.g. when you During normal usage, you shouldn't encounter this limit at all, and even if you burst a few requests shortly (e.g. when you
reconnect after a connection drop), it shouldn't have any effect. reconnect after a connection drop), it shouldn't have any effect.
## Config options
Each config options can be set in the config file `/etc/ntfy/config.yml` (e.g. `listen-http: :80`) or as a
CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment
variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| Config option | Env variable | Format | Default | Description |
|---|---|---|---|---|
| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. |
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. |
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. |
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 30s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 5000 | Rate limiting: Total number of topics before the server rejects new topics. |
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 10s | Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
## Command line options ## Command line options
``` ```
$ ntfy --help $ ntfy --help

View File

@ -1,5 +1,69 @@
# Building # Building
## ntfy server ## ntfy server
To quickly build on amd64, you can use `make build-simple`:
```
git clone git@github.com:binwiederhier/ntfy.git
cd ntfy
make build-simple
```
That'll generate a statically linked binary in `dist/ntfy_linux_amd64/ntfy`.
For all other platforms (including Docker), and for production or other snapshot builds, you should use the amazingly
awesome [GoReleaser](https://goreleaser.com/) make targets:
```
Build:
make build - Build
make build-snapshot - Build snapshot
make build-simple - Build (using go build, without goreleaser)
make clean - Clean build folder
Releasing (requires goreleaser):
make release - Create a release
make release-snapshot - Create a test release
```
There are currently no platform-specific make targets, so they will build for all platforms (which may take a while).
## Android app ## Android app
The Android app has two flavors:
* **Google Play:** The `play` flavor includes Firebase (FCM) and requires a Firebase account
* **F-Droid:** The `fdroid` flavor does not include Firebase or Google dependencies
First check out the repository:
```
git clone git@github.com:binwiederhier/ntfy-android.git
cd ntfy-android
```
Then either follow the steps for building with or without Firebase.
### Building without Firebase (F-Droid flavor)
Without Firebase, you may want to still change the default `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
if you're self-hosting the server. Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
./gradlew assembleFdroidRelease
# To build a bundle .aab (app/fdroid/release/*.aab)
./gradlew bundleFdroidRelease
```
### Building with Firebase (FCM, Google Play flavor)
To build your own version with Firebase, you must:
* Create a Firebase/FCM account
* Place your account file at `app/google-services.json`
* And change `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
* Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
./gradlew assemblePlayRelease
# To build a bundle .aab (app/play/release/*.aab)
./gradlew bundlePlayRelease
```

View File

@ -1 +1,68 @@
# Examples # Examples
There are a million ways to use ntfy, but here are some inspirations. I try to collect
<a href="https://github.com/binwiederhier/ntfy/tree/main/examples">examples on GitHub</a>, so be sure to check
those out, too.
## A long process is done: backups, copying data, pipelines, ...
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
```
rsync -a root@laptop /backups/laptop \
&& zfs snapshot ... \
&& curl -H prio:low -d "Laptop backup succeeded" ntfy.sh/backups \
|| curl -H tags:warning -H prio:high -d "Laptop backup failed" ntfy.sh/backups
```
## Server-sent messages in your web app
Just as you can [subscribe to topics in the Web UI](subscribe/web.md), you can use ntfy in your own
web application. Check out the <a href="example.html">live example</a> or just look the source of this page.
## Notify on SSH login
Years ago my home server was broken into. That shook me hard, so every time someone logs into any machine that I
own, I now message myself. Here's an example of how to use <a href="https://en.wikipedia.org/wiki/Linux_PAM">PAM</a>
to notify yourself on SSH login.
=== "/etc/pam.d/sshd"
```
# at the end of the file
session optional pam_exec.so /usr/bin/ntfy-ssh-login.sh
```
=== "/usr/bin/ntfy-ssh-login.sh"
```bash
#!/bin/bash
if [ "${PAM_TYPE}" = "open_session" ]; then
curl \
-H prio:high \
-H tags:warning \
-d "SSH login: ${PAM_USER} from ${PAM_RHOST}" \
ntfy.sh/alerts
fi
```
## Collect data from multiple machines
The other day I was running tasks on 20 servers, and I wanted to collect the interim results
as a CSV in one place. Each of the servers was publishing to a topic as the results completed (`publish-result.sh`),
and I had one central collector to grab the results as they came in (`collect-results.sh`).
It looked something like this:
=== "collect-results.sh"
```bash
while read result; do
[ -n "$result" ] && echo "$result" >> results.csv
done < <(stdbuf -i0 -o0 curl -s ntfy.sh/results/raw)
```
=== "publish-result.sh"
```bash
// This script was run on each of the 20 servers. It was doing heavy processing ...
// Publish script results
curl -d "$(hostname),$count,$time" ntfy.sh/results
```

View File

@ -1,6 +1,6 @@
# Install your own ntfy server # Install your own ntfy server
The following steps are only required if you want to **self-host your own ntfy server**. If you just want to The following steps are only required if you want to **self-host your own ntfy server**. If you just want to
[send messages using ntfy.sh](publish/index.md), you don't need to install anything. Just use `curl` [send messages using ntfy.sh](publish.md), you don't need to install anything. Just use `curl`
or your favorite HTTP client. or your favorite HTTP client.
## General steps ## General steps
@ -11,24 +11,23 @@ We support amd64, armv7 and arm64.
2. Then (optionally) edit `/etc/ntfy/config.yml` (see [configuration](config.md)) 2. Then (optionally) edit `/etc/ntfy/config.yml` (see [configuration](config.md))
3. Then just run it with `ntfy` (or `systemctl start ntfy` when using the deb/rpm). 3. Then just run it with `ntfy` (or `systemctl start ntfy` when using the deb/rpm).
## Binaries and packages ## Binaries and packages
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
deb/rpm packages. deb/rpm packages.
x86_64/amd64: **x86_64/amd64:**
``` ```
wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_x86_64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_x86_64.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
``` ```
armv7: **armv7:**
``` ```
wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_armv7.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_armv7.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
``` ```
arm64/v8: **arm64/v8:**
``` ```
wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_arm64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_arm64.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy

View File

@ -41,10 +41,10 @@ Here's an example showing how to publish a simple message using a POST request:
])); ]));
``` ```
If you have the [Android app](../subscribe/phone.md) installed on your phone, this will create a notification that looks like this: If you have the [Android app](subscribe/phone.md) installed on your phone, this will create a notification that looks like this:
<figure markdown> <figure markdown>
![basic notification](../static/img/basic-notification.png){ width=500 } ![basic notification](static/img/basic-notification.png){ width=500 }
<figcaption>Android notification</figcaption> <figcaption>Android notification</figcaption>
</figure> </figure>
@ -111,7 +111,7 @@ a [title](#message-title), and [tag messages](#tags-emojis) 🥳 🎉. Here's an
``` ```
<figure markdown> <figure markdown>
![priority notification](../static/img/priority-notification.png){ width=500 } ![priority notification](static/img/priority-notification.png){ width=500 }
<figcaption>Urgent notification with tags and title</figcaption> <figcaption>Urgent notification with tags and title</figcaption>
</figure> </figure>
@ -165,23 +165,23 @@ you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
``` ```
<figure markdown> <figure markdown>
![notification with title](../static/img/notification-with-title.png){ width=500 } ![notification with title](static/img/notification-with-title.png){ width=500 }
<figcaption>Detail view of notification with title</figcaption> <figcaption>Detail view of notification with title</figcaption>
</figure> </figure>
## Message priority ## Message priority
All messages have a priority, which defines how urgently your phone notifies you. You can set custom All messages have a priority, which defines how urgently your phone notifies you. You can set custom
notification sounds and vibration patterns on your phone to map to these priorities (see [Android config](../subscribe/phone.md)). notification sounds and vibration patterns on your phone to map to these priorities (see [Android config](subscribe/phone.md)).
The following priorities exist: The following priorities exist:
| Priority | Icon | ID | Name | Description | | Priority | Icon | ID | Name | Description |
|---|---|---|---|---| |---|---|---|---|---|
| Max priority | ![min priority](../static/img/priority-5.svg) | `5` | `max`/`urgent` | Really long vibration bursts, default notification sound with a pop-over notification. | | Max priority | ![min priority](static/img/priority-5.svg) | `5` | `max`/`urgent` | Really long vibration bursts, default notification sound with a pop-over notification. |
| High priority | ![min priority](../static/img/priority-4.svg) | `4` | `high` | Long vibration burst, default notification sound with a pop-over notification. | | High priority | ![min priority](static/img/priority-4.svg) | `4` | `high` | Long vibration burst, default notification sound with a pop-over notification. |
| **Default priority** | *(none)* | `3` | `default` | Short default vibration and sound. Default notification behavior. | | **Default priority** | *(none)* | `3` | `default` | Short default vibration and sound. Default notification behavior. |
| Low priority | ![min priority](../static/img/priority-2.svg) |`2` | `low` | No vibration or sound. Notification will not visibly show up until notification drawer is pulled down. | | Low priority | ![min priority](static/img/priority-2.svg) |`2` | `low` | No vibration or sound. Notification will not visibly show up until notification drawer is pulled down. |
| Min priority | ![min priority](../static/img/priority-1.svg) | `1` | `min` | No vibration or sound. The notification will be under the fold in "Other notifications". | | Min priority | ![min priority](static/img/priority-1.svg) | `1` | `min` | No vibration or sound. The notification will be under the fold in "Other notifications". |
You can set the priority with the header `X-Priority` (or any of its aliases: `Priority`, `prio`, or `p`). You can set the priority with the header `X-Priority` (or any of its aliases: `Priority`, `prio`, or `p`).
@ -231,19 +231,19 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
``` ```
<figure markdown> <figure markdown>
![priority notification](../static/img/priority-detail-overview.png){ width=500 } ![priority notification](static/img/priority-detail-overview.png){ width=500 }
<figcaption>Detail view of priority notifications</figcaption> <figcaption>Detail view of priority notifications</figcaption>
</figure> </figure>
## Tags & emojis 🥳 🎉 ## Tags & emojis 🥳 🎉
You can tag messages with emojis and other relevant strings: 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 * **Emojis**: If a tag matches an [emoji short code](emojis.md), it'll be converted to an emoji and prepended
to title or message. to title or message.
* **Other tags:** If a tag doesn't match, it will be listed below the notification. * **Other tags:** If a tag doesn't match, it will be listed below the notification.
This feature is useful for things like warnings (⚠️, ️🚨, or 🚩), but also to simply tag messages otherwise (e.g. script This feature is useful for things like warnings (⚠️, ️🚨, or 🚩), but also to simply tag messages otherwise (e.g. script
names, hostnames, etc.). Use [the emoji short code list](../emojis.md) to figure out what tags can be converted to emojis. names, hostnames, etc.). Use [the emoji short code list](emojis.md) to figure out what tags can be converted to emojis.
Here's an **excerpt of emojis** I've found very useful in alert messages: Here's an **excerpt of emojis** I've found very useful in alert messages:
<table class="remove-md-box"><tr> <table class="remove-md-box"><tr>
@ -328,7 +328,7 @@ them with a comma, e.g. `tag1,tag2,tag3`.
``` ```
<figure markdown> <figure markdown>
![priority notification](../static/img/notification-with-tags.png){ width=500 } ![priority notification](static/img/notification-with-tags.png){ width=500 }
<figcaption>Detail view of notifications with tags</figcaption> <figcaption>Detail view of notifications with tags</figcaption>
</figure> </figure>

View File

@ -1 +1,31 @@
# Subscribe from your phone # Subscribe via API
## Fetching cached messages
Messages may be cached for a couple of hours (see [message caching](../config.md#message-cache)) to account for network
interruptions of subscribers. If the server has configured message caching, you can read back what you missed by using
the `since=` query parameter. It takes either a duration (e.g. `10m` or `30s`), a Unix timestamp (e.g. `1635528757`)
or `all` (all cached messages).
```
curl -s "ntfy.sh/mytopic/json?since=10m"
```
## Polling
You can also just poll for messages if you don't like the long-standing connection using the `poll=1`
query parameter. The connection will end after all available messages have been read. This parameter can be
combined with `since=` (defaults to `since=all`).
```
curl -s "ntfy.sh/mytopic/json?poll=1"
```
## Subscribing to multiple topics
It's possible to subscribe to multiple topics in one HTTP call by providing a
comma-separated list of topics in the URL. This allows you to reduce the number of connections you have to maintain:
```
$ curl -s ntfy.sh/mytopic1,mytopic2/json
{"id":"0OkXIryH3H","time":1637182619,"event":"open","topic":"mytopic1,mytopic2,mytopic3"}
{"id":"dzJJm7BCWs","time":1637182634,"event":"message","topic":"mytopic1","message":"for topic 1"}
{"id":"Cm02DsxUHb","time":1637182643,"event":"message","topic":"mytopic2","message":"for topic 2"}
```

View File

@ -1 +1,7 @@
# Subscribe from your phone # 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).
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="static/img/badge-googleplay.png"></a>
<a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="static/img/badge-appstore.png"></a>

View File

@ -1 +0,0 @@
# Subscribe from your phone

View File

@ -1 +0,0 @@
# Subscribe from your phone

View File

@ -1,4 +1,5 @@
site_name: ntfy.sh site_dir: server/docs
site_name: ntfy
site_url: https://ntfy.sh site_url: https://ntfy.sh
site_description: simple HTTP-based pub-sub site_description: simple HTTP-based pub-sub
copyright: Made with ❤️ by Philipp C. Heckel copyright: Made with ❤️ by Philipp C. Heckel
@ -8,7 +9,6 @@ edit_uri: edit/main/docs/
theme: theme:
name: material name: material
# custom_dir: docs/overrides
language: en language: en
logo: static/img/ntfy.png logo: static/img/ntfy.png
favicon: static/img/favicon.png favicon: static/img/favicon.png
@ -69,14 +69,11 @@ nav:
- "Installation": install.md - "Installation": install.md
- "Configuration": config.md - "Configuration": config.md
- "Publishing": - "Publishing":
- "Sending messages": publish/index.md - "Sending messages": publish.md
- "Subscribing": - "Subscribing":
- "From the Android/iOS app": subscribe/phone.md - "From the Android/iOS app": subscribe/phone.md
- "From the Web UI": subscribe/web.md - "From the Web UI": subscribe/web.md
- "Using the API": - "Using the API": subscribe/api.md
- "Basic API usage": subscribe/api.md
- "Fetching cached messages": subscribe/since.md
- "Polling": subscribe/poll.md
- "Other things": - "Other things":
- "Examples": examples.md - "Examples": examples.md
- "Emojis 🥳 🎉": emojis.md - "Emojis 🥳 🎉": emojis.md

View File

@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ntfy.sh | simple HTTP-based pub-sub</title> <title>ntfy.sh | PUT/POST push notifications to your phone</title>
<link rel="stylesheet" href="static/css/app.css" type="text/css"> <link rel="stylesheet" href="static/css/app.css" type="text/css">
<!-- Mobile view --> <!-- Mobile view -->
@ -24,7 +24,7 @@
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta property="og:site_name" content="ntfy.sh" /> <meta property="og:site_name" content="ntfy.sh" />
<meta property="og:title" content="ntfy.sh | simple HTTP-based pub-sub" /> <meta property="og:title" content="ntfy.sh | send push notifications to your phone via simple PUT/POST requests" />
<meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." /> <meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
<meta property="og:image" content="/static/img/ntfy.png" /> <meta property="og:image" content="/static/img/ntfy.png" />
<meta property="og:url" content="https://ntfy.sh" /> <meta property="og:url" content="https://ntfy.sh" />
@ -36,7 +36,7 @@
<body> <body>
<div id="header"><div id="headerBox"><img src="static/img/ntfy.png" alt="ntfy"/></div></div> <div id="header"><div id="headerBox"><img src="static/img/ntfy.png" alt="ntfy"/></div></div>
<div id="main"{{if .Topic}} style="display: none"{{end}}> <div id="main"{{if .Topic}} style="display: none"{{end}}>
<h1>ntfy.sh | simple HTTP-based pub-sub</h1> <h1>ntfy.sh | PUT/POST push notifications to your phone</h1>
<p> <p>
<b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based <a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">pub-sub</a> notification service. <b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based <a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">pub-sub</a> notification service.
It allows you to send notifications to your phone or desktop via scripts from any computer, It allows you to send notifications to your phone or desktop via scripts from any computer,
@ -67,15 +67,6 @@
<code> <code>
curl -d "Backup successful 😀" <span class="ntfyUrl">ntfy.sh</span>/mytopic curl -d "Backup successful 😀" <span class="ntfyUrl">ntfy.sh</span>/mytopic
</code> </code>
<p class="smallMarginBottom">
Here's an example in JS with <tt>fetch()</tt> (see <a href="https://github.com/binwiederhier/ntfy/tree/main/examples">full example</a>):
</p>
<code>
fetch('https://<span class="ntfyUrl">ntfy.sh</span>/mytopic', {<br/>
&nbsp;&nbsp;method: 'POST', // PUT works too<br/>
&nbsp;&nbsp;body: 'Hello from the other side.'<br/>
})
</code>
<p class="smallMarginBottom"> <p class="smallMarginBottom">
There are <a href="#other-features">more features</a> related to publishing messages: You can set a There are <a href="#other-features">more features</a> related to publishing messages: You can set a
<a href="#priority">notification priority</a>, a <a href="#title">title</a>, and <a href="#tags">tag messages</a>. <a href="#priority">notification priority</a>, a <a href="#title">title</a>, and <a href="#tags">tag messages</a>.
@ -169,208 +160,6 @@
And another one with a smiley face 😀 And another one with a smiley face 😀
</code> </code>
<h2 id="other-features" class="anchor">Other features</h2>
<h3 id="fetching-cached-messages" class="anchor">Fetching cached messages (<tt>since=</tt>)</h3>
<p class="smallMarginBottom">
Messages are cached on disk for {{.CacheDuration}} to account for network interruptions of subscribers.
You can read back what you missed by using the <tt>since=</tt> query parameter. It takes either a
duration (e.g. <tt>10m</tt> or <tt>30s</tt>), a Unix timestamp (e.g. <tt>1635528757</tt>) or <tt>all</tt> (all
cached messages).
</p>
<code>
curl -s "<span class="ntfyUrl">ntfy.sh</span>/mytopic/json?since=10m"
</code>
<h3 id="polling" class="anchor">Polling (<tt>poll=1</tt>)</h3>
<p class="smallMarginBottom">
You can also just poll for messages if you don't like the long-standing connection using the <tt>poll=1</tt>
query parameter. The connection will end after all available messages have been read. This parameter can be
combined with <tt>since=</tt> (defaults to <tt>since=all</tt>).
</p>
<code>
curl -s "<span class="ntfyUrl">ntfy.sh</span>/mytopic/json?poll=1"
</code>
<h3 id="multiple-topics" class="anchor">Subscribing to multiple topics (<tt>topic1,topic2,...</tt>)</h3>
<p class="smallMarginBottom">
It's possible to subscribe to multiple topics in one HTTP call by providing a
comma-separated list of topics in the URL. This allows you to reduce the number of connections you have to maintain:
</p>
<code>
$ curl -s <span class="ntfyUrl">ntfy.sh</span>/mytopic1,mytopic2/json<br/>
{"id":"0OkXIryH3H","time":1637182619,"event":"open","topic":"mytopic1,mytopic2,mytopic3"}<br/>
{"id":"dzJJm7BCWs","time":1637182634,"event":"message","topic":"mytopic1","message":"for topic 1"}<br/>
{"id":"Cm02DsxUHb","time":1637182643,"event":"message","topic":"mytopic2","message":"for topic 2"}
</code>
<h3 id="priority" class="anchor">Message priority (<tt>X-Priority</tt>, <tt>Priority</tt>, <tt>prio</tt>, or <tt>p</tt>)</h3>
<p>
All messages have a priority, which defines how your urgently your phone notifies you. You can set custom
notification sounds and vibration patterns on your phone to map to these priorities.
</p>
<p class="smallMarginBottom">
The following priorities exist: <tt>1</tt> (<tt>min</tt>), <tt>2</tt> (<tt>low</tt>), <tt>3</tt> (<tt>default</tt>),
<tt>4</tt> (<tt>high</tt>), and <tt>5</tt> (<tt>max</tt>/<tt>urgent</tt>). You can set the priority with the
header <tt>X-Priority</tt> (or any of its aliases: <tt>Priority</tt>, <tt>prio</tt>, or <tt>p</tt>). Here are a few examples:
</p>
<code>
curl -H "X-Priority: urgent" -d "An urgent message" <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
curl -H "Priority: 2" -d "Low priority message" <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
curl -H p:4 -d "A high priority message" <span class="ntfyUrl">ntfy.sh</span>/mytopic
</code>
<h3 id="title" class="anchor">Notification title (<tt>X-Title</tt>, <tt>Title</tt>, <tt>ti</tt>, or <tt>t</tt>)</h3>
<p class="smallMarginBottom">
The notification title is typically set to the topic short URL (e.g. <tt><span class="ntfyUrl">ntfy.sh</span>/mytopic</tt>.
To override it, you can set the <tt>X-Title</tt> header (or any of its aliases: <tt>Title</tt>, <tt>ti</tt>, or <tt>t</tt>).
</p>
<code>
curl -H "Title: Dogs are better than cats" -d "Oh my ..." <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
</code>
<h3 id="tags" class="anchor">Tags &amp; emojis 🥳 🎉 (<tt>X-Tags</tt>, <tt>Tags</tt>, or <tt>ta</tt>)</h3>
<p>
You can tag messages with emojis (or other relevant strings). If a tag matches a <a href="https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json">known emoji short code</a>,
it will be converted to an emoji. If it doesn't match, it will be listed below the notification. This is useful
for things like warnings and such (⚠️, ️🚨, or 🚩), but also to simply tag messages otherwise (e.g. which script the
message came from, ...).
</p>
<p class="smallMarginBottom">
You can set tags with the <tt>X-Tags</tt> header (or any of its aliases: <tt>Tags</tt>, or <tt>ta</tt>).
Use <a href="https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json">this reference</a>
to figure out what tags can be converted to emojis. In the example below, the tag "warning" matches the emoji ⚠️,
the tag "ssh-login" doesn't match and will be displayed below the message.
</p>
<code>
$ curl -H "Tags: warning,ssh-login" -d "Unauthorized SSH access" <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
{"id":"ZEIwjfHlSS",...,"tags":["warning","ssh-login"],"message":"Unauthorized SSH access"}
</code>
<h2 id="examples" class="anchor">Examples</h2>
<p>
There are a million ways to use ntfy, but here are some inspirations. I try to collect
<a href="https://github.com/binwiederhier/ntfy/tree/main/examples">examples on GitHub</a>, so be sure to check
those out, too.
</p>
<h3 id="example-alerts" class="anchor">Example: A long process is done: backups, copying data, pipelines, ...</h3>
<p class="smallMarginBottom">
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
</p>
<code>
rsync -a root@laptop /backups/laptop \<br/>
&nbsp;&nbsp;&& zfs snapshot ... \<br/>
&nbsp;&nbsp;&& curl -d "Laptop backup succeeded" <span class="ntfyUrl">ntfy.sh</span>/backups \<br/>
&nbsp;&nbsp;|| curl -H tags:warning -H prio:high -d "Laptop backup failed" <span class="ntfyUrl">ntfy.sh</span>/backups
</code>
<h3 id="example-web" class="anchor">Example: Server-sent messages in your web app</h3>
<p>
Just as you can <a href="#subscribe-web">subscribe to topics in this Web UI</a>, you can use ntfy in your own
web application. Check out the <a href="example.html">live example</a> or just look the source of this page.
</p>
<h3 id="example-notify-ssh" class="anchor">Example: Notify on SSH login</h3>
<p>
Years ago my home server was broken into. That shook me hard, so every time someone logs into any machine that I
own, I now message myself. Here's an example of how to use <a href="https://en.wikipedia.org/wiki/Linux_PAM">PAM</a>
to notify yourself on SSH login.
</p>
<p class="smallMarginBottom">
<b>/etc/pam.d/sshd</b> (at the end of the file):
</p>
<code>
session optional pam_exec.so /usr/local/bin/ntfy-ssh-login.sh
</code>
<p class="smallMarginBottom">
<b>/usr/local/bin/ntfy-ssh-login.sh</b>:
</p>
<code>
#!/bin/bash<br/>
if [ "${PAM_TYPE}" = "open_session" ]; then<br/>
&nbsp;&nbsp;curl -H tags:warning -d "SSH login: ${PAM_USER} from ${PAM_RHOST}" <span class="ntfyUrl">ntfy.sh</span>/alerts<br/>
fi
</code>
<h3 id="example-collect-data" class="anchor">Example: Collect data from multiple machines</h3>
<p>
The other day I was running tasks on 20 servers and I wanted to collect the interim results
as a CSV in one place. Here's the script I wrote:
</p>
<code>
while read result; do<br/>
&nbsp;&nbsp;[ -n "$result" ] && echo "$result" >> results.csv<br/>
done < <(stdbuf -i0 -o0 curl -s <span class="ntfyUrl">ntfy.sh</span>/results/raw)
</code>
<h2 id="faq" class="anchor">FAQ</h2>
<p>
<b id="isnt-this-like" class="anchor">Isn't this like ...?</b><br/>
Who knows. I didn't do a lot of research before making this. It was fun making it.
</p>
<p>
<b id="is-it-free" class="anchor">Can I use this in my app? Will it stay free?</b><br/>
Yes. As long as you don't abuse it, it'll be available and free of charge. I do not plan on monetizing
the service.
</p>
<p>
<b id="uptime-guarantees" class="anchor">What are the uptime guarantees?</b><br/>
Best effort.
</p>
<p>
<b id="multiple-subscribers" class="anchor">What happens if there are multiple subscribers to the same topic?</b><br/>
As per usual with pub-sub, all subscribers receive notifications if they are
subscribed to a topic.
</p>
<p>
<b id="can-you-spy-on-me" class="anchor">Will you know what topics exist, can you spy on me?</b><br/>
If you don't trust me or your messages are sensitive, run your own server. It's <a href="https://github.com/binwiederhier/ntfy">open source</a>.
That said, the logs do not contain any topic names or other details about you.
Messages are cached for {{.CacheDuration}} to facilitate service restarts, message polling and to overcome
client network disruptions.
</p>
<p>
<b id="selfhosted" class="anchor">Can I self-host it?</b><br/>
Yes. The server (including this Web UI) can be self-hosted, and the Android app supports adding topics from
your own server as well. There are <a href="https://github.com/binwiederhier/ntfy#installation">install instructions</a>
on GitHub.
</p>
<p>
<b id="why-firebase" class="anchor">Why is Firebase used?</b><br/>
In addition to caching messages locally and delivering them to long-polling subscribers, all messages are also
published to Firebase Cloud Messaging (FCM) (if <tt>FirebaseKeyFile</tt> is set, which it is on ntfy.sh). This
is to facilitate instant notifications on Android.
</p>
<p>
<b id="battery-usage" class="anchor">How much battery does the Android app use?</b><br/>
If you use the ntfy.sh server and you don't use the <i>instant delivery</i> feature, the Android app uses no
additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server, or you use
<i>instant delivery</i>, the app has to maintain a constant connection to the server, which consumes about 4% of
battery in 17h of use (on my phone). I use it and it makes no difference to me.
</p>
<p>
<b id="instant-delivery" class="anchor">What is instant delivery?</b><br/>
Instant delivery is a feature in the Android app. If turned on, the app maintains a constant connection to the
server and listens for incoming notifications. This consumes <a href="#battery-usage">additional battery</a>,
but delivers notifications instantly.
</p>
<p>
<b id="why-no-ios" class="anchor">Why is there no iOS app (yet)?</b><br/>
I don't have an iPhone or a Mac, so I didn't make an iOS app yet. It'd be awesome if
<a href="https://github.com/binwiederhier/ntfy/issues/4">someone else could help out</a>.
</p>
<h2 id="privacy" class="anchor">Privacy policy</h2> <h2 id="privacy" class="anchor">Privacy policy</h2>
<p> <p>
Neither the server nor the app record any personal information, or share any of the messages and topics with Neither the server nor the app record any personal information, or share any of the messages and topics with

View File

@ -83,6 +83,7 @@ var (
rawRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`) rawRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
staticRegex = regexp.MustCompile(`^/static/.+`) staticRegex = regexp.MustCompile(`^/static/.+`)
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
//go:embed "index.gohtml" //go:embed "index.gohtml"
indexSource string indexSource string
@ -95,6 +96,10 @@ var (
webStaticFs embed.FS webStaticFs embed.FS
webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs} webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs}
//go:embed docs
docsStaticFs embed.FS
docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
errHTTPBadRequest = &errHTTP{http.StatusBadRequest, http.StatusText(http.StatusBadRequest)} errHTTPBadRequest = &errHTTP{http.StatusBadRequest, http.StatusText(http.StatusBadRequest)}
errHTTPNotFound = &errHTTP{http.StatusNotFound, http.StatusText(http.StatusNotFound)} errHTTPNotFound = &errHTTP{http.StatusNotFound, http.StatusText(http.StatusNotFound)}
errHTTPTooManyRequests = &errHTTP{http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)} errHTTPTooManyRequests = &errHTTP{http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)}
@ -203,7 +208,8 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
if r.Method == http.MethodGet && (r.URL.Path == "/" || topicRegex.MatchString(r.URL.Path)) { log.Printf(r.URL.Path)
if r.Method == http.MethodGet && r.URL.Path == "/" {
return s.handleHome(w, r) return s.handleHome(w, r)
} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" { } else if r.Method == http.MethodGet && r.URL.Path == "/example.html" {
return s.handleExample(w, r) return s.handleExample(w, r)
@ -211,8 +217,12 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
return s.handleEmpty(w, r) return s.handleEmpty(w, r)
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
return s.handleStatic(w, r) return s.handleStatic(w, r)
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
return s.handleDocs(w, r)
} else if r.Method == http.MethodOptions { } else if r.Method == http.MethodOptions {
return s.handleOptions(w, r) return s.handleOptions(w, r)
} else if r.Method == http.MethodGet && topicRegex.MatchString(r.URL.Path) {
return s.handleHome(w, r)
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicRegex.MatchString(r.URL.Path) { } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicRegex.MatchString(r.URL.Path) {
return s.withRateLimit(w, r, s.handlePublish) return s.withRateLimit(w, r, s.handlePublish)
} else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) {
@ -246,6 +256,11 @@ func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
http.FileServer(http.FS(docsStaticCached)).ServeHTTP(w, r)
return nil
}
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error { func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
t, err := s.topicFromID(r.URL.Path[1:]) t, err := s.topicFromID(r.URL.Path[1:])
if err != nil { if err != nil {