From 75bef92417cb52b7bc1d919f71130fc1ce067639 Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Mon, 3 Jan 2022 11:49:39 -0500 Subject: [PATCH 1/7] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 487d5dea..ce36b19d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ [![Tests](https://github.com/binwiederhier/ntfy/workflows/test/badge.svg)](https://github.com/binwiederhier/ntfy/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/binwiederhier/ntfy)](https://goreportcard.com/report/github.com/binwiederhier/ntfy) [![codecov](https://codecov.io/gh/binwiederhier/ntfy/branch/main/graph/badge.svg?token=A597KQ463G)](https://codecov.io/gh/binwiederhier/ntfy) -[![Discord](https://img.shields.io/discord/874398661709295626)](https://discord.gg/cT7ECsZj9w) +[![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w) +[![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org) [![Healthcheck](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)](https://ntfy.statuspage.io/) **ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service. @@ -36,8 +37,9 @@ too. I welcome any and all contributions. Just create a PR or an issue. ## Contact me -You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)**, or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), -or find more contact information [on my website](https://heckel.io/about). +You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org) +(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information +[on my website](https://heckel.io/about). ## License Made with ❤️ by [Philipp C. Heckel](https://heckel.io). From 722c579db0143221ec539917bf3f7a9d0b54c955 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 4 Jan 2022 19:59:54 +0100 Subject: [PATCH 2/7] Increase FCM priority for ntfy priority high and max, closes #70 --- server/server.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server/server.go b/server/server.go index 9cf76dea..14017964 100644 --- a/server/server.go +++ b/server/server.go @@ -213,9 +213,16 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) { "message": m.Message, } } + var androidConfig *messaging.AndroidConfig + if m.Priority >= 4 { + androidConfig = &messaging.AndroidConfig{ + Priority: "high", + } + } _, err := msg.Send(context.Background(), &messaging.Message{ - Topic: m.Topic, - Data: data, + Topic: m.Topic, + Data: data, + Android: androidConfig, }) return err }, nil From 807d2b0d9dbbe992b22473e06365d10d6b18d832 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 4 Jan 2022 20:43:37 +0100 Subject: [PATCH 3/7] Truncate FCM messages if they are too long; This was trickier than expected; relates to #84 --- server/server.go | 23 +++++++++++++++++++++-- server/server_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/server/server.go b/server/server.go index 14017964..035821d7 100644 --- a/server/server.go +++ b/server/server.go @@ -138,6 +138,7 @@ var ( const ( firebaseControlTopic = "~control" // See Android if changed emptyMessageBody = "triggered" + fcmMessageLimitReal = 4100 // see maybeTruncateFCMMessage for details ) // New instantiates a new Server. It creates the cache and adds a Firebase @@ -219,15 +220,33 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) { Priority: "high", } } - _, err := msg.Send(context.Background(), &messaging.Message{ + _, err := msg.Send(context.Background(), maybeTruncateFCMMessage(&messaging.Message{ Topic: m.Topic, Data: data, Android: androidConfig, - }) + })) return err }, nil } +// maybeTruncateFCMMessage performs best-effort truncation of FCM messages. +// The docs says the limit is 4000 characters, but the real FCM message limit is 4100 of the +// serialized payload; I tested this diligently. +func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message { + s, err := json.Marshal(m) + if err != nil { + return m + } + if len(s) > fcmMessageLimitReal { + over := len(s) - fcmMessageLimitReal + message, ok := m.Data["message"] + if ok && len(message) > over { + m.Data["message"] = message[:len(message)-over] + } + } + return m +} + // Run executes the main server. It listens on HTTP (+ HTTPS, if configured), and starts // a manager go routine to print stats and prune messages. func (s *Server) Run() error { diff --git a/server/server_test.go b/server/server_test.go index e713e604..b9bcf8b7 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "firebase.google.com/go/messaging" "fmt" "github.com/stretchr/testify/require" "net/http" @@ -591,6 +592,35 @@ func TestServer_UnifiedPushDiscovery(t *testing.T) { require.Equal(t, `{"unifiedpush":{"version":1}}`+"\n", response.Body.String()) } +func TestServer_MaybeTruncateFCMMessage(t *testing.T) { + origMessage := strings.Repeat("this is a long string", 300) + origFCMMessage := &messaging.Message{ + Topic: "mytopic", + Data: map[string]string{ + "id": "abcdefg", + "time": "1641324761", + "event": "message", + "topic": "mytopic", + "priority": "0", + "tags": "", + "title": "", + "message": origMessage, + }, + Android: &messaging.AndroidConfig{ + Priority: "high", + }, + } + origMessageLength := len(origFCMMessage.Data["message"]) + serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage) + require.Greater(t, len(serializedOrigFCMMessage), fcmMessageLimitReal) // Pre-condition + + truncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage) + truncatedMessageLength := len(truncatedFCMMessage.Data["message"]) + serializedTruncatedFCMMessage, _ := json.Marshal(truncatedFCMMessage) + require.Equal(t, fcmMessageLimitReal, len(serializedTruncatedFCMMessage)) + require.NotEqual(t, origMessageLength, truncatedMessageLength) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") From 76d102f964abf35032397eec8a79c022fe1e845c Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 4 Jan 2022 20:53:32 +0100 Subject: [PATCH 4/7] Add "truncated" flag to let Android app know --- server/server.go | 3 ++- server/server_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index 035821d7..a9799c6e 100644 --- a/server/server.go +++ b/server/server.go @@ -238,9 +238,10 @@ func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message { return m } if len(s) > fcmMessageLimitReal { - over := len(s) - fcmMessageLimitReal + over := len(s) - fcmMessageLimitReal + 16 // = len("truncated":"1",), sigh ... message, ok := m.Data["message"] if ok && len(message) > over { + m.Data["truncated"] = "1" m.Data["message"] = message[:len(message)-over] } } diff --git a/server/server_test.go b/server/server_test.go index b9bcf8b7..92952a3a 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -618,9 +618,37 @@ func TestServer_MaybeTruncateFCMMessage(t *testing.T) { truncatedMessageLength := len(truncatedFCMMessage.Data["message"]) serializedTruncatedFCMMessage, _ := json.Marshal(truncatedFCMMessage) require.Equal(t, fcmMessageLimitReal, len(serializedTruncatedFCMMessage)) + require.Equal(t, "1", truncatedFCMMessage.Data["truncated"]) require.NotEqual(t, origMessageLength, truncatedMessageLength) } +func TestServer_MaybeTruncateFCMMessage_NotTooLong(t *testing.T) { + origMessage := "not really a long string" + origFCMMessage := &messaging.Message{ + Topic: "mytopic", + Data: map[string]string{ + "id": "abcdefg", + "time": "1641324761", + "event": "message", + "topic": "mytopic", + "priority": "0", + "tags": "", + "title": "", + "message": origMessage, + }, + } + origMessageLength := len(origFCMMessage.Data["message"]) + serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage) + require.LessOrEqual(t, len(serializedOrigFCMMessage), fcmMessageLimitReal) // Pre-condition + + notTruncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage) + notTruncatedMessageLength := len(notTruncatedFCMMessage.Data["message"]) + serializedNotTruncatedFCMMessage, _ := json.Marshal(notTruncatedFCMMessage) + require.Equal(t, origMessageLength, notTruncatedMessageLength) + require.Equal(t, len(serializedOrigFCMMessage), len(serializedNotTruncatedFCMMessage)) + require.Equal(t, "", notTruncatedFCMMessage.Data["truncated"]) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") From 2e40b895a707b787d7243bff98dfd6617557c00d Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 4 Jan 2022 21:09:47 +0100 Subject: [PATCH 5/7] Fix message truncation, relates to #84 --- server/server.go | 10 +++++----- server/server_test.go | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/server.go b/server/server.go index a9799c6e..8a96c28e 100644 --- a/server/server.go +++ b/server/server.go @@ -138,7 +138,7 @@ var ( const ( firebaseControlTopic = "~control" // See Android if changed emptyMessageBody = "triggered" - fcmMessageLimitReal = 4100 // see maybeTruncateFCMMessage for details + fcmMessageLimit = 4000 // see maybeTruncateFCMMessage for details ) // New instantiates a new Server. It creates the cache and adds a Firebase @@ -230,15 +230,15 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) { } // maybeTruncateFCMMessage performs best-effort truncation of FCM messages. -// The docs says the limit is 4000 characters, but the real FCM message limit is 4100 of the -// serialized payload; I tested this diligently. +// The docs say the limit is 4000 characters, but during testing it wasn't quite clear +// what fields matter; so we're just capping the serialized JSON to 4000 bytes. func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message { s, err := json.Marshal(m) if err != nil { return m } - if len(s) > fcmMessageLimitReal { - over := len(s) - fcmMessageLimitReal + 16 // = len("truncated":"1",), sigh ... + if len(s) > fcmMessageLimit { + over := len(s) - fcmMessageLimit + 16 // = len("truncated":"1",), sigh ... message, ok := m.Data["message"] if ok && len(message) > over { m.Data["truncated"] = "1" diff --git a/server/server_test.go b/server/server_test.go index 92952a3a..ac956a07 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -612,12 +612,12 @@ func TestServer_MaybeTruncateFCMMessage(t *testing.T) { } origMessageLength := len(origFCMMessage.Data["message"]) serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage) - require.Greater(t, len(serializedOrigFCMMessage), fcmMessageLimitReal) // Pre-condition + require.Greater(t, len(serializedOrigFCMMessage), fcmMessageLimit) // Pre-condition truncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage) truncatedMessageLength := len(truncatedFCMMessage.Data["message"]) serializedTruncatedFCMMessage, _ := json.Marshal(truncatedFCMMessage) - require.Equal(t, fcmMessageLimitReal, len(serializedTruncatedFCMMessage)) + require.Equal(t, fcmMessageLimit, len(serializedTruncatedFCMMessage)) require.Equal(t, "1", truncatedFCMMessage.Data["truncated"]) require.NotEqual(t, origMessageLength, truncatedMessageLength) } @@ -639,7 +639,7 @@ func TestServer_MaybeTruncateFCMMessage_NotTooLong(t *testing.T) { } origMessageLength := len(origFCMMessage.Data["message"]) serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage) - require.LessOrEqual(t, len(serializedOrigFCMMessage), fcmMessageLimitReal) // Pre-condition + require.LessOrEqual(t, len(serializedOrigFCMMessage), fcmMessageLimit) // Pre-condition notTruncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage) notTruncatedMessageLength := len(notTruncatedFCMMessage.Data["message"]) From 41c1189feead0649880f6da03fab9cfe185c854b Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 4 Jan 2022 23:40:41 +0100 Subject: [PATCH 6/7] Persist "click" --- server/cache_sqlite.go | 36 ++++++++++++++++++++++++++++-------- server/message.go | 1 + server/server.go | 2 ++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/server/cache_sqlite.go b/server/cache_sqlite.go index b0572895..7f62baa5 100644 --- a/server/cache_sqlite.go +++ b/server/cache_sqlite.go @@ -22,27 +22,28 @@ const ( title TEXT NOT NULL, priority INT NOT NULL, tags TEXT NOT NULL, + click TEXT NOT NULL, published INT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); COMMIT; ` - insertMessageQuery = `INSERT INTO messages (id, time, topic, message, title, priority, tags, published) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + insertMessageQuery = `INSERT INTO messages (id, time, topic, message, title, priority, tags, click, published) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` selectMessagesSinceTimeQuery = ` - SELECT id, time, topic, message, title, priority, tags + SELECT id, time, topic, message, title, priority, tags, click FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time ASC ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT id, time, topic, message, title, priority, tags + SELECT id, time, topic, message, title, priority, tags, click FROM messages WHERE topic = ? AND time >= ? ORDER BY time ASC ` selectMessagesDueQuery = ` - SELECT id, time, topic, message, title, priority, tags + SELECT id, time, topic, message, title, priority, tags, click FROM messages WHERE time <= ? AND published = 0 ` @@ -54,7 +55,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 2 + currentSchemaVersion = 3 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -78,6 +79,11 @@ const ( migrate1To2AlterMessagesTableQuery = ` ALTER TABLE messages ADD COLUMN published INT NOT NULL DEFAULT(1); ` + + // 2 -> 3 + migrate2To3AlterMessagesTableQuery = ` + ALTER TABLE messages ADD COLUMN click TEXT NOT NULL DEFAULT(''); + ` ) type sqliteCache struct { @@ -104,7 +110,7 @@ func (c *sqliteCache) AddMessage(m *message) error { return errUnexpectedMessageType } published := m.Time <= time.Now().Unix() - _, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message, m.Title, m.Priority, strings.Join(m.Tags, ","), published) + _, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message, m.Title, m.Priority, strings.Join(m.Tags, ","), m.Click, published) return err } @@ -187,8 +193,8 @@ func readMessages(rows *sql.Rows) ([]*message, error) { for rows.Next() { var timestamp int64 var priority int - var id, topic, msg, title, tagsStr string - if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr); err != nil { + var id, topic, msg, title, tagsStr, click string + if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr, &click); err != nil { return nil, err } var tags []string @@ -204,6 +210,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { Title: title, Priority: priority, Tags: tags, + Click: click, }) } if err := rows.Err(); err != nil { @@ -241,6 +248,8 @@ func setupDB(db *sql.DB) error { return migrateFrom0(db) } else if schemaVersion == 1 { return migrateFrom1(db) + } else if schemaVersion == 2 { + return migrateFrom2(db) } return fmt.Errorf("unexpected schema version found: %d", schemaVersion) } @@ -280,5 +289,16 @@ func migrateFrom1(db *sql.DB) error { if _, err := db.Exec(updateSchemaVersion, 2); err != nil { return err } + return migrateFrom2(db) +} + +func migrateFrom2(db *sql.DB) error { + log.Print("Migrating cache database schema: from 2 to 3") + if _, err := db.Exec(migrate2To3AlterMessagesTableQuery); err != nil { + return err + } + if _, err := db.Exec(updateSchemaVersion, 3); err != nil { + return err + } return nil // Update this when a new version is added } diff --git a/server/message.go b/server/message.go index ad870e09..eb5c70bd 100644 --- a/server/message.go +++ b/server/message.go @@ -24,6 +24,7 @@ type message struct { Topic string `json:"topic"` Priority int `json:"priority,omitempty"` Tags []string `json:"tags,omitempty"` + Click string `json:"click,omitempty"` Title string `json:"title,omitempty"` Message string `json:"message,omitempty"` } diff --git a/server/server.go b/server/server.go index 8a96c28e..973fd367 100644 --- a/server/server.go +++ b/server/server.go @@ -210,6 +210,7 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) { "topic": m.Topic, "priority": fmt.Sprintf("%d", m.Priority), "tags": strings.Join(m.Tags, ","), + "click": m.Click, "title": m.Title, "message": m.Message, } @@ -449,6 +450,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi firebase = readParam(r, "x-firebase", "firebase") != "no" email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") m.Title = readParam(r, "x-title", "title", "t") + m.Click = readParam(r, "x-click", "click") messageStr := readParam(r, "x-message", "message", "m") if messageStr != "" { m.Message = messageStr From 7a7e7ca359c2a894ea52564785aaab9fc3e1841f Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Wed, 5 Jan 2022 00:11:36 +0100 Subject: [PATCH 7/7] Add docs for click action --- client/options.go | 5 ++++ cmd/publish.go | 6 +++++ docs/publish.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/client/options.go b/client/options.go index 298a5332..716528d7 100644 --- a/client/options.go +++ b/client/options.go @@ -45,6 +45,11 @@ func WithDelay(delay string) PublishOption { return WithHeader("X-Delay", delay) } +// WithClick makes the notification action open the given URL as opposed to entering the detail view +func WithClick(url string) PublishOption { + return WithHeader("X-Click", url) +} + // WithEmail instructs the server to also send the message to the given e-mail address func WithEmail(email string) PublishOption { return WithHeader("X-Email", email) diff --git a/cmd/publish.go b/cmd/publish.go index 5817ccc2..7c71a1b2 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -20,6 +20,7 @@ var cmdPublish = &cli.Command{ &cli.StringFlag{Name: "priority", Aliases: []string{"p"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"}, &cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"}, &cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"}, + &cli.StringFlag{Name: "click", Aliases: []string{"U"}, Usage: "URL to open when notification is clicked"}, &cli.StringFlag{Name: "email", Aliases: []string{"e-mail", "mail", "e"}, Usage: "also send to e-mail address"}, &cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"}, &cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"}, @@ -35,6 +36,7 @@ Examples: ntfy pub --delay=10s delayed_topic Laterzz # Delay message by 10s ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am ntfy pub -e phil@example.com alerts 'App is down!' # Also send email to phil@example.com + ntfy pub --click="https://reddit.com" redd 'New msg' # Opens Reddit when notification is clicked ntfy trigger mywebhook # Sending without message, useful for webhooks Please also check out the docs on publishing messages. Especially for the --tags and --delay options, @@ -56,6 +58,7 @@ func execPublish(c *cli.Context) error { priority := c.String("priority") tags := c.String("tags") delay := c.String("delay") + click := c.String("click") email := c.String("email") noCache := c.Bool("no-cache") noFirebase := c.Bool("no-firebase") @@ -78,6 +81,9 @@ func execPublish(c *cli.Context) error { if delay != "" { options = append(options, client.WithDelay(delay)) } + if click != "" { + options = append(options, client.WithClick(email)) + } if email != "" { options = append(options, client.WithEmail(email)) } diff --git a/docs/publish.md b/docs/publish.md index ec017e05..844a8498 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -592,6 +592,73 @@ Here's an example with a custom message, tags and a priority: file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull'); ``` +## Click action +You can define which URL to open when a notification is clicked. This may be useful if your notification is related +to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open +the web browser (or the app) and open the website. + +Here's an example that will open Reddit when the notification is clicked: + +=== "Command line (curl)" + ``` + curl \ + -d "New messages on Reddit" \ + -H "Click: https://www.reddit.com/message/messages" \ + ntfy.sh/reddit_alerts + ``` + +=== "ntfy CLI" + ``` + ntfy publish \ + --click="https://www.reddit.com/message/messages" \ + reddit_alerts "New messages on Reddit" + ``` + +=== "HTTP" + ``` http + POST /reddit_alerts HTTP/1.1 + Host: ntfy.sh + Click: https://www.reddit.com/message/messages + + New messages on Reddit + ``` + +=== "JavaScript" + ``` javascript + fetch('https://ntfy.sh/reddit_alerts', { + method: 'POST', + body: 'New messages on Reddit', + headers: { 'Click': 'https://www.reddit.com/message/messages' } + }) + ``` + +=== "Go" + ``` go + req, _ := http.NewRequest("POST", "https://ntfy.sh/reddit_alerts", strings.NewReader("New messages on Reddit")) + req.Header.Set("Click", "https://www.reddit.com/message/messages") + http.DefaultClient.Do(req) + ``` + +=== "Python" + ``` python + requests.post("https://ntfy.sh/reddit_alerts", + data="New messages on Reddit", + headers={ "Click": "https://www.reddit.com/message/messages" }) + ``` + +=== "PHP" + ``` php-inline + file_get_contents('https://ntfy.sh/reddit_alerts', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => + "Content-Type: text/plain\r\n" . + "Click: https://www.reddit.com/message/messages", + 'content' => 'New messages on Reddit' + ] + ])); + ``` + ## E-mail notifications You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that you'd like to persist longer, or to blast-notify yourself on all possible channels. @@ -883,6 +950,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a | `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) | | `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) | | `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) | +| `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) | | `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) | | `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) | | `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |