Merge 0ab5d20045
into abe7275f0c
This commit is contained in:
commit
0b8b7817b0
9 changed files with 236 additions and 48 deletions
118
docs/publish.md
118
docs/publish.md
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
×tamp,
|
×tamp,
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -25,24 +25,25 @@ const (
|
||||||
|
|
||||||
// message represents a message published to a topic
|
// message represents a message published to a topic
|
||||||
type message struct {
|
type message struct {
|
||||||
ID string `json:"id"` // Random message ID
|
ID string `json:"id"` // Random message ID
|
||||||
Time int64 `json:"time"` // Unix time in seconds
|
Time int64 `json:"time"` // Unix time in seconds
|
||||||
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
|
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
|
||||||
Event string `json:"event"` // One of the above
|
Event string `json:"event"` // One of the above
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Priority int `json:"priority,omitempty"`
|
Priority int `json:"priority,omitempty"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
Click string `json:"click,omitempty"`
|
Click string `json:"click,omitempty"`
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
Actions []*action `json:"actions,omitempty"`
|
Actions []*action `json:"actions,omitempty"`
|
||||||
Attachment *attachment `json:"attachment,omitempty"`
|
Attachment *attachment `json:"attachment,omitempty"`
|
||||||
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
|
||||||
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
|
Extras map[string]string `json:"extras,omitempty"`
|
||||||
User string `json:"-"` // UserID of the uploader, used to associated attachments
|
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
|
||||||
|
User string `json:"-"` // UserID of the uploader, used to associated attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *message) Context() log.Context {
|
func (m *message) Context() log.Context {
|
||||||
|
@ -92,20 +93,21 @@ func newAction() *action {
|
||||||
|
|
||||||
// publishMessage is used as input when publishing as JSON
|
// publishMessage is used as input when publishing as JSON
|
||||||
type publishMessage struct {
|
type publishMessage struct {
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Click string `json:"click"`
|
Click string `json:"click"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Actions []action `json:"actions"`
|
Actions []action `json:"actions"`
|
||||||
Attach string `json:"attach"`
|
Attach string `json:"attach"`
|
||||||
Markdown bool `json:"markdown"`
|
Markdown bool `json:"markdown"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue