User actions docs, tests and release notes
This commit is contained in:
parent
1f6118f068
commit
6bd4e4bd7c
4 changed files with 88 additions and 38 deletions
|
@ -850,27 +850,32 @@ To define actions using the `X-Actions` header (or any of its aliases: `Actions`
|
||||||
<action1>, <label1>, paramN=... [; <action2>, <label2>, ...]
|
<action1>, <label1>, paramN=... [; <action2>, <label2>, ...]
|
||||||
```
|
```
|
||||||
|
|
||||||
The `action=` and `label=` prefix are optional in all actions, and the `url=` prefix is optional in the `view` and `http` action.
|
Multiple actions are separated by a semicolon (`;`), and key/value pairs are separated by commas (`,`). Values may be
|
||||||
The format has **some limitations**: You cannot use `,` or `;` in any of the values, and depending on your language/library, UTF-8
|
quoted with double quotes (`"`) or single quotes (`'`) if the value itself contains commas or semicolons.
|
||||||
characters may not work. Use the [JSON array format](#using-a-json-array) instead to overcome these limitations.
|
|
||||||
|
The `action=` and `label=` prefix are optional in all actions, and the `url=` prefix is optional in the `view` and
|
||||||
|
`http` action. The only limitation of this format is that depending on your language/library, UTF-8 characters may not
|
||||||
|
work. If they don't, use the [JSON array format](#using-a-json-array) instead.
|
||||||
|
|
||||||
As an example, here's how you can create the above notification using this format. Refer to the [`view` action](#open-websiteapp) and
|
As an example, here's how you can create the above notification using this format. Refer to the [`view` action](#open-websiteapp) and
|
||||||
[`http` action](#send-http-request) section for details on the specific actions:
|
[`http` action](#send-http-request) section for details on the specific actions:
|
||||||
|
|
||||||
=== "Command line (curl)"
|
=== "Command line (curl)"
|
||||||
```
|
```
|
||||||
|
body='{"temperature": 65}'
|
||||||
curl \
|
curl \
|
||||||
-d "You left the house. Turn down the A/C?" \
|
-d "You left the house. Turn down the A/C?" \
|
||||||
-H "Actions: view, Open portal, https://home.nest.com/, clear=true; \
|
-H "Actions: view, Open portal, https://home.nest.com/, clear=true; \
|
||||||
http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65" \
|
http, Turn down, https://api.nest.com/, body='$body'" \
|
||||||
ntfy.sh/myhome
|
ntfy.sh/myhome
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "ntfy CLI"
|
=== "ntfy CLI"
|
||||||
```
|
```
|
||||||
|
body='{"temperature": 65}'
|
||||||
ntfy publish \
|
ntfy publish \
|
||||||
--actions="view, Open portal, https://home.nest.com/, clear=true; \
|
--actions="view, Open portal, https://home.nest.com/, clear=true; \
|
||||||
http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65" \
|
http, Turn down, https://api.nest.com/, body='$body'" \
|
||||||
myhome \
|
myhome \
|
||||||
"You left the house. Turn down the A/C?"
|
"You left the house. Turn down the A/C?"
|
||||||
```
|
```
|
||||||
|
@ -879,7 +884,7 @@ As an example, here's how you can create the above notification using this forma
|
||||||
``` http
|
``` http
|
||||||
POST /myhome HTTP/1.1
|
POST /myhome HTTP/1.1
|
||||||
Host: ntfy.sh
|
Host: ntfy.sh
|
||||||
Actions: view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65
|
Actions: view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{"temperature": 65}'
|
||||||
|
|
||||||
You left the house. Turn down the A/C?
|
You left the house. Turn down the A/C?
|
||||||
```
|
```
|
||||||
|
@ -890,7 +895,7 @@ As an example, here's how you can create the above notification using this forma
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: 'You left the house. Turn down the A/C?',
|
body: 'You left the house. Turn down the A/C?',
|
||||||
headers: {
|
headers: {
|
||||||
'Actions': 'view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65'
|
'Actions': 'view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body=\'{"temperature": 65}\''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
@ -898,14 +903,14 @@ As an example, here's how you can create the above notification using this forma
|
||||||
=== "Go"
|
=== "Go"
|
||||||
``` go
|
``` go
|
||||||
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("You left the house. Turn down the A/C?"))
|
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("You left the house. Turn down the A/C?"))
|
||||||
req.Header.Set("Actions", "view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65")
|
req.Header.Set("Actions", "view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'")
|
||||||
http.DefaultClient.Do(req)
|
http.DefaultClient.Do(req)
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "PowerShell"
|
=== "PowerShell"
|
||||||
``` powershell
|
``` powershell
|
||||||
$uri = "https://ntfy.sh/myhome"
|
$uri = "https://ntfy.sh/myhome"
|
||||||
$headers = @{ Actions="view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65" }
|
$headers = @{ Actions="view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'" }
|
||||||
$body = "You left the house. Turn down the A/C?"
|
$body = "You left the house. Turn down the A/C?"
|
||||||
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
|
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
|
||||||
```
|
```
|
||||||
|
@ -914,7 +919,7 @@ As an example, here's how you can create the above notification using this forma
|
||||||
``` python
|
``` python
|
||||||
requests.post("https://ntfy.sh/myhome",
|
requests.post("https://ntfy.sh/myhome",
|
||||||
data="You left the house. Turn down the A/C?",
|
data="You left the house. Turn down the A/C?",
|
||||||
headers={ "Actions": "view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65" })
|
headers={ "Actions": "view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'" })
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "PHP"
|
=== "PHP"
|
||||||
|
@ -924,7 +929,7 @@ As an example, here's how you can create the above notification using this forma
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'header' =>
|
'header' =>
|
||||||
"Content-Type: text/plain\r\n" .
|
"Content-Type: text/plain\r\n" .
|
||||||
"Actions: view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65",
|
"Actions: view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'",
|
||||||
'content' => 'You left the house. Turn down the A/C?'
|
'content' => 'You left the house. Turn down the A/C?'
|
||||||
]
|
]
|
||||||
]));
|
]));
|
||||||
|
@ -950,8 +955,8 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||||
{
|
{
|
||||||
"action": "http",
|
"action": "http",
|
||||||
"label": "Turn down",
|
"label": "Turn down",
|
||||||
"url": "https://api.nest.com/device/XZ1D2",
|
"url": "https://api.nest.com/",
|
||||||
"body": "target_temp_f=65"
|
"body": "{\"temperature\": 65}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}'
|
}'
|
||||||
|
@ -970,8 +975,8 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||||
{
|
{
|
||||||
"action": "http",
|
"action": "http",
|
||||||
"label": "Turn down",
|
"label": "Turn down",
|
||||||
"url": "https://api.nest.com/device/XZ1D2",
|
"url": "https://api.nest.com/",
|
||||||
"body": "target_temp_f=65"
|
"body": "{\"temperature\": 65}"
|
||||||
}
|
}
|
||||||
]' \
|
]' \
|
||||||
myhome \
|
myhome \
|
||||||
|
@ -996,8 +1001,8 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||||
{
|
{
|
||||||
"action": "http",
|
"action": "http",
|
||||||
"label": "Turn down",
|
"label": "Turn down",
|
||||||
"url": "https://api.nest.com/device/XZ1D2",
|
"url": "https://api.nest.com/",
|
||||||
"body": "target_temp_f=65"
|
"body": "{\"temperature\": 65}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1020,8 +1025,8 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||||
{
|
{
|
||||||
action: "http",
|
action: "http",
|
||||||
label: "Turn down",
|
label: "Turn down",
|
||||||
url: "https://api.nest.com/device/XZ1D2",
|
url: "https://api.nest.com/",
|
||||||
body: "target_temp_f=65"
|
body: "{\"temperature\": 65}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -1046,8 +1051,8 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||||
{
|
{
|
||||||
"action": "http",
|
"action": "http",
|
||||||
"label": "Turn down",
|
"label": "Turn down",
|
||||||
"url": "https://api.nest.com/device/XZ1D2",
|
"url": "https://api.nest.com/",
|
||||||
"body": "target_temp_f=65"
|
"body": "{\"temperature\": 65}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
|
@ -1071,8 +1076,8 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||||
@{
|
@{
|
||||||
"action"="http",
|
"action"="http",
|
||||||
"label"="Turn down"
|
"label"="Turn down"
|
||||||
"url"="https://api.nest.com/device/XZ1D2"
|
"url"="https://api.nest.com/"
|
||||||
"body"="target_temp_f=65"
|
"body"="{\"temperature\": 65}"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} | ConvertTo-Json
|
} | ConvertTo-Json
|
||||||
|
@ -1095,8 +1100,8 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||||
{
|
{
|
||||||
"action": "http",
|
"action": "http",
|
||||||
"label": "Turn down",
|
"label": "Turn down",
|
||||||
"url": "https://api.nest.com/device/XZ1D2",
|
"url": "https://api.nest.com/",
|
||||||
"body": "target_temp_f=65"
|
"body": "{\"temperature\": 65}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -1122,11 +1127,11 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||||
[
|
[
|
||||||
"action": "http",
|
"action": "http",
|
||||||
"label": "Turn down",
|
"label": "Turn down",
|
||||||
"url": "https://api.nest.com/device/XZ1D2",
|
"url": "https://api.nest.com/",
|
||||||
"headers": [
|
"headers": [
|
||||||
"Authorization": "Bearer ..."
|
"Authorization": "Bearer ..."
|
||||||
],
|
],
|
||||||
"body": "target_temp_f=65"
|
"body": "{\"temperature\": 65}"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
|
|
@ -2,6 +2,22 @@
|
||||||
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
## ntfy Android app v1.13.0 (UNRELEASED)
|
||||||
|
|
||||||
|
Bugs:
|
||||||
|
* Accurate naming of "mute notifications" from "pause notifications" ([#224](https://github.com/binwiederhier/ntfy/issues/224),
|
||||||
|
thanks to [@shadow00](https://github.com/shadow00) for reporting)
|
||||||
|
|
||||||
|
## ntfy server v1.22.0 (UNRELEASED)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Better parsing of the user actions, allowing quotes (no ticket)
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
## ntfy Android app v1.12.0
|
## ntfy Android app v1.12.0
|
||||||
Released Apr 25, 2022
|
Released Apr 25, 2022
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,6 @@ func (p *actionParser) parseAction() (*action, error) {
|
||||||
section := 0
|
section := 0
|
||||||
for {
|
for {
|
||||||
key, value, last, err := p.parseSection()
|
key, value, last, err := p.parseSection()
|
||||||
fmt.Printf("--> key=%s, value=%s, last=%t, err=%#v\n", key, value, last, err)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -226,14 +225,15 @@ func (p *actionParser) parseKey() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseValue reads the input until EOF, "," or ";" and returns the value string. Unlike parseQuotedValue,
|
// parseValue reads the input until EOF, "," or ";" and returns the value string. Unlike parseQuotedValue,
|
||||||
// this function does not support "," or ";" in the value itself.
|
// this function does not support "," or ";" in the value itself, and spaces in the beginning and end of the
|
||||||
|
// string are trimmed.
|
||||||
func (p *actionParser) parseValue() (value string, last bool) {
|
func (p *actionParser) parseValue() (value string, last bool) {
|
||||||
start := p.pos
|
start := p.pos
|
||||||
for {
|
for {
|
||||||
r, w := p.peek()
|
r, w := p.peek()
|
||||||
if isSectionEnd(r) {
|
if isSectionEnd(r) {
|
||||||
last = isLastSection(r)
|
last = isLastSection(r)
|
||||||
value = p.input[start:p.pos]
|
value = strings.TrimSpace(p.input[start:p.pos])
|
||||||
p.pos += w
|
p.pos += w
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,15 @@ func TestParseActions(t *testing.T) {
|
||||||
require.Equal(t, `"quotes" and \'single quotes\'`, actions[0].Label)
|
require.Equal(t, `"quotes" and \'single quotes\'`, actions[0].Label)
|
||||||
require.Equal(t, `http://example.com`, actions[0].URL)
|
require.Equal(t, `http://example.com`, actions[0].URL)
|
||||||
|
|
||||||
|
// Single quotes (JSON)
|
||||||
|
actions, err = parseActions(`action=http, Post it, url=http://example.com, body='{"temperature": 65}'`)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, 1, len(actions))
|
||||||
|
require.Equal(t, "http", actions[0].Action)
|
||||||
|
require.Equal(t, "Post it", actions[0].Label)
|
||||||
|
require.Equal(t, `http://example.com`, actions[0].URL)
|
||||||
|
require.Equal(t, `{"temperature": 65}`, actions[0].Body)
|
||||||
|
|
||||||
// Out of order
|
// Out of order
|
||||||
actions, err = parseActions(`label="Out of order!" , action="http", url=http://example.com`)
|
actions, err = parseActions(`label="Out of order!" , action="http", url=http://example.com`)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
@ -102,25 +111,45 @@ func TestParseActions(t *testing.T) {
|
||||||
require.Equal(t, `Кохайтеся а не воюйте, 💙🫤`, actions[0].Label)
|
require.Equal(t, `Кохайтеся а не воюйте, 💙🫤`, actions[0].Label)
|
||||||
require.Equal(t, `http://google.com`, actions[0].URL)
|
require.Equal(t, `http://google.com`, actions[0].URL)
|
||||||
|
|
||||||
|
// Multiple actions, awkward spacing
|
||||||
|
actions, err = parseActions(`http , 'Make love, not war 💙🫤' , https://ntfy.sh ; view, " yo ", https://x.org`)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, 2, len(actions))
|
||||||
|
require.Equal(t, "http", actions[0].Action)
|
||||||
|
require.Equal(t, `Make love, not war 💙🫤`, actions[0].Label)
|
||||||
|
require.Equal(t, `https://ntfy.sh`, actions[0].URL)
|
||||||
|
require.Equal(t, "view", actions[1].Action)
|
||||||
|
require.Equal(t, " yo ", actions[1].Label)
|
||||||
|
require.Equal(t, `https://x.org`, actions[1].URL)
|
||||||
|
|
||||||
// Invalid syntax
|
// Invalid syntax
|
||||||
actions, err = parseActions(`label="Out of order!" x, action="http", url=http://example.com`)
|
_, err = parseActions(`label="Out of order!" x, action="http", url=http://example.com`)
|
||||||
require.EqualError(t, err, "unexpected character 'x' at position 22")
|
require.EqualError(t, err, "unexpected character 'x' at position 22")
|
||||||
|
|
||||||
actions, err = parseActions(`label="", action="http", url=http://example.com`)
|
_, err = parseActions(`label="", action="http", url=http://example.com`)
|
||||||
require.EqualError(t, err, "parameter 'label' is required")
|
require.EqualError(t, err, "parameter 'label' is required")
|
||||||
|
|
||||||
actions, err = parseActions(`label=, action="http", url=http://example.com`)
|
_, err = parseActions(`label=, action="http", url=http://example.com`)
|
||||||
require.EqualError(t, err, "parameter 'label' is required")
|
require.EqualError(t, err, "parameter 'label' is required")
|
||||||
|
|
||||||
actions, err = parseActions(`label="xx", action="http", url=http://example.com, what is this anyway`)
|
_, err = parseActions(`label="xx", action="http", url=http://example.com, what is this anyway`)
|
||||||
require.EqualError(t, err, "term 'what is this anyway' unknown")
|
require.EqualError(t, err, "term 'what is this anyway' unknown")
|
||||||
|
|
||||||
actions, err = parseActions(`fdsfdsf`)
|
_, err = parseActions(`fdsfdsf`)
|
||||||
require.EqualError(t, err, "action 'fdsfdsf' unknown")
|
require.EqualError(t, err, "action 'fdsfdsf' unknown")
|
||||||
|
|
||||||
actions, err = parseActions(`aaa=a, "bbb, 'ccc, ddd, eee "`)
|
_, err = parseActions(`aaa=a, "bbb, 'ccc, ddd, eee "`)
|
||||||
require.EqualError(t, err, "key 'aaa' unknown")
|
require.EqualError(t, err, "key 'aaa' unknown")
|
||||||
|
|
||||||
actions, err = parseActions(`action=http, label="omg the end quote is missing`)
|
_, err = parseActions(`action=http, label="omg the end quote is missing`)
|
||||||
require.EqualError(t, err, "unexpected end of input, quote started at position 20")
|
require.EqualError(t, err, "unexpected end of input, quote started at position 20")
|
||||||
|
|
||||||
|
_, err = parseActions(`;;;;`)
|
||||||
|
require.EqualError(t, err, "only 3 actions allowed")
|
||||||
|
|
||||||
|
_, err = parseActions(`,,,,,,;;`)
|
||||||
|
require.EqualError(t, err, "term '' unknown")
|
||||||
|
|
||||||
|
_, err = parseActions(`''";,;"`)
|
||||||
|
require.EqualError(t, err, "unexpected character '\"' at position 2")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue