[MM-24714] Render markdown in email notifications (#18043)
Automatic Merge
Этот коммит содержится в:
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/v6/model"
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,10 +214,16 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post,
|
|||||||
if emailNotificationContentsType == model.EmailNotificationContentsFull {
|
if emailNotificationContentsType == model.EmailNotificationContentsFull {
|
||||||
postMessage := a.GetMessageForNotification(post, translateFunc)
|
postMessage := a.GetMessageForNotification(post, translateFunc)
|
||||||
postMessage = html.EscapeString(postMessage)
|
postMessage = html.EscapeString(postMessage)
|
||||||
normalizedPostMessage, err := a.generateHyperlinkForChannels(postMessage, teamName, landingURL)
|
mdPostMessage, mdErr := utils.MarkdownToHTML(postMessage)
|
||||||
|
if mdErr != nil {
|
||||||
|
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(mdErr))
|
||||||
|
mdPostMessage = postMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedPostMessage, err := a.generateHyperlinkForChannels(mdPostMessage, teamName, landingURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mlog.Warn("Encountered error while generating hyperlink for channels", mlog.String("team_name", teamName), mlog.Err(err))
|
mlog.Warn("Encountered error while generating hyperlink for channels", mlog.String("team_name", teamName), mlog.Err(err))
|
||||||
normalizedPostMessage = postMessage
|
normalizedPostMessage = mdPostMessage
|
||||||
}
|
}
|
||||||
pData.Message = template.HTML(normalizedPostMessage)
|
pData.Message = template.HTML(normalizedPostMessage)
|
||||||
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
|
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
|
||||||
|
|||||||
@@ -825,3 +825,112 @@ func TestLandingLinkPermalink(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Contains(t, body, teamURL+"/pl/"+post.Id, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body))
|
require.Contains(t, body, teamURL+"/pl/"+post.Id, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarkdownConversion(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "markdown: escape string test",
|
||||||
|
args: "<b>not bold</b>",
|
||||||
|
want: "<b>not bold</b>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "markdown: strong",
|
||||||
|
args: "This is **Mattermost**",
|
||||||
|
want: "This is <strong>Mattermost</strong>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "markdown: blockquote",
|
||||||
|
args: "Below is blockquote\n" +
|
||||||
|
"> This is Mattermost blockquote\n" +
|
||||||
|
"> on multiple lines!",
|
||||||
|
want: "<blockquote>\n" +
|
||||||
|
"<p>This is Mattermost blockquote\n" +
|
||||||
|
"on multiple lines!</p>\n" +
|
||||||
|
"</blockquote>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "markdown: emphasis",
|
||||||
|
args: "This is *Mattermost*",
|
||||||
|
want: "This is <em>Mattermost</em>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "markdown: links",
|
||||||
|
args: "This is [Mattermost](https://mattermost.com)",
|
||||||
|
want: "This is <a href=\"https://mattermost.com\">Mattermost</a>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "markdown: strikethrough",
|
||||||
|
args: "This is ~~Mattermost~~",
|
||||||
|
want: "This is <del>Mattermost</del>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "markdown: table",
|
||||||
|
args: "| Tables | Are | Cool |\n" +
|
||||||
|
"| ------------- |:-------------:| -----:|\n" +
|
||||||
|
"| col 3 is | right-aligned | $1600 |\n" +
|
||||||
|
"| col 2 is | centered | $12 |\n" +
|
||||||
|
"| zebra stripes | are neat | $1 |\n",
|
||||||
|
want: "<table>\n" +
|
||||||
|
"<thead>\n" +
|
||||||
|
"<tr>\n" +
|
||||||
|
"<th>Tables</th>\n" +
|
||||||
|
"<th style=\"text-align:center\">Are</th>\n" +
|
||||||
|
"<th style=\"text-align:right\">Cool</th>\n" +
|
||||||
|
"</tr>\n" +
|
||||||
|
"</thead>\n" +
|
||||||
|
"<tbody>\n" +
|
||||||
|
"<tr>\n" +
|
||||||
|
"<td>col 3 is</td>\n" +
|
||||||
|
"<td style=\"text-align:center\">right-aligned</td>\n" +
|
||||||
|
"<td style=\"text-align:right\">$1600</td>\n" +
|
||||||
|
"</tr>\n" +
|
||||||
|
"<tr>\n" +
|
||||||
|
"<td>col 2 is</td>\n" +
|
||||||
|
"<td style=\"text-align:center\">centered</td>\n" +
|
||||||
|
"<td style=\"text-align:right\">$12</td>\n" +
|
||||||
|
"</tr>\n" +
|
||||||
|
"<tr>\n" +
|
||||||
|
"<td>zebra stripes</td>\n" +
|
||||||
|
"<td style=\"text-align:center\">are neat</td>\n" +
|
||||||
|
"<td style=\"text-align:right\">$1</td>\n" +
|
||||||
|
"</tr>\n" +
|
||||||
|
"</tbody>\n" +
|
||||||
|
"</table>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "markdown: multiline with header and links",
|
||||||
|
args: "###### H6 header\n[link 1](https://mattermost.com) - [link 2](https://mattermost.com)",
|
||||||
|
want: "<h6>H6 header</h6>\n" +
|
||||||
|
"<p><a href=\"https://mattermost.com\">link 1</a> - <a href=\"https://mattermost.com\">link 2</a></p>",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
th := SetupWithStoreMock(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
recipient := &model.User{}
|
||||||
|
storeMock := th.App.Srv().Store.(*mocks.Store)
|
||||||
|
teamStoreMock := mocks.TeamStore{}
|
||||||
|
teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil)
|
||||||
|
storeMock.On("Team").Return(&teamStoreMock)
|
||||||
|
channel := &model.Channel{
|
||||||
|
DisplayName: "ChannelName",
|
||||||
|
Type: model.ChannelTypeOpen,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
post := &model.Post{
|
||||||
|
Id: "Test_id",
|
||||||
|
Message: tt.args,
|
||||||
|
}
|
||||||
|
got, err := th.App.getNotificationEmailBody(recipient, post, channel, "ChannelName", "sender", "testteam", "http://localhost:8065/landing#/testteam", model.EmailNotificationContentsFull, true, i18n.GetUserTranslations("en"), "user-avatar.png")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, got, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
@@ -33,6 +35,28 @@ func StripMarkdown(markdown string) (string, error) {
|
|||||||
return strings.TrimSpace(buf.String()), nil
|
return strings.TrimSpace(buf.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkdownToHTML takes a string containing Markdown and returns a string with HTML tagged version
|
||||||
|
func MarkdownToHTML(markdown string) (string, error) {
|
||||||
|
// Unescape any blockquote text to be parsed by the markdown parser.
|
||||||
|
re := regexp.MustCompile(`^|\n(>)`)
|
||||||
|
markdownClean := re.ReplaceAllFunc([]byte(markdown), func(s []byte) []byte {
|
||||||
|
out := html.UnescapeString(string(s))
|
||||||
|
return []byte(out)
|
||||||
|
})
|
||||||
|
|
||||||
|
md := goldmark.New(
|
||||||
|
goldmark.WithExtensions(extension.GFM),
|
||||||
|
)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
err := md.Convert(markdownClean, &b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
type notificationRenderer struct {
|
type notificationRenderer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user