This commit is contained in:
Hunter Kehoe 2023-08-08 12:31:09 -06:00
parent 528a67722b
commit ed4579469c
7 changed files with 179 additions and 33 deletions

View file

@ -1114,6 +1114,7 @@ all the supported fields:
| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery |
| `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) |
| `extras` | - | *JSON object* | `{"customField": "customValue"}` | Extra key:value pairs will be included in the notification |
## Action buttons
_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>
</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
_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)
**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:**
* 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) |
| `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, ...) |
| `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):
@ -363,6 +364,9 @@ Here's an example for each message type:
"expires": 1643946728,
"url": "https://ntfy.sh/file/sPs71M8A2T.png"
},
"extras": {
"customField": "customValue"
},
"title": "Unauthorized access detected",
"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}
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}
errHTTPBadRequestExtrasInvalid = &errHTTP{40041, http.StatusBadRequest, "invalid request: extras invalid", "", nil}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", 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}

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())
}
}
extrasStr := readParam(r, "x-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")
if markdown || strings.ToLower(contentType) == "text/markdown" {
m.ContentType = "text/markdown"
@ -1808,6 +1816,14 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
if 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)
}
}

View file

@ -1555,7 +1555,7 @@ func TestServer_PublishAsJSON(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
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,` +
`"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)
require.Equal(t, 200, response.Code)
@ -1569,6 +1569,7 @@ func TestServer_PublishAsJSON(t *testing.T) {
require.Equal(t, "http://ntfy.sh", m.Click)
require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
require.Equal(t, "", m.ContentType)
require.Equal(t, map[string]string{"customField": "foo"}, m.Extras)
require.Equal(t, 4, m.Priority)
require.True(t, m.Time > time.Now().Unix()+29*60)

View file

@ -25,24 +25,25 @@ const (
// message represents a message published to a topic
type message struct {
ID string `json:"id"` // Random message ID
Time int64 `json:"time"` // Unix time in seconds
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
Event string `json:"event"` // One of the above
Topic string `json:"topic"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
Priority int `json:"priority,omitempty"`
Tags []string `json:"tags,omitempty"`
Click string `json:"click,omitempty"`
Icon string `json:"icon,omitempty"`
Actions []*action `json:"actions,omitempty"`
Attachment *attachment `json:"attachment,omitempty"`
PollID string `json:"poll_id,omitempty"`
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
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
User string `json:"-"` // UserID of the uploader, used to associated attachments
ID string `json:"id"` // Random message ID
Time int64 `json:"time"` // Unix time in seconds
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
Event string `json:"event"` // One of the above
Topic string `json:"topic"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
Priority int `json:"priority,omitempty"`
Tags []string `json:"tags,omitempty"`
Click string `json:"click,omitempty"`
Icon string `json:"icon,omitempty"`
Actions []*action `json:"actions,omitempty"`
Attachment *attachment `json:"attachment,omitempty"`
PollID string `json:"poll_id,omitempty"`
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
Extras map[string]string `json:"extras,omitempty"`
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 {
@ -92,20 +93,21 @@ func newAction() *action {
// publishMessage is used as input when publishing as JSON
type publishMessage struct {
Topic string `json:"topic"`
Title string `json:"title"`
Message string `json:"message"`
Priority int `json:"priority"`
Tags []string `json:"tags"`
Click string `json:"click"`
Icon string `json:"icon"`
Actions []action `json:"actions"`
Attach string `json:"attach"`
Markdown bool `json:"markdown"`
Filename string `json:"filename"`
Email string `json:"email"`
Call string `json:"call"`
Delay string `json:"delay"`
Topic string `json:"topic"`
Title string `json:"title"`
Message string `json:"message"`
Priority int `json:"priority"`
Tags []string `json:"tags"`
Click string `json:"click"`
Icon string `json:"icon"`
Actions []action `json:"actions"`
Attach string `json:"attach"`
Markdown bool `json:"markdown"`
Filename string `json:"filename"`
Email string `json:"email"`
Call string `json:"call"`
Delay string `json:"delay"`
Extras map[string]string `json:"extras"`
}
// messageEncoder is a function that knows how to encode a message