Delay/At/In header | Message will be delivered at | Explanation |
@@ -411,6 +455,87 @@ Here are a few examples (assuming today's date is **12/10/2021, 9am, Eastern Tim
+## 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) |
diff --git a/examples/publish-python/publish.py b/examples/publish-python/publish.py
new file mode 100755
index 00000000..79bb1fde
--- /dev/null
+++ b/examples/publish-python/publish.py
@@ -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()
diff --git a/go.mod b/go.mod
index 2d4db264..c61d0f99 100644
--- a/go.mod
+++ b/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
)
diff --git a/go.sum b/go.sum
index e04bb27f..c7b18e18 100644
--- a/go.sum
+++ b/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=
diff --git a/server/server.go b/server/server.go
index d5ea31ea..1cb6edad 100644
--- a/server/server.go
+++ b/server/server.go
@@ -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)
diff --git a/server/server_test.go b/server/server_test.go
index cbe24a5f..d06377ad 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -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(), ``)
+
+ rr = request(t, s, "GET", "/example.html", "", nil)
+ require.Equal(t, 200, rr.Code)
+ require.Contains(t, rr.Body.String(), " |