diff --git a/docs/publish.md b/docs/publish.md index 3c8cbdcc..8b267150 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -2643,7 +2643,7 @@ You can enable templating by setting the `X-Template` header (or its aliases `Te will use a custom template file from the template directory (defaults to `/etc/ntfy/templates`, can be overridden with `template-dir`). See [custom templates](#custom-templates) for more details. * **Inline templating**: Setting the `X-Template` header or query parameter to `yes` or `1` (e.g. `?template=yes`) - will enable inline templating, which means that the `message` and/or `title` will be parsed as a Go template. + will enable inline templating, which means that the `message`, `title`, and/or `priority` will be parsed as a Go template. See [inline templating](#inline-templating) for more details. To learn the basics of Go's templating language, please see [template syntax](#template-syntax). @@ -2686,7 +2686,7 @@ and set the `X-Template` header or query parameter to the name of the template f For example, if you have a template file `/etc/ntfy/templates/myapp.yml`, you can set the header `X-Template: myapp` or the query parameter `?template=myapp` to use it. -Template files must have the `.yml` (not: `.yaml`!) extension and must be formatted as YAML. They may contain `title` and `message` keys, +Template files must have the `.yml` (not: `.yaml`!) extension and must be formatted as YAML. They may contain `title`, `message`, and `priority` keys, which are interpreted as Go templates. Here's an **example custom template**: @@ -2704,6 +2704,11 @@ Here's an **example custom template**: Status: {{ .status }} Type: {{ .type | upper }} ({{ .percent }}%) Server: {{ .server }} + priority: | + {{ if gt .percent 90.0 }}5 + {{ else if gt .percent 75.0 }}4 + {{ else }}3 + {{ end }} ``` Once you have the template file in place, you can send the payload to your topic using the `X-Template` @@ -2785,7 +2790,7 @@ Which will result in a notification that looks like this: ### Inline templating -When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your +When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message`, `title`, and `priority` fields of your webhook payload. Inline templates are most useful for templated one-off messages, or if you do not control the ntfy server (e.g., if you're using ntfy.sh). @@ -2841,12 +2846,12 @@ Here's an **easier example with a shorter JSON payload**: curl \ --globoff \ -d '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \ - 'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}' + 'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}' ``` === "HTTP" ``` http - POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}} HTTP/1.1 + POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}} HTTP/1.1 Host: ntfy.sh {"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} @@ -2854,7 +2859,7 @@ Here's an **easier example with a shorter JSON payload**: === "JavaScript" ``` javascript - fetch('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}', { + fetch('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}', { method: 'POST', body: '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' }) @@ -2863,7 +2868,7 @@ Here's an **easier example with a shorter JSON payload**: === "Go" ``` go body := `{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}` - uri := "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}" + uri := `https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if eq .error.level "severe"}}5{{else}}3{{end}}` req, _ := http.NewRequest("POST", uri, strings.NewReader(body)) http.DefaultClient.Do(req) ``` @@ -2873,7 +2878,7 @@ Here's an **easier example with a shorter JSON payload**: ``` powershell $Request = @{ Method = "POST" - URI = "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}" + URI = 'https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}' Body = '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' ContentType = "application/json" } @@ -2883,14 +2888,14 @@ Here's an **easier example with a shorter JSON payload**: === "Python" ``` python requests.post( - "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}", + 'https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}', data='{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' ) ``` === "PHP" ``` php-inline - file_get_contents("https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}", false, stream_context_create([ + file_get_contents('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}&p={{if+eq+.error.level+"severe"}}5{{else}}3{{end}}', false, stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json", @@ -2899,9 +2904,9 @@ Here's an **easier example with a shorter JSON payload**: ])); ``` -This example uses the `message`/`m` and `title`/`t` query parameters, but obviously this also works with the corresponding -`Message`/`Title` headers. It will send a notification with a title `phil-pc: A severe error has occurred` and a message -`Error message: Disk has run out of space`. +This example uses the `message`/`m`, `title`/`t`, and `priority`/`p` query parameters, but obviously this also works with the +corresponding headers. It will send a notification with a title `phil-pc: A severe error has occurred`, a message +`Error message: Disk has run out of space`, and priority `5` (max) if the level is "severe", or `3` (default) otherwise. ### Template syntax ntfy uses [Go templates](https://pkg.go.dev/text/template) for its templates, which is arguably one of the most powerful, @@ -2920,7 +2925,7 @@ your templates there first ([example for Grafana alert](https://repeatit.io/#/sh ntfy supports a subset of the **[Sprig template functions](publish/template-functions.md)** (originally copied from [Sprig](https://github.com/Masterminds/sprig), thank you to the Sprig developers 🙏). This is useful for advanced message templating and for transforming the data provided through the JSON payload. -Below are the functions that are available to use inside your message/title templates. +Below are the functions that are available to use inside your message, title, and priority templates. * [String Functions](publish/template-functions.md#string-functions): `trim`, `trunc`, `substr`, `plural`, etc. * [String List Functions](publish/template-functions.md#string-list-functions): `splitList`, `sortAlpha`, etc. diff --git a/docs/releases.md b/docs/releases.md index 2a809fcd..fccf9317 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1681,6 +1681,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ### ntfy server v2.17.x (UNRELEASED) +**Features:** + +* Support templating in the priority field ([#1426](https://github.com/binwiederhier/ntfy/issues/1426), thanks to [@seantomburke](https://github.com/seantomburke) for reporting) + **Bug fixes + maintenance:** * Fix crash when commit string is shorter than 7 characters in non-GitHub-Action builds ([#1493](https://github.com/binwiederhier/ntfy/issues/1493), thanks to [@cyrinux](https://github.com/cyrinux) for reporting) diff --git a/server/server.go b/server/server.go index ecd05f21..4e03ef5a 100644 --- a/server/server.go +++ b/server/server.go @@ -793,7 +793,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e return nil, err } m := newDefaultMessage(t.ID, "") - cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m) + cache, firebase, email, call, template, unifiedpush, priorityStr, e := s.parsePublishParams(r, m) if e != nil { return nil, e.With(t) } @@ -824,7 +824,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e if cache { m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() } - if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil { + if err := s.handlePublishBody(r, v, m, body, template, unifiedpush, priorityStr); err != nil { return nil, err } if m.Message == "" { @@ -1055,11 +1055,11 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { } } -func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) { +func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, priorityStr string, err *errHTTP) { if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) { pathSequenceID, err := s.sequenceIDFromPath(r.URL.Path) if err != nil { - return false, false, "", "", "", false, err + return false, false, "", "", "", false, "", err } m.SequenceID = pathSequenceID } else { @@ -1068,7 +1068,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if sequenceIDRegex.MatchString(sequenceID) { m.SequenceID = sequenceID } else { - return false, false, "", "", "", false, errHTTPBadRequestSequenceIDInvalid + return false, false, "", "", "", false, "", errHTTPBadRequestSequenceIDInvalid } } else { m.SequenceID = m.ID @@ -1089,7 +1089,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if attach != "" { if !urlRegex.MatchString(attach) { - return false, false, "", "", "", false, errHTTPBadRequestAttachmentURLInvalid + return false, false, "", "", "", false, "", errHTTPBadRequestAttachmentURLInvalid } m.Attachment.URL = attach if m.Attachment.Name == "" { @@ -1107,19 +1107,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if icon != "" { if !urlRegex.MatchString(icon) { - return false, false, "", "", "", false, errHTTPBadRequestIconURLInvalid + return false, false, "", "", "", false, "", errHTTPBadRequestIconURLInvalid } m.Icon = icon } email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") if s.smtpSender == nil && email != "" { - return false, false, "", "", "", false, errHTTPBadRequestEmailDisabled + return false, false, "", "", "", false, "", errHTTPBadRequestEmailDisabled } call = readParam(r, "x-call", "call") if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { - return false, false, "", "", "", false, errHTTPBadRequestPhoneCallsDisabled + return false, false, "", "", "", false, "", errHTTPBadRequestPhoneCallsDisabled } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { - return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid + return false, false, "", "", "", false, "", errHTTPBadRequestPhoneNumberInvalid } template = templateMode(readParam(r, "x-template", "template", "tpl")) messageStr := readParam(r, "x-message", "message", "m") @@ -1131,29 +1131,33 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi m.Message = messageStr } var e error - m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) - if e != nil { - return false, false, "", "", "", false, errHTTPBadRequestPriorityInvalid + priorityStr = readParam(r, "x-priority", "priority", "prio", "p") + if !template.Enabled() { + m.Priority, e = util.ParsePriority(priorityStr) + if e != nil { + return false, false, "", "", "", false, "", errHTTPBadRequestPriorityInvalid + } + priorityStr = "" // Clear since it's already parsed } m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta") delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") if delayStr != "" { if !cache { - return false, false, "", "", "", false, errHTTPBadRequestDelayNoCache + return false, false, "", "", "", false, "", errHTTPBadRequestDelayNoCache } if email != "" { - return false, false, "", "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) + return false, false, "", "", "", false, "", errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) } if call != "" { - return false, false, "", "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) + return false, false, "", "", "", false, "", errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) } delay, err := util.ParseFutureTime(delayStr, time.Now()) if err != nil { - return false, false, "", "", "", false, errHTTPBadRequestDelayCannotParse + return false, false, "", "", "", false, "", errHTTPBadRequestDelayCannotParse } else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() { - return false, false, "", "", "", false, errHTTPBadRequestDelayTooSmall + return false, false, "", "", "", false, "", errHTTPBadRequestDelayTooSmall } else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() { - return false, false, "", "", "", false, errHTTPBadRequestDelayTooLarge + return false, false, "", "", "", false, "", errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() } @@ -1161,7 +1165,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if actionsStr != "" { m.Actions, e = parseActions(actionsStr) if e != nil { - return false, false, "", "", "", false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error()) + return false, false, "", "", "", false, "", errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error()) } } contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") @@ -1180,7 +1184,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi cache = false email = "" } - return cache, firebase, email, call, template, unifiedpush, nil + return cache, firebase, email, call, template, unifiedpush, priorityStr, nil } // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. @@ -1199,7 +1203,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message // 7. curl -T file.txt ntfy.sh/mytopic // In all other cases, mostly if file.txt is > message limit, treat it as an attachment -func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template templateMode, unifiedpush bool) error { +func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template templateMode, unifiedpush bool, priorityStr string) error { if m.Event == pollRequestEvent { // Case 1 return s.handleBodyDiscard(body) } else if unifiedpush { @@ -1209,7 +1213,7 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body } else if m.Attachment != nil && m.Attachment.Name != "" { return s.handleBodyAsAttachment(r, v, m, body) // Case 4 } else if template.Enabled() { - return s.handleBodyAsTemplatedTextMessage(m, template, body) // Case 5 + return s.handleBodyAsTemplatedTextMessage(m, template, body, priorityStr) // Case 5 } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { return s.handleBodyAsTextMessage(m, body) // Case 6 } @@ -1245,7 +1249,7 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser return nil } -func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateMode, body *util.PeekedReadCloser) error { +func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateMode, body *util.PeekedReadCloser, priorityStr string) error { body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit)) if err != nil { return err @@ -1258,7 +1262,7 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateM return err } } else { - if err := s.renderTemplateFromParams(m, peekedBody); err != nil { + if err := s.renderTemplateFromParams(m, peekedBody, priorityStr); err != nil { return err } } @@ -1289,33 +1293,51 @@ func (s *Server) renderTemplateFromFile(m *message, templateName, peekedBody str } var err error if tpl.Message != nil { - if m.Message, err = s.renderTemplate(*tpl.Message, peekedBody); err != nil { + if m.Message, err = s.renderTemplate(templateName+" (message)", *tpl.Message, peekedBody); err != nil { return err } } if tpl.Title != nil { - if m.Title, err = s.renderTemplate(*tpl.Title, peekedBody); err != nil { + if m.Title, err = s.renderTemplate(templateName+" (title)", *tpl.Title, peekedBody); err != nil { return err } } + if tpl.Priority != nil { + renderedPriority, err := s.renderTemplate(templateName+" (priority)", *tpl.Priority, peekedBody) + if err != nil { + return err + } + if m.Priority, err = util.ParsePriority(renderedPriority); err != nil { + return errHTTPBadRequestPriorityInvalid + } + } return nil } // renderTemplateFromParams transforms the JSON message body according to the inline template in the -// message and title parameters. -func (s *Server) renderTemplateFromParams(m *message, peekedBody string) error { +// message, title, and priority parameters. +func (s *Server) renderTemplateFromParams(m *message, peekedBody string, priorityStr string) error { var err error - if m.Message, err = s.renderTemplate(m.Message, peekedBody); err != nil { + if m.Message, err = s.renderTemplate("priority query parameter", m.Message, peekedBody); err != nil { return err } - if m.Title, err = s.renderTemplate(m.Title, peekedBody); err != nil { + if m.Title, err = s.renderTemplate("title query parameter", m.Title, peekedBody); err != nil { return err } + if priorityStr != "" { + renderedPriority, err := s.renderTemplate("priority query parameter", priorityStr, peekedBody) + if err != nil { + return err + } + if m.Priority, err = util.ParsePriority(renderedPriority); err != nil { + return errHTTPBadRequestPriorityInvalid + } + } return nil } // renderTemplate renders a template with the given JSON source data. -func (s *Server) renderTemplate(tpl string, source string) (string, error) { +func (s *Server) renderTemplate(name, tpl, source string) (string, error) { if templateDisallowedRegex.MatchString(tpl) { return "", errHTTPBadRequestTemplateDisallowedFunctionCalls } @@ -1330,7 +1352,7 @@ func (s *Server) renderTemplate(tpl string, source string) (string, error) { var buf bytes.Buffer limitWriter := util.NewLimitWriter(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), util.NewFixedLimiter(templateMaxOutputBytes)) if err := t.Execute(limitWriter, data); err != nil { - return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error()) + return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("template %s: %s", name, err.Error()) } return strings.TrimSpace(strings.ReplaceAll(buf.String(), "\\n", "\n")), nil // replace any remaining "\n" (those outside of template curly braces) with newlines } diff --git a/server/server_test.go b/server/server_test.go index aaae563f..a0eb14c4 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3290,6 +3290,117 @@ func TestServer_MessageTemplate_Until100_000(t *testing.T) { require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations") } +func TestServer_MessageTemplate_Priority(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"priority":"5"}`, map[string]string{ + "X-Message": "Test message", + "X-Priority": "{{.priority}}", + "X-Template": "1", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Test message", m.Message) + require.Equal(t, 5, m.Priority) +} + +func TestServer_MessageTemplate_Priority_Conditional(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + + // Test with error status -> priority 5 + response := request(t, s, "PUT", "/mytopic", `{"status":"Error","message":"Something went wrong"}`, map[string]string{ + "X-Message": "Status: {{.status}} - {{.message}}", + "X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`, + "X-Template": "1", + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Status: Error - Something went wrong", m.Message) + require.Equal(t, 5, m.Priority) + + // Test with success status -> priority 3 + response = request(t, s, "PUT", "/mytopic", `{"status":"Success","message":"All good"}`, map[string]string{ + "X-Message": "Status: {{.status}} - {{.message}}", + "X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`, + "X-Template": "1", + }) + require.Equal(t, 200, response.Code) + m = toMessage(t, response.Body.String()) + require.Equal(t, "Status: Success - All good", m.Message) + require.Equal(t, 3, m.Priority) +} + +func TestServer_MessageTemplate_Priority_NamedValue(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"severity":"high"}`, map[string]string{ + "X-Message": "Alert", + "X-Priority": "{{.severity}}", + "X-Template": "1", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, 4, m.Priority) // "high" = 4 +} + +func TestServer_MessageTemplate_Priority_Invalid(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"priority":"invalid"}`, map[string]string{ + "X-Message": "Test message", + "X-Priority": "{{.priority}}", + "X-Template": "1", + }) + + require.Equal(t, 400, response.Code) + require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code) +} + +func TestServer_MessageTemplate_Priority_QueryParam(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic?template=1&priority={{.priority}}", `{"priority":"max"}`, nil) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, 5, m.Priority) // "max" = 5 +} + +func TestServer_MessageTemplate_Priority_FromTemplateFile(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.TemplateDir = t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "priority-test.yml"), []byte(` +title: "{{.title}}" +message: "{{.message}}" +priority: '{{if eq .level "critical"}}5{{else if eq .level "warning"}}4{{else}}3{{end}}' +`), 0644)) + s := newTestServer(t, c) + + // Test with critical level + response := request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"System down","level":"critical"}`, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Alert", m.Title) + require.Equal(t, "System down", m.Message) + require.Equal(t, 5, m.Priority) + + // Test with warning level + response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"High load","level":"warning"}`, nil) + require.Equal(t, 200, response.Code) + m = toMessage(t, response.Body.String()) + require.Equal(t, 4, m.Priority) + + // Test with info level + response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"All good","level":"info"}`, nil) + require.Equal(t, 200, response.Code) + m = toMessage(t, response.Body.String()) + require.Equal(t, 3, m.Priority) +} + func TestServer_DeleteMessage(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfig(t)) diff --git a/server/types.go b/server/types.go index fae44e5b..3494f570 100644 --- a/server/types.go +++ b/server/types.go @@ -299,7 +299,7 @@ func (t templateMode) FileName() string { return "" } -// templateFile represents a template file with title and message +// templateFile represents a template file with title, message, and priority // It is used for file-based templates, e.g. grafana, influxdb, etc. // // Example YAML: @@ -308,9 +308,11 @@ func (t templateMode) FileName() string { // message: | // This is a {{ .Type }} alert. // It can be multiline. +// priority: '{{ if eq .status "Error" }}5{{ else }}3{{ end }}' type templateFile struct { - Title *string `yaml:"title"` - Message *string `yaml:"message"` + Title *string `yaml:"title"` + Message *string `yaml:"message"` + Priority *string `yaml:"priority"` } type apiHealthResponse struct {