Browse Source

Support for templating the priority header

binwiederhier 1 tuần trước cách đây
mục cha
commit
570b188a88
5 tập tin đã thay đổi với 194 bổ sung50 xóa
  1. 19 14
      docs/publish.md
  2. 4 0
      docs/releases.md
  3. 55 33
      server/server.go
  4. 111 0
      server/server_test.go
  5. 5 3
      server/types.go

+ 19 - 14
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.

+ 4 - 0
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)

+ 55 - 33
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
 }

+ 111 - 0
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))

+ 5 - 3
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 {