parent
02f8a32b46
commit
534b93e142
13 changed files with 425 additions and 29 deletions
2
Makefile
2
Makefile
|
@ -50,7 +50,7 @@ docs: docs-deps
|
|||
check: test fmt-check vet lint staticcheck
|
||||
|
||||
test: .PHONY
|
||||
$(GO) test ./...
|
||||
$(GO) test -v ./...
|
||||
|
||||
race: .PHONY
|
||||
$(GO) test -race ./...
|
||||
|
|
155
docs/publish.md
155
docs/publish.md
|
@ -30,6 +30,12 @@ Here's an example showing how to publish a simple message using a POST request:
|
|||
strings.NewReader("Backup successful 😀"))
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/mytopic",
|
||||
data="Backup successful 😀".encode(encoding='utf-8'))
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
|
||||
|
@ -95,6 +101,17 @@ a [title](#message-title), and [tag messages](#tags-emojis) 🥳 🎉. Here's an
|
|||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/phil_alerts",
|
||||
data="Remote access to phils-laptop detected. Act right away.",
|
||||
headers={
|
||||
"Title": "Unauthorized access detected",
|
||||
"Priority": "urgent",
|
||||
"Tags": "warning,skull"
|
||||
})
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/phil_alerts', false, stream_context_create([
|
||||
|
@ -151,6 +168,13 @@ you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
|
|||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/controversial",
|
||||
data="Oh my ...",
|
||||
headers={ "Title": "Dogs are better than cats" })
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/controversial', false, stream_context_create([
|
||||
|
@ -217,6 +241,13 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
|
|||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/phil_alerts",
|
||||
data="An urgent message",
|
||||
headers={ "Priority": "5" })
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/phil_alerts', false, stream_context_create([
|
||||
|
@ -314,6 +345,13 @@ them with a comma, e.g. `tag1,tag2,tag3`.
|
|||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/backups",
|
||||
data="Backup of mailsrv13 failed",
|
||||
headers={ "Tags": "warning,mailsrv13,daily-backup" })
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/backups', false, stream_context_create([
|
||||
|
@ -382,6 +420,13 @@ to be delivered in 3 days, it'll remain in the cache for 3 days and 12 hours. Al
|
|||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/hello",
|
||||
data="Good morning",
|
||||
headers={ "At": "tomorrow, 10am" })
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/backups', false, stream_context_create([
|
||||
|
@ -397,7 +442,6 @@ to be delivered in 3 days, it'll remain in the cache for 3 days and 12 hours. Al
|
|||
|
||||
Here are a few examples (assuming today's date is **12/10/2021, 9am, Eastern Time Zone**):
|
||||
|
||||
|
||||
<table class="remove-md-box"><tr>
|
||||
<td>
|
||||
<table><thead><tr><th><code>Delay/At/In</code> header</th><th>Message will be delivered at</th><th>Explanation</th></tr></thead><tbody>
|
||||
|
@ -411,6 +455,87 @@ Here are a few examples (assuming today's date is **12/10/2021, 9am, Eastern Tim
|
|||
</td>
|
||||
</tr></table>
|
||||
|
||||
## Webhooks (Send via GET)
|
||||
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).
|
||||
|
||||
To send messages via HTTP GET, simply call the `/publish` endpoint (or its aliases `/send` and `/trigger`). Without
|
||||
any arguments, this will send the message `triggered` to the topic. However, you can provide all arguments that are
|
||||
also supported as HTTP headers as URL-encoded arguments. Be sure to check the list of all
|
||||
[supported parameters and headers](#list-of-all-parameters) for details.
|
||||
|
||||
For instance, assuming your topic is `mywebhook`, you can simply call `/mywebhook/trigger` to send a message
|
||||
(aka trigger the webhook):
|
||||
|
||||
=== "Command line (curl)"
|
||||
```
|
||||
curl ntfy.sh/mywebhook/trigger
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
GET /mywebhook/trigger HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
fetch('https://ntfy.sh/mywebhook/trigger')
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
``` go
|
||||
http.Get("https://ntfy.sh/mywebhook/trigger")
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.get("https://ntfy.sh/mywebhook/trigger")
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/mywebhook/trigger');
|
||||
```
|
||||
|
||||
To add a custom message, simply append the `message=` URL parameter. And of course you can set the
|
||||
[message priority](#message-priority), the [message title](#message-title), and [tags](#tags-emojis) as well.
|
||||
For a full list of possible parameters, check the list of [supported parameters and headers](#list-of-all-parameters).
|
||||
|
||||
Here's an example with a custom message, tags and a priority:
|
||||
|
||||
=== "Command line (curl)"
|
||||
```
|
||||
curl "ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull"
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
GET /mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
fetch('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull')
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
``` go
|
||||
http.Get("https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull")
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.get("https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull")
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull');
|
||||
```
|
||||
|
||||
## Advanced features
|
||||
|
||||
### Message caching
|
||||
|
@ -459,6 +584,13 @@ are still delivered to connected subscribers, but [`since=`](subscribe/api.md#fe
|
|||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/mytopic",
|
||||
data="This message won't be stored server-side",
|
||||
headers={ "Cache": "no" })
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
|
||||
|
@ -517,6 +649,13 @@ to `no`. This will instruct the server not to forward messages to Firebase.
|
|||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/mytopic",
|
||||
data="This message won't be forwarded to FCM",
|
||||
headers={ "Firebase": "no" })
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
|
||||
|
@ -529,3 +668,17 @@ to `no`. This will instruct the server not to forward messages to Firebase.
|
|||
]
|
||||
]));
|
||||
```
|
||||
|
||||
## List of all parameters
|
||||
The following is a list of all parameters that can be passed when publishing a message. Parameter names are **case-insensitive**,
|
||||
and can be passed as **HTTP headers** or **query parameters in the URL**.
|
||||
|
||||
| Parameter | Aliases (case-insensitive) | Description |
|
||||
|---|---|---|
|
||||
| `X-Message` | `Message`, `m` | Main body of the message as shown in the notification |
|
||||
| `X-Title` | `Title`, `t` | [Message title](#message-title) |
|
||||
| `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) |
|
||||
| `X-Tags` | `Tags`, `ta` | [Tags and emojis](#tags-emojis) |
|
||||
| `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) |
|
||||
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
|
||||
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
|
||||
|
|
12
examples/publish-python/publish.py
Executable file
12
examples/publish-python/publish.py
Executable file
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
|
||||
resp = requests.get("https://ntfy.sh/mytopic/trigger",
|
||||
data="Backup successful 😀".encode(encoding='utf-8'),
|
||||
headers={
|
||||
"Priority": "high",
|
||||
"Tags": "warning,skull",
|
||||
"Title": "Hello there"
|
||||
})
|
||||
resp.raise_for_status()
|
10
go.mod
10
go.mod
|
@ -2,8 +2,6 @@ module heckel.io/ntfy
|
|||
|
||||
go 1.17
|
||||
|
||||
replace github.com/olebedev/when => github.com/binwiederhier/when v0.0.1-binwiederhier2
|
||||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.6.1 // indirect
|
||||
cloud.google.com/go/storage v1.18.2 // indirect
|
||||
|
@ -11,12 +9,12 @@ require (
|
|||
github.com/BurntSushi/toml v0.4.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.9
|
||||
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254
|
||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
google.golang.org/api v0.62.0
|
||||
google.golang.org/api v0.63.0
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
|
@ -39,12 +37,12 @@ require (
|
|||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||
google.golang.org/grpc v1.42.0 // indirect
|
||||
google.golang.org/grpc v1.43.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
)
|
||||
|
|
19
go.sum
19
go.sum
|
@ -25,7 +25,6 @@ cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aD
|
|||
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
||||
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
|
||||
cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY=
|
||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
|
@ -60,8 +59,6 @@ github.com/BurntSushi/toml v0.4.1/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/binwiederhier/when v0.0.1-binwiederhier2 h1:BjQC7OQI4MK0vXeltn2BEuf0Tdh/M6YNh1JrepnVr2I=
|
||||
github.com/binwiederhier/when v0.0.1-binwiederhier2/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
|
@ -204,6 +201,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|||
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
|
||||
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
|
||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
@ -400,8 +399,8 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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=
|
||||
|
@ -506,8 +505,8 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr
|
|||
google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E=
|
||||
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
|
||||
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
|
||||
google.golang.org/api v0.62.0 h1:PhGymJMXfGBzc4lBRmrx9+1w4w2wEzURHNGF/sD/xGc=
|
||||
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
|
||||
google.golang.org/api v0.63.0 h1:n2bqqK895ygnBpdPDYetfy23K7fJ22wsrZKCyfuRkkA=
|
||||
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
|
||||
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=
|
||||
|
@ -577,8 +576,6 @@ google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ6
|
|||
google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
|
@ -608,8 +605,8 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
|
|||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM=
|
||||
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
|
|
@ -76,7 +76,7 @@ var (
|
|||
jsonRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
|
||||
sseRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
|
||||
rawRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
|
||||
sendRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(send|trigger)$`)
|
||||
sendRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
|
||||
|
||||
staticRegex = regexp.MustCompile(`^/static/.+`)
|
||||
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
||||
|
@ -311,13 +311,12 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
|
|||
return err
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
||||
if err := json.NewEncoder(w).Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.messages++
|
||||
s.mu.Unlock()
|
||||
s.inc(&s.messages)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -691,6 +690,12 @@ func (s *Server) visitor(r *http.Request) *visitor {
|
|||
return v
|
||||
}
|
||||
|
||||
func (s *Server) inc(counter *int64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
*counter++
|
||||
}
|
||||
|
||||
func (s *Server) fail(w http.ResponseWriter, r *http.Request, code int, err error) {
|
||||
log.Printf("[%s] %s - %d - %s", r.RemoteAddr, r.Method, code, err.Error())
|
||||
w.WriteHeader(code)
|
||||
|
|
|
@ -4,10 +4,12 @@ import (
|
|||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/config"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -34,7 +36,7 @@ func TestServer_PublishAndPoll(t *testing.T) {
|
|||
require.Equal(t, "my first message", messages[0].Message)
|
||||
require.Equal(t, "my second\n\nmessage", messages[1].Message)
|
||||
|
||||
response = request(t, s, "GET", "/mytopic/sse?poll=1", "", nil)
|
||||
response = request(t, s, "GET", "/mytopic/sse?poll=1&since=all", "", nil)
|
||||
lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
|
||||
require.Equal(t, 3, len(lines))
|
||||
require.Equal(t, "my first message", toMessage(t, strings.TrimPrefix(lines[0], "data: ")).Message)
|
||||
|
@ -132,6 +134,9 @@ func TestServer_StaticSites(t *testing.T) {
|
|||
rr = request(t, s, "HEAD", "/", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
rr = request(t, s, "OPTIONS", "/", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
rr = request(t, s, "GET", "/does-not-exist.txt", "", nil)
|
||||
require.Equal(t, 404, rr.Code)
|
||||
|
||||
|
@ -150,6 +155,10 @@ func TestServer_StaticSites(t *testing.T) {
|
|||
require.Equal(t, 200, rr.Code)
|
||||
require.Contains(t, rr.Body.String(), `Made with ❤️ by Philipp C. Heckel`)
|
||||
require.Contains(t, rr.Body.String(), `<script src=static/js/extra.js></script>`)
|
||||
|
||||
rr = request(t, s, "GET", "/example.html", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Contains(t, rr.Body.String(), "</html>")
|
||||
}
|
||||
|
||||
func TestServer_PublishLargeMessage(t *testing.T) {
|
||||
|
@ -168,6 +177,34 @@ func TestServer_PublishLargeMessage(t *testing.T) {
|
|||
require.Equal(t, truncated, messages[0].Message)
|
||||
}
|
||||
|
||||
func TestServer_PublishPriority(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
for prio := 1; prio <= 5; prio++ {
|
||||
response := request(t, s, "GET", fmt.Sprintf("/mytopic/publish?priority=%d", prio), fmt.Sprintf("priority %d", prio), nil)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.Equal(t, prio, msg.Priority)
|
||||
}
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/publish?priority=min", "test", nil)
|
||||
require.Equal(t, 1, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "GET", "/mytopic/send?priority=low", "test", nil)
|
||||
require.Equal(t, 2, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "GET", "/mytopic/send?priority=default", "test", nil)
|
||||
require.Equal(t, 3, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "GET", "/mytopic/send?priority=high", "test", nil)
|
||||
require.Equal(t, 4, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "GET", "/mytopic/send?priority=max", "test", nil)
|
||||
require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "GET", "/mytopic/trigger?priority=urgent", "test", nil)
|
||||
require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
|
||||
}
|
||||
|
||||
func TestServer_PublishNoCache(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
|
@ -182,6 +219,7 @@ func TestServer_PublishNoCache(t *testing.T) {
|
|||
messages := toMessages(t, response.Body.String())
|
||||
require.Empty(t, messages)
|
||||
}
|
||||
|
||||
func TestServer_PublishAt(t *testing.T) {
|
||||
c := newTestConfig(t)
|
||||
c.MinDelay = time.Second
|
||||
|
@ -302,6 +340,59 @@ func TestServer_PublishWithNopCache(t *testing.T) {
|
|||
require.Empty(t, messages)
|
||||
}
|
||||
|
||||
func TestServer_PublishAndPollSince(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
request(t, s, "PUT", "/mytopic", "test 1", nil)
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
|
||||
since := time.Now().Unix()
|
||||
request(t, s, "PUT", "/mytopic", "test 2", nil)
|
||||
|
||||
response := request(t, s, "GET", fmt.Sprintf("/mytopic/json?poll=1&since=%d", since), "", nil)
|
||||
messages := toMessages(t, response.Body.String())
|
||||
require.Equal(t, 1, len(messages))
|
||||
require.Equal(t, "test 2", messages[0].Message)
|
||||
}
|
||||
|
||||
func TestServer_PublishViaGET(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/trigger", "", nil)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "triggered", msg.Message)
|
||||
|
||||
response = request(t, s, "GET", "/mytopic/send?message=This+is+a+test&t=This+is+a+title&tags=skull&x-priority=5&delay=24h", "", nil)
|
||||
msg = toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "This is a test", msg.Message)
|
||||
require.Equal(t, "This is a title", msg.Title)
|
||||
require.Equal(t, []string{"skull"}, msg.Tags)
|
||||
require.Equal(t, 5, msg.Priority)
|
||||
require.Greater(t, msg.Time, time.Now().Add(23*time.Hour).Unix())
|
||||
}
|
||||
|
||||
func TestServer_PublishFirebase(t *testing.T) {
|
||||
// This is unfortunately not much of a test, since it merely fires the messages towards Firebase,
|
||||
// but cannot re-read them. There is no way from Go to read the messages back, or even get an error back.
|
||||
// I tried everything. I already had written the test, and it increases the code coverage, so I'll leave it ... :shrug: ...
|
||||
|
||||
c := newTestConfig(t)
|
||||
c.FirebaseKeyFile = firebaseServiceAccountFile(t) // May skip the test!
|
||||
s := newTestServer(t, c)
|
||||
|
||||
// Normal message
|
||||
response := request(t, s, "PUT", "/mytopic", "This is a message for firebase", nil)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
|
||||
// Keepalive message
|
||||
require.Nil(t, s.firebase(newKeepaliveMessage(firebaseControlTopic)))
|
||||
|
||||
time.Sleep(500 * time.Millisecond) // Time for sends
|
||||
}
|
||||
|
||||
func newTestConfig(t *testing.T) *config.Config {
|
||||
conf := config.New(":80")
|
||||
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
||||
|
@ -363,3 +454,15 @@ func toMessage(t *testing.T, s string) *message {
|
|||
require.Nil(t, json.NewDecoder(strings.NewReader(s)).Decode(&m))
|
||||
return &m
|
||||
}
|
||||
|
||||
func firebaseServiceAccountFile(t *testing.T) string {
|
||||
if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE") != "" {
|
||||
return os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE")
|
||||
} else if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT") != "" {
|
||||
filename := filepath.Join(t.TempDir(), "firebase.json")
|
||||
require.NotNil(t, os.WriteFile(filename, []byte(os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT")), 0600))
|
||||
return filename
|
||||
}
|
||||
t.SkipNow()
|
||||
return ""
|
||||
}
|
||||
|
|
2
tools/fbsend/README.md
Normal file
2
tools/fbsend/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# fbsend
|
||||
fbsend is a tiny tool to send data messages to Firebase. It's only used for testing.
|
1
util/embedfs/test.txt
Normal file
1
util/embedfs/test.txt
Normal file
|
@ -0,0 +1 @@
|
|||
This is a test file for embedfs_test.go
|
44
util/embedfs_test.go
Normal file
44
util/embedfs_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
modTime = time.Now()
|
||||
|
||||
//go:embed embedfs
|
||||
testFs embed.FS
|
||||
testFsCached = &CachingEmbedFS{ModTime: modTime, FS: testFs}
|
||||
)
|
||||
|
||||
func TestCachingEmbedFS(t *testing.T) {
|
||||
s := http.FileServer(http.FS(testFsCached))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/embedfs/test.txt", nil)
|
||||
s.ServeHTTP(rr, req)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
lastModified := rr.Header().Get("Last-Modified")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/embedfs/test.txt", nil)
|
||||
req.Header.Set("If-Modified-Since", lastModified)
|
||||
s.ServeHTTP(rr, req)
|
||||
require.Equal(t, 304, rr.Code) // Huzzah!
|
||||
}
|
||||
|
||||
func TestCachingEmbedFS_Range(t *testing.T) {
|
||||
s := http.FileServer(http.FS(testFsCached))
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/embedfs/test.txt", nil)
|
||||
req.Header.Set("Range", "bytes=1-20")
|
||||
s.ServeHTTP(rr, req)
|
||||
require.Equal(t, 206, rr.Code)
|
||||
require.Equal(t, "his is a test file f", rr.Body.String())
|
||||
}
|
|
@ -58,8 +58,3 @@ func (l *Limiter) Value() int64 {
|
|||
defer l.mu.Unlock()
|
||||
return l.value
|
||||
}
|
||||
|
||||
// Limit returns the defined limit
|
||||
func (l *Limiter) Limit() int64 {
|
||||
return l.limit
|
||||
}
|
||||
|
|
30
util/limit_test.go
Normal file
30
util/limit_test.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLimiter_Add(t *testing.T) {
|
||||
l := NewLimiter(10)
|
||||
if err := l.Add(5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := l.Add(5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := l.Add(5); err != ErrLimitReached {
|
||||
t.Fatalf("expected ErrLimitReached, got %#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimiter_AddSub(t *testing.T) {
|
||||
l := NewLimiter(10)
|
||||
l.Add(5)
|
||||
if l.Value() != 5 {
|
||||
t.Fatalf("expected value to be %d, got %d", 5, l.Value())
|
||||
}
|
||||
l.Sub(2)
|
||||
if l.Value() != 3 {
|
||||
t.Fatalf("expected value to be %d, got %d", 3, l.Value())
|
||||
}
|
||||
}
|
56
util/util_test.go
Normal file
56
util/util_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDurationToHuman_SevenDays(t *testing.T) {
|
||||
d := 7 * 24 * time.Hour
|
||||
require.Equal(t, "7d", DurationToHuman(d))
|
||||
}
|
||||
|
||||
func TestDurationToHuman_MoreThanOneDay(t *testing.T) {
|
||||
d := 49 * time.Hour
|
||||
require.Equal(t, "2d1h", DurationToHuman(d))
|
||||
}
|
||||
|
||||
func TestDurationToHuman_LessThanOneDay(t *testing.T) {
|
||||
d := 17*time.Hour + 15*time.Minute
|
||||
require.Equal(t, "17h15m", DurationToHuman(d))
|
||||
}
|
||||
|
||||
func TestDurationToHuman_TenOfThings(t *testing.T) {
|
||||
d := 10*time.Hour + 10*time.Minute + 10*time.Second
|
||||
require.Equal(t, "10h10m10s", DurationToHuman(d))
|
||||
}
|
||||
|
||||
func TestDurationToHuman_Zero(t *testing.T) {
|
||||
require.Equal(t, "0", DurationToHuman(0))
|
||||
}
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
s1 := RandomString(10)
|
||||
s2 := RandomString(10)
|
||||
s3 := RandomString(12)
|
||||
require.Equal(t, 10, len(s1))
|
||||
require.Equal(t, 10, len(s2))
|
||||
require.Equal(t, 12, len(s3))
|
||||
require.NotEqual(t, s1, s2)
|
||||
}
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "somefile.txt")
|
||||
require.Nil(t, ioutil.WriteFile(filename, []byte{0x25, 0x86}, 0600))
|
||||
require.True(t, FileExists(filename))
|
||||
require.False(t, FileExists(filename+".doesnotexist"))
|
||||
}
|
||||
|
||||
func TestInStringList(t *testing.T) {
|
||||
s := []string{"one", "two"}
|
||||
require.True(t, InStringList(s, "two"))
|
||||
require.False(t, InStringList(s, "three"))
|
||||
}
|
Loading…
Reference in a new issue