From 499b2fb0d6e03a19a1a82b195f76857b18364c0e Mon Sep 17 00:00:00 2001
From: binwiederhier <philipp.heckel@gmail.com>
Date: Sat, 8 Jul 2023 15:48:08 -0400
Subject: [PATCH] Docs, tests

---
 docs/publish.md                      | 30 ++++++++++++++++++
 server/server.go                     |  5 ++-
 server/server_test.go                | 46 ++++++++++++++++++++++++++++
 server/types.go                      |  1 +
 web/src/components/Notifications.jsx |  2 +-
 5 files changed, 82 insertions(+), 2 deletions(-)

diff --git a/docs/publish.md b/docs/publish.md
index 11e33e61..905508fe 100644
--- a/docs/publish.md
+++ b/docs/publish.md
@@ -623,6 +623,35 @@ them with a comma, e.g. `tag1,tag2,tag3`.
     as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `tag1,=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
     or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
 
+## Markdown
+_Supported on:_ :material-firefox:
+
+You can format messages using [Markdown](https://www.markdownguide.org/basic-syntax/). 🤩
+
+By default, messages sent to ntfy are rendered as plain text. To enable Markdown, set the `X-Markdown` header (or any of 
+its aliases: `Markdown`, or `md`) to `true` (or `1` or `yes`), or set the `Content-Type` header to `text/markdown`. 
+
+Supported Markdown features:
+
+- **bold** (`**bold**`)
+- *italic* (`*italic*`)
+- [links](https://www.markdownguide.org/basic-syntax/#links) (`[links](https://www.markdownguide.org/basic-syntax/#links)`)
+- [images](https://www.markdownguide.org/basic-syntax/#images) (`![images](https://www.markdownguide.org/basic-syntax/#images)`)
+- [code blocks](https://www.markdownguide.org/basic-syntax/#code-blocks) (`` `code blocks` ``)
+- [inline code](https://www.markdownguide.org/basic-syntax/#inline-code) (`` `inline code` ``)
+- [headings](https://www.markdownguide.org/basic-syntax/#headings) (`# headings`)
+- [lists](https://www.markdownguide.org/basic-syntax/#lists) (`- lists`)
+- [blockquotes](https://www.markdownguide.org/basic-syntax/#blockquotes) (`> blockquotes`)
+- [horizontal rules](https://www.markdownguide.org/basic-syntax/#horizontal-rules) (`---`)
+
+XXXXXXXXXXXXXXXXXXXXXx
+- examples
+- supported only on Web for now
+
+XXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx
+
+
+
 ## Scheduled delivery
 _Supported on:_ :material-android: :material-apple: :material-firefox:
 
@@ -1004,6 +1033,7 @@ all the supported fields:
 | `actions`  | -        | *JSON array*                     | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications       |
 | `click`    | -        | *URL*                            | `https://example.com`                     | Website opened when notification is [clicked](#click-action)          |
 | `attach`   | -        | *URL*                            | `https://example.com/file.jpg`            | URL of an attachment, see [attach via URL](#attach-file-from-url)     |
+| `markdown` | -        | *bool*                           | `true`                                    | Set to true if the `message` is Markdown-formatted                    |
 | `icon`     | -        | *string*                         | `https://example.com/icon.png`            | URL to use as notification [icon](#icons)                             |
 | `filename` | -        | *string*                         | `file.jpg`                                | File name of the attachment                                           |
 | `delay`    | -        | *string*                         | `30min`, `9am`                            | Timestamp or duration for delayed delivery                            |
diff --git a/server/server.go b/server/server.go
index 60a2fb30..0ab36524 100644
--- a/server/server.go
+++ b/server/server.go
@@ -1010,7 +1010,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
 			return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
 		}
 	}
-	contentType, markdown := readParam(r, "content-type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
+	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"
 	}
@@ -1789,6 +1789,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
 		if m.Icon != "" {
 			r.Header.Set("X-Icon", m.Icon)
 		}
+		if m.Markdown {
+			r.Header.Set("X-Markdown", "yes")
+		}
 		if len(m.Actions) > 0 {
 			actionsStr, err := json.Marshal(m.Actions)
 			if err != nil {
diff --git a/server/server_test.go b/server/server_test.go
index e9ff6fcb..46751acd 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -1518,6 +1518,39 @@ func TestServer_PublishActions_AndPoll(t *testing.T) {
 	require.Equal(t, "target_temp_f=65", m.Actions[1].Body)
 }
 
+func TestServer_PublishMarkdown(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+	response := request(t, s, "PUT", "/mytopic", "_underline this_", map[string]string{
+		"Content-Type": "text/markdown",
+	})
+	require.Equal(t, 200, response.Code)
+
+	m := toMessage(t, response.Body.String())
+	require.Equal(t, "_underline this_", m.Message)
+	require.Equal(t, "text/markdown", m.ContentType)
+}
+
+func TestServer_PublishMarkdown_QueryParam(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+	response := request(t, s, "PUT", "/mytopic?md=1", "_underline this_", nil)
+	require.Equal(t, 200, response.Code)
+
+	m := toMessage(t, response.Body.String())
+	require.Equal(t, "_underline this_", m.Message)
+	require.Equal(t, "text/markdown", m.ContentType)
+}
+
+func TestServer_PublishMarkdown_NotMarkdown(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+	response := request(t, s, "PUT", "/mytopic", "_underline this_", map[string]string{
+		"Content-Type": "not-markdown",
+	})
+	require.Equal(t, 200, response.Code)
+
+	m := toMessage(t, response.Body.String())
+	require.Equal(t, "", m.ContentType)
+}
+
 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"],` +
@@ -1535,12 +1568,25 @@ func TestServer_PublishAsJSON(t *testing.T) {
 	require.Equal(t, "google.pdf", m.Attachment.Name)
 	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, 4, m.Priority)
 	require.True(t, m.Time > time.Now().Unix()+29*60)
 	require.True(t, m.Time < time.Now().Unix()+31*60)
 }
 
+func TestServer_PublishAsJSON_Markdown(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+	body := `{"topic":"mytopic","message":"**This is bold**","markdown":true}`
+	response := request(t, s, "PUT", "/", body, nil)
+	require.Equal(t, 200, response.Code)
+
+	m := toMessage(t, response.Body.String())
+	require.Equal(t, "mytopic", m.Topic)
+	require.Equal(t, "**This is bold**", m.Message)
+	require.Equal(t, "text/markdown", m.ContentType)
+}
+
 func TestServer_PublishAsJSON_RateLimit_MessageDailyLimit(t *testing.T) {
 	// Publishing as JSON follows a different path. This ensures that rate
 	// limiting works for this endpoint as well
diff --git a/server/types.go b/server/types.go
index 279f4ce8..eeb566fc 100644
--- a/server/types.go
+++ b/server/types.go
@@ -101,6 +101,7 @@ type publishMessage struct {
 	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"`
diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx
index ccd2deb9..bd319dc5 100644
--- a/web/src/components/Notifications.jsx
+++ b/web/src/components/Notifications.jsx
@@ -192,7 +192,7 @@ const MarkdownContainer = styled("div")`
   ol {
     padding-inline: 1rem;
   }
-  
+
   img {
     max-width: 100%;
   }