Move action parsing to action package

This commit is contained in:
binwiederhier
2026-07-03 20:54:45 -04:00
parent 2eabfd2f01
commit be3dbdcb48
3 changed files with 43 additions and 40 deletions
+5 -3
View File
@@ -1,4 +1,6 @@
package server // Package action parses the "action buttons" that can be attached to a notification, in both the
// JSON and the human-readable "simple" format described at https://ntfy.sh/docs/publish/#action-buttons.
package action
import ( import (
"encoding/json" "encoding/json"
@@ -37,10 +39,10 @@ type actionParser struct {
pos int pos int
} }
// parseActions parses the actions string as described in https://ntfy.sh/docs/publish/#action-buttons. // Parse parses the actions string as described in https://ntfy.sh/docs/publish/#action-buttons.
// It supports both a JSON representation (if the string begins with "[", see parseActionsFromJSON), // It supports both a JSON representation (if the string begins with "[", see parseActionsFromJSON),
// and the "simple" format, which is more human-readable, but harder to parse (see parseActionsFromSimple). // and the "simple" format, which is more human-readable, but harder to parse (see parseActionsFromSimple).
func parseActions(s string) (actions []*model.Action, err error) { func Parse(s string) (actions []*model.Action, err error) {
// Parse JSON or simple format // Parse JSON or simple format
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
if strings.HasPrefix(s, "[") { if strings.HasPrefix(s, "[") {
@@ -1,4 +1,4 @@
package server package action
import ( import (
"testing" "testing"
@@ -7,12 +7,12 @@ import (
) )
func TestParseActions(t *testing.T) { func TestParseActions(t *testing.T) {
actions, err := parseActions("[]") actions, err := Parse("[]")
require.Nil(t, err) require.Nil(t, err)
require.Empty(t, actions) require.Empty(t, actions)
// Basic test // Basic test
actions, err = parseActions("action=http, label=Open door, url=https://door.lan/open; view, Show portal, https://door.lan") actions, err = Parse("action=http, label=Open door, url=https://door.lan/open; view, Show portal, https://door.lan")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 2, len(actions)) require.Equal(t, 2, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -23,7 +23,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, "https://door.lan", actions[1].URL) require.Equal(t, "https://door.lan", actions[1].URL)
// JSON // JSON
actions, err = parseActions(`[{"action":"http","label":"Open door","url":"https://door.lan/open"}, {"action":"view","label":"Show portal","url":"https://door.lan"}]`) actions, err = Parse(`[{"action":"http","label":"Open door","url":"https://door.lan/open"}, {"action":"view","label":"Show portal","url":"https://door.lan"}]`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 2, len(actions)) require.Equal(t, 2, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -34,7 +34,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, "https://door.lan", actions[1].URL) require.Equal(t, "https://door.lan", actions[1].URL)
// Other params // Other params
actions, err = parseActions("action=http, label=Open door, url=https://door.lan/open, body=this is a body, method=PUT") actions, err = Parse("action=http, label=Open door, url=https://door.lan/open, body=this is a body, method=PUT")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -44,7 +44,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, "this is a body", actions[0].Body) require.Equal(t, "this is a body", actions[0].Body)
// Extras with underscores // Extras with underscores
actions, err = parseActions("action=broadcast, label=Do a thing, extras.command=some command, extras.some_param=a parameter") actions, err = Parse("action=broadcast, label=Do a thing, extras.command=some command, extras.some_param=a parameter")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "broadcast", actions[0].Action) require.Equal(t, "broadcast", actions[0].Action)
@@ -54,7 +54,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, "a parameter", actions[0].Extras["some_param"]) require.Equal(t, "a parameter", actions[0].Extras["some_param"])
// Broadcast action with intent // Broadcast action with intent
actions, err = parseActions("action=broadcast, label=Do a thing, intent=io.heckel.ntfy.TEST_INTENT") actions, err = Parse("action=broadcast, label=Do a thing, intent=io.heckel.ntfy.TEST_INTENT")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "broadcast", actions[0].Action) require.Equal(t, "broadcast", actions[0].Action)
@@ -62,7 +62,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, "io.heckel.ntfy.TEST_INTENT", actions[0].Intent) require.Equal(t, "io.heckel.ntfy.TEST_INTENT", actions[0].Intent)
// Headers with dashes // Headers with dashes
actions, err = parseActions("action=http, label=Send request, url=http://example.com, method=GET, headers.Content-Type=application/json, headers.Authorization=Basic sdasffsf") actions, err = Parse("action=http, label=Send request, url=http://example.com, method=GET, headers.Content-Type=application/json, headers.Authorization=Basic sdasffsf")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -72,7 +72,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, "Basic sdasffsf", actions[0].Headers["Authorization"]) require.Equal(t, "Basic sdasffsf", actions[0].Headers["Authorization"])
// Quotes // Quotes
actions, err = parseActions(`action=http, "Look ma, \"quotes\"; and semicolons", url=http://example.com`) actions, err = Parse(`action=http, "Look ma, \"quotes\"; and semicolons", url=http://example.com`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -80,7 +80,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, `http://example.com`, actions[0].URL) require.Equal(t, `http://example.com`, actions[0].URL)
// Single quotes // Single quotes
actions, err = parseActions(`action=http, '"quotes" and \'single quotes\'', url=http://example.com`) actions, err = Parse(`action=http, '"quotes" and \'single quotes\'', url=http://example.com`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -88,7 +88,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, `http://example.com`, actions[0].URL) require.Equal(t, `http://example.com`, actions[0].URL)
// Single quotes (JSON) // Single quotes (JSON)
actions, err = parseActions(`action=http, Post it, url=http://example.com, body='{"temperature": 65}'`) actions, err = Parse(`action=http, Post it, url=http://example.com, body='{"temperature": 65}'`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -97,7 +97,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, `{"temperature": 65}`, actions[0].Body) 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 = Parse(`label="Out of order!" , action="http", url=http://example.com`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -105,7 +105,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, `http://example.com`, actions[0].URL) require.Equal(t, `http://example.com`, actions[0].URL)
// Spaces // Spaces
actions, err = parseActions(`action = http, label = 'this is a label', url = "http://google.com"`) actions, err = Parse(`action = http, label = 'this is a label', url = "http://google.com"`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -113,7 +113,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, `http://google.com`, actions[0].URL) require.Equal(t, `http://google.com`, actions[0].URL)
// Non-ASCII // Non-ASCII
actions, err = parseActions(`action = http, 'Кохайтеся а не воюйте, 💙🫤', url = "http://google.com"`) actions, err = Parse(`action = http, 'Кохайтеся а не воюйте, 💙🫤', url = "http://google.com"`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -121,7 +121,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, `http://google.com`, actions[0].URL) require.Equal(t, `http://google.com`, actions[0].URL)
// Multiple actions, awkward spacing // Multiple actions, awkward spacing
actions, err = parseActions(`http , 'Make love, not war 💙🫤' , https://ntfy.sh ; view, " yo ", https://x.org, clear=true`) actions, err = Parse(`http , 'Make love, not war 💙🫤' , https://ntfy.sh ; view, " yo ", https://x.org, clear=true`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 2, len(actions)) require.Equal(t, 2, len(actions))
require.Equal(t, "http", actions[0].Action) require.Equal(t, "http", actions[0].Action)
@@ -134,7 +134,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, true, actions[1].Clear) require.Equal(t, true, actions[1].Clear)
// Copy action (simple format) // Copy action (simple format)
actions, err = parseActions("copy, Copy code, 1234") actions, err = Parse("copy, Copy code, 1234")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "copy", actions[0].Action) require.Equal(t, "copy", actions[0].Action)
@@ -142,7 +142,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, "1234", actions[0].Value) require.Equal(t, "1234", actions[0].Value)
// Copy action (JSON) // Copy action (JSON)
actions, err = parseActions(`[{"action":"copy","label":"Copy OTP","value":"567890"}]`) actions, err = Parse(`[{"action":"copy","label":"Copy OTP","value":"567890"}]`)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "copy", actions[0].Action) require.Equal(t, "copy", actions[0].Action)
@@ -150,7 +150,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, "567890", actions[0].Value) require.Equal(t, "567890", actions[0].Value)
// Copy action with clear // Copy action with clear
actions, err = parseActions("copy, Copy code, 1234, clear=true") actions, err = Parse("copy, Copy code, 1234, clear=true")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "copy", actions[0].Action) require.Equal(t, "copy", actions[0].Action)
@@ -159,7 +159,7 @@ func TestParseActions(t *testing.T) {
require.Equal(t, true, actions[0].Clear) require.Equal(t, true, actions[0].Clear)
// Copy action with explicit value key // Copy action with explicit value key
actions, err = parseActions("action=copy, label=Copy token, clear=true, value=abc-123-def") actions, err = Parse("action=copy, label=Copy token, clear=true, value=abc-123-def")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, 1, len(actions)) require.Equal(t, 1, len(actions))
require.Equal(t, "copy", actions[0].Action) require.Equal(t, "copy", actions[0].Action)
@@ -168,56 +168,56 @@ func TestParseActions(t *testing.T) {
require.True(t, actions[0].Clear) require.True(t, actions[0].Clear)
// Copy action without value (error) // Copy action without value (error)
_, err = parseActions("copy, Copy code") _, err = Parse("copy, Copy code")
require.EqualError(t, err, "parameter 'value' is required for action 'copy'") require.EqualError(t, err, "parameter 'value' is required for action 'copy'")
// Invalid syntax // Invalid syntax
_, err = parseActions(`label="Out of order!" x, action="http", url=http://example.com`) _, err = Parse(`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")
_, err = parseActions(`label="", action="http", url=http://example.com`) _, err = Parse(`label="", action="http", url=http://example.com`)
require.EqualError(t, err, "parameter 'label' is required") require.EqualError(t, err, "parameter 'label' is required")
_, err = parseActions(`label=, action="http", url=http://example.com`) _, err = Parse(`label=, action="http", url=http://example.com`)
require.EqualError(t, err, "parameter 'label' is required") require.EqualError(t, err, "parameter 'label' is required")
_, err = parseActions(`label="xx", action="http", url=http://example.com, what is this anyway`) _, err = Parse(`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")
_, err = parseActions(`fdsfdsf`) _, err = Parse(`fdsfdsf`)
require.EqualError(t, err, "parameter 'action' cannot be 'fdsfdsf', valid values are 'view', 'broadcast', 'http' and 'copy'") require.EqualError(t, err, "parameter 'action' cannot be 'fdsfdsf', valid values are 'view', 'broadcast', 'http' and 'copy'")
_, err = parseActions(`aaa=a, "bbb, 'ccc, ddd, eee "`) _, err = Parse(`aaa=a, "bbb, 'ccc, ddd, eee "`)
require.EqualError(t, err, "key 'aaa' unknown") require.EqualError(t, err, "key 'aaa' unknown")
_, err = parseActions(`action=http, label="omg the end quote is missing`) _, err = Parse(`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(`;;;;`) _, err = Parse(`;;;;`)
require.EqualError(t, err, "only 3 actions allowed") require.EqualError(t, err, "only 3 actions allowed")
_, err = parseActions(`,,,,,,;;`) _, err = Parse(`,,,,,,;;`)
require.EqualError(t, err, "term '' unknown") require.EqualError(t, err, "term '' unknown")
_, err = parseActions(`''";,;"`) _, err = Parse(`''";,;"`)
require.EqualError(t, err, "unexpected character '\"' at position 2") require.EqualError(t, err, "unexpected character '\"' at position 2")
_, err = parseActions(`action=http, label=a label, body=somebody`) _, err = Parse(`action=http, label=a label, body=somebody`)
require.EqualError(t, err, "parameter 'url' is required for action 'http'") require.EqualError(t, err, "parameter 'url' is required for action 'http'")
_, err = parseActions(`action=http, label=a label, url=http://ntfy.sh, method=HEAD, body=somebody`) _, err = Parse(`action=http, label=a label, url=http://ntfy.sh, method=HEAD, body=somebody`)
require.EqualError(t, err, "parameter 'body' cannot be set if method is HEAD") require.EqualError(t, err, "parameter 'body' cannot be set if method is HEAD")
_, err = parseActions(`[ invalid json ]`) _, err = Parse(`[ invalid json ]`)
require.EqualError(t, err, "JSON error: invalid character 'i' looking for beginning of value") require.EqualError(t, err, "JSON error: invalid character 'i' looking for beginning of value")
_, err = parseActions(`[ { "some": "object" } ]`) _, err = Parse(`[ { "some": "object" } ]`)
require.EqualError(t, err, "parameter 'action' cannot be '', valid values are 'view', 'broadcast', 'http' and 'copy'") require.EqualError(t, err, "parameter 'action' cannot be '', valid values are 'view', 'broadcast', 'http' and 'copy'")
_, err = parseActions("\x00\x01\xFFx\xFE") _, err = Parse("\x00\x01\xFFx\xFE")
require.EqualError(t, err, "invalid utf-8 string") require.EqualError(t, err, "invalid utf-8 string")
_, err = parseActions(`http, label, http://x.org, clear=x`) _, err = Parse(`http, label, http://x.org, clear=x`)
require.EqualError(t, err, "parameter 'clear' cannot be 'x', only boolean values are allowed (true/yes/1/false/no/0)") require.EqualError(t, err, "parameter 'clear' cannot be 'x', only boolean values are allowed (true/yes/1/false/no/0)")
} }
+2 -1
View File
@@ -29,6 +29,7 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"heckel.io/ntfy/v2/action"
"heckel.io/ntfy/v2/attachment" "heckel.io/ntfy/v2/attachment"
"heckel.io/ntfy/v2/db" "heckel.io/ntfy/v2/db"
"heckel.io/ntfy/v2/db/pg" "heckel.io/ntfy/v2/db/pg"
@@ -1195,7 +1196,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *model.Message) (cache bo
} }
actionsStr := readParam(r, "x-actions", "actions", "action") actionsStr := readParam(r, "x-actions", "actions", "action")
if actionsStr != "" { if actionsStr != "" {
m.Actions, e = parseActions(actionsStr) m.Actions, e = action.Parse(actionsStr)
if e != nil { if e != nil {
return false, false, "", "", "", false, "", errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error()) return false, false, "", "", "", false, "", errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error())
} }