This commit is contained in:
wunter8 2023-11-08 12:46:19 +01:00 committed by GitHub
commit 0b8b7817b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 236 additions and 48 deletions

View file

@ -1114,6 +1114,7 @@ all the supported fields:
| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery | | `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery |
| `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications | | `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications |
| `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) | | `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) |
| `extras` | - | *JSON object* | `{"customField": "customValue"}` | Extra key:value pairs will be included in the notification |
## Action buttons ## Action buttons
_Supported on:_ :material-android: :material-apple: :material-firefox: _Supported on:_ :material-android: :material-apple: :material-firefox:
@ -2662,6 +2663,123 @@ Here's an example of how it will look on Android:
<figcaption>Custom icon from an external URL</figcaption> <figcaption>Custom icon from an external URL</figcaption>
</figure> </figure>
## Custom fields
_Supported on:_ :material-android:
You can send custom key:value pairs that will be included as-is in the notification. This can be helpful if you are
using ntfy to pass messages between different computer programs or services, for example. Simply pass a stringified
JSON object in the `X-Extras` header, or include the JSON object in the `extras` key when using [JSON publishing]
(#publish-as-json). **The JSON object can only be 1 level deep, nesting is not supported**.
Here's an example showing how to send custom fields:
=== "Command line (curl) (JSON)"
```
curl ntfy.sh \
-d '{
"topic": "mytopic",
"message": "Disk space is low at 5.1 GB",
"title": "Low disk space alert",
"tags": ["warning","cd"],
"priority": 4,
"extras": {"lastChecked": "20230205"}
}'
```
=== "Command line (curl) (Header)"
```
curl \
-H "Title: Low disk space alert" \
-H "Tags: warning,cd" \
-H "X-Priority: 4" \
-H 'X-Extras: {"lastChecked": "20230205"}' \
-d "Disk space is low at 5.1 GB" \
ntfy.sh/mytopic
```
=== "HTTP"
``` http
POST /mytopic HTTP/1.1
Host: ntfy.sh
Title: Low disk space alert
Tags: warning,cd
X-Priority: 4
X-Extras: {"lastChecked": "20230205"}
Disk space is low at 5.1 GB
```
=== "JavaScript"
``` javascript
fetch('https://ntfy.sh/mytopic', {
method: 'POST',
headers: {
'Title': 'Low disk space alert',
'Tags': 'warning,cd'
'X-Priority': '4',
'X-Extras': {'lastChecked': '20230205'}
},
body: "Disk space is low at 5.1 GB"
})
```
=== "Go"
``` go
req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", strings.NewReader("Disk space is low at 5.1 GB"))
req.Header.Set("Title", "Low disk space alert")
req.Header.Set("Tags", "warning,cd")
req.Header.Set("X-Priority", "4")
req.Header.Set("X-Extras", `{"lastChecked": "20230205"}`)
http.DefaultClient.Do(req)
```
=== "PowerShell"
``` powershell
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = @{
Topic = "mytopic"
Title = "Low disk space alert"
Tags = @("warning", "cd")
Priority = 4
Message = "Disk space is low at 5.1 GB"
Extras = ConvertTo-JSON @{
lastChecked = "20230205"
}
}
ContentType = "application/json"
}
Invoke-RestMethod @Request
```
=== "Python"
``` python
requests.post("https://ntfy.sh/mytopic",
data="Disk space is low at 5.1 GB",
headers={
"Title": "Low disk space alert",
"Tags": "warning,cd",
"X-Priority": "4",
"X-Extras": '{"lastChecked": "20230205"}'
})
```
=== "PHP"
``` php-inline
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
'http' => [
'method' => 'PUT',
'header' =>
"Content-Type: text/plain\r\n" . // Does not matter
"Title: Low disk space alert\r\n" .
"Tags: warning,cd\r\n" .
"X-Priority: 4\r\n" .
"X-Extras: {\"lastChecked\": \"20230205\"}",
],
'content' => "Disk space is low at 5.1 GB"
]));
```
## E-mail notifications ## E-mail notifications
_Supported on:_ :material-android: :material-apple: :material-firefox: _Supported on:_ :material-android: :material-apple: :material-firefox:

View file

@ -1285,6 +1285,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
### ntfy server v2.8.0 (UNRELEASED) ### ntfy server v2.8.0 (UNRELEASED)
**Features:**
* You can now send custom fields within an `extras` field in a JSON POST/PUT request ([#827](https://github.com/binwiederhier/ntfy/issues/827), thanks to [@tka85](https://github.com/tka85) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
**Bug fixes + maintenance:** **Bug fixes + maintenance:**
* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting) * Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting)

View file

@ -329,6 +329,7 @@ format of the message. It's very straight forward:
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) | | `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
| `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification | | `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification |
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) | | `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
| `extras` | - | *JSON object* | `{"customField": "customValue"}` | Extra key:value pairs provided by the publisher |
**Attachment** (part of the message, see [attachments](../publish.md#attachments) for details): **Attachment** (part of the message, see [attachments](../publish.md#attachments) for details):
@ -363,6 +364,9 @@ Here's an example for each message type:
"expires": 1643946728, "expires": 1643946728,
"url": "https://ntfy.sh/file/sPs71M8A2T.png" "url": "https://ntfy.sh/file/sPs71M8A2T.png"
}, },
"extras": {
"customField": "customValue"
},
"title": "Unauthorized access detected", "title": "Unauthorized access detected",
"message": "Movement detected in the yard. You better go check" "message": "Movement detected in the yard. You better go check"
} }

View file

@ -117,6 +117,7 @@ var (
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil} errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil}
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil} errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil}
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil} errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
errHTTPBadRequestExtrasInvalid = &errHTTP{40041, http.StatusBadRequest, "invalid request: extras invalid", "", nil}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}

View file

@ -46,6 +46,7 @@ const (
sender TEXT NOT NULL, sender TEXT NOT NULL,
user TEXT NOT NULL, user TEXT NOT NULL,
content_type TEXT NOT NULL, content_type TEXT NOT NULL,
extras TEXT NOT NULL,
encoding TEXT NOT NULL, encoding TEXT NOT NULL,
published INT NOT NULL published INT NOT NULL
); );
@ -64,43 +65,43 @@ const (
COMMIT; COMMIT;
` `
insertMessageQuery = ` insertMessageQuery = `
INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published) INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, extras, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?`
updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
selectMessagesByIDQuery = ` selectMessagesByIDQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding
FROM messages FROM messages
WHERE mid = ? WHERE mid = ?
` `
selectMessagesSinceTimeQuery = ` selectMessagesSinceTimeQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding
FROM messages FROM messages
WHERE topic = ? AND time >= ? AND published = 1 WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time, id ORDER BY time, id
` `
selectMessagesSinceTimeIncludeScheduledQuery = ` selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding
FROM messages FROM messages
WHERE topic = ? AND time >= ? WHERE topic = ? AND time >= ?
ORDER BY time, id ORDER BY time, id
` `
selectMessagesSinceIDQuery = ` selectMessagesSinceIDQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding
FROM messages FROM messages
WHERE topic = ? AND id > ? AND published = 1 WHERE topic = ? AND id > ? AND published = 1
ORDER BY time, id ORDER BY time, id
` `
selectMessagesSinceIDIncludeScheduledQuery = ` selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding
FROM messages FROM messages
WHERE topic = ? AND (id > ? OR published = 0) WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY time, id ORDER BY time, id
` `
selectMessagesDueQuery = ` selectMessagesDueQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, extras, encoding
FROM messages FROM messages
WHERE time <= ? AND published = 0 WHERE time <= ? AND published = 0
ORDER BY time, id ORDER BY time, id
@ -122,7 +123,7 @@ const (
// Schema management queries // Schema management queries
const ( const (
currentSchemaVersion = 12 currentSchemaVersion = 13
createSchemaVersionTableQuery = ` createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion ( CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY, id INT PRIMARY KEY,
@ -246,6 +247,11 @@ const (
migrate11To12AlterMessagesTableQuery = ` migrate11To12AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN content_type TEXT NOT NULL DEFAULT(''); ALTER TABLE messages ADD COLUMN content_type TEXT NOT NULL DEFAULT('');
` `
// 12 -> 13
migrate12To13AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN extras TEXT NOT NULL DEFAULT('');
`
) )
var ( var (
@ -262,6 +268,7 @@ var (
9: migrateFrom9, 9: migrateFrom9,
10: migrateFrom10, 10: migrateFrom10,
11: migrateFrom11, 11: migrateFrom11,
12: migrateFrom12,
} }
) )
@ -367,6 +374,14 @@ func (c *messageCache) addMessages(ms []*message) error {
} }
actionsStr = string(actionsBytes) actionsStr = string(actionsBytes)
} }
var extrasStr string
if len(m.Extras) > 0 {
extrasBytes, err := json.Marshal(m.Extras)
if err != nil {
return err
}
extrasStr = string(extrasBytes)
}
var sender string var sender string
if m.Sender.IsValid() { if m.Sender.IsValid() {
sender = m.Sender.String() sender = m.Sender.String()
@ -392,6 +407,7 @@ func (c *messageCache) addMessages(ms []*message) error {
sender, sender,
m.User, m.User,
m.ContentType, m.ContentType,
extrasStr,
m.Encoding, m.Encoding,
published, published,
) )
@ -664,7 +680,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
func readMessage(rows *sql.Rows) (*message, error) { func readMessage(rows *sql.Rows) (*message, error) {
var timestamp, expires, attachmentSize, attachmentExpires int64 var timestamp, expires, attachmentSize, attachmentExpires int64
var priority int var priority int
var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, extrasStr, encoding string
err := rows.Scan( err := rows.Scan(
&id, &id,
&timestamp, &timestamp,
@ -685,6 +701,7 @@ func readMessage(rows *sql.Rows) (*message, error) {
&sender, &sender,
&user, &user,
&contentType, &contentType,
&extrasStr,
&encoding, &encoding,
) )
if err != nil { if err != nil {
@ -700,6 +717,12 @@ func readMessage(rows *sql.Rows) (*message, error) {
return nil, err return nil, err
} }
} }
var extras map[string]string
if extrasStr != "" {
if err := json.Unmarshal([]byte(extrasStr), &extras); err != nil {
return nil, err
}
}
senderIP, err := netip.ParseAddr(sender) senderIP, err := netip.ParseAddr(sender)
if err != nil { if err != nil {
senderIP = netip.Addr{} // if no IP stored in database, return invalid address senderIP = netip.Addr{} // if no IP stored in database, return invalid address
@ -731,6 +754,7 @@ func readMessage(rows *sql.Rows) (*message, error) {
Sender: senderIP, // Must parse assuming database must be correct Sender: senderIP, // Must parse assuming database must be correct
User: user, User: user,
ContentType: contentType, ContentType: contentType,
Extras: extras,
Encoding: encoding, Encoding: encoding,
}, nil }, nil
} }
@ -970,3 +994,19 @@ func migrateFrom11(db *sql.DB, _ time.Duration) error {
} }
return tx.Commit() return tx.Commit()
} }
func migrateFrom12(db *sql.DB, _ time.Duration) error {
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 12 to 13")
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(migrate12To13AlterMessagesTableQuery); err != nil {
return err
}
if _, err := tx.Exec(updateSchemaVersion, 13); err != nil {
return err
}
return tx.Commit()
}

View file

@ -143,25 +143,27 @@ func testCacheTopics(t *testing.T, c *messageCache) {
require.Equal(t, "topic2", topics["topic2"].ID) require.Equal(t, "topic2", topics["topic2"].ID)
} }
func TestSqliteCache_MessagesTagsPrioAndTitle(t *testing.T) { func TestSqliteCache_MessagesTagsPrioTitleAndExtras(t *testing.T) {
testCacheMessagesTagsPrioAndTitle(t, newSqliteTestCache(t)) testCacheMessagesTagsPrioTitleAndExtras(t, newSqliteTestCache(t))
} }
func TestMemCache_MessagesTagsPrioAndTitle(t *testing.T) { func TestMemCache_MessagesTagsPrioTitleAndExtras(t *testing.T) {
testCacheMessagesTagsPrioAndTitle(t, newMemTestCache(t)) testCacheMessagesTagsPrioTitleAndExtras(t, newMemTestCache(t))
} }
func testCacheMessagesTagsPrioAndTitle(t *testing.T, c *messageCache) { func testCacheMessagesTagsPrioTitleAndExtras(t *testing.T, c *messageCache) {
m := newDefaultMessage("mytopic", "some message") m := newDefaultMessage("mytopic", "some message")
m.Tags = []string{"tag1", "tag2"} m.Tags = []string{"tag1", "tag2"}
m.Priority = 5 m.Priority = 5
m.Title = "some title" m.Title = "some title"
m.Extras = map[string]string{"foo": "bar"}
require.Nil(t, c.AddMessage(m)) require.Nil(t, c.AddMessage(m))
messages, _ := c.Messages("mytopic", sinceAllMessages, false) messages, _ := c.Messages("mytopic", sinceAllMessages, false)
require.Equal(t, []string{"tag1", "tag2"}, messages[0].Tags) require.Equal(t, []string{"tag1", "tag2"}, messages[0].Tags)
require.Equal(t, 5, messages[0].Priority) require.Equal(t, 5, messages[0].Priority)
require.Equal(t, "some title", messages[0].Title) require.Equal(t, "some title", messages[0].Title)
require.Equal(t, map[string]string{"foo": "bar"}, messages[0].Extras)
} }
func TestSqliteCache_MessagesSinceID(t *testing.T) { func TestSqliteCache_MessagesSinceID(t *testing.T) {

View file

@ -1010,6 +1010,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
} }
} }
extrasStr := readParam(r, "x-extras", "extras")
if extrasStr != "" {
extras := make(map[string]string)
if err := json.Unmarshal([]byte(extrasStr), &extras); err != nil {
return false, false, "", "", false, errHTTPBadRequestExtrasInvalid.Wrap(e.Error())
}
m.Extras = extras
}
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
if markdown || strings.ToLower(contentType) == "text/markdown" { if markdown || strings.ToLower(contentType) == "text/markdown" {
m.ContentType = "text/markdown" m.ContentType = "text/markdown"
@ -1808,6 +1816,14 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
if m.Call != "" { if m.Call != "" {
r.Header.Set("X-Call", m.Call) r.Header.Set("X-Call", m.Call)
} }
if len(m.Extras) > 0 {
extrasStr, err := json.Marshal(m.Extras)
if err != nil {
return errHTTPBadRequestMessageJSONInvalid
}
r.Header.Set("X-Extras", string(extrasStr))
}
return next(w, r, v) return next(w, r, v)
} }
} }

View file

@ -1578,7 +1578,7 @@ func TestServer_PublishAsJSON(t *testing.T) {
s := newTestServer(t, newTestConfig(t)) s := newTestServer(t, newTestConfig(t))
body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` + body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` +
`"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` + `"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` +
`"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min"}` `"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min", "extras": {"customField":"foo"}}`
response := request(t, s, "PUT", "/", body, nil) response := request(t, s, "PUT", "/", body, nil)
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
@ -1592,6 +1592,7 @@ func TestServer_PublishAsJSON(t *testing.T) {
require.Equal(t, "http://ntfy.sh", m.Click) require.Equal(t, "http://ntfy.sh", m.Click)
require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon) require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
require.Equal(t, "", m.ContentType) require.Equal(t, "", m.ContentType)
require.Equal(t, map[string]string{"customField": "foo"}, m.Extras)
require.Equal(t, 4, m.Priority) require.Equal(t, 4, m.Priority)
require.True(t, m.Time > time.Now().Unix()+29*60) require.True(t, m.Time > time.Now().Unix()+29*60)

View file

@ -41,6 +41,7 @@ type message struct {
PollID string `json:"poll_id,omitempty"` PollID string `json:"poll_id,omitempty"`
ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
Extras map[string]string `json:"extras,omitempty"`
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
User string `json:"-"` // UserID of the uploader, used to associated attachments User string `json:"-"` // UserID of the uploader, used to associated attachments
} }
@ -106,6 +107,7 @@ type publishMessage struct {
Email string `json:"email"` Email string `json:"email"`
Call string `json:"call"` Call string `json:"call"`
Delay string `json:"delay"` Delay string `json:"delay"`
Extras map[string]string `json:"extras"`
} }
// messageEncoder is a function that knows how to encode a message // messageEncoder is a function that knows how to encode a message