send telegram messages in HTML mode
That resolves problem with inability to properly render message text in the resulting telegram message due to markdown escaping trickiness.
This commit is contained in:
committed by
Umputun
parent
f817fd38ed
commit
498073c509
@@ -5,13 +5,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/go-pkgz/repeater"
|
||||
@@ -110,7 +110,7 @@ func (t *Telegram) Send(ctx context.Context, req Request) error {
|
||||
log.Printf("[DEBUG] send telegram notification for comment ID %s", req.Comment.ID)
|
||||
result := new(multierror.Error)
|
||||
|
||||
msg, err := buildTelegramMessage(req)
|
||||
msg, err := buildMessage(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to make telegram message body for comment ID %s", req.Comment.ID)
|
||||
}
|
||||
@@ -178,26 +178,27 @@ func (t *Telegram) sendMessage(ctx context.Context, b []byte, chatID string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildTelegramMessage(req Request) ([]byte, error) {
|
||||
// buildMessage generates message for generic notification about new comment
|
||||
func buildMessage(req Request) ([]byte, error) {
|
||||
commentURLPrefix := req.Comment.Locator.URL + uiNav
|
||||
|
||||
msg := fmt.Sprintf("[%s](%s)", escapeText(req.Comment.User.Name), commentURLPrefix+req.Comment.ID)
|
||||
msg := fmt.Sprintf(`<a href="%s">%s</a>`, commentURLPrefix+req.Comment.ID, escapeTelegramText(req.Comment.User.Name))
|
||||
|
||||
if req.Comment.ParentID != "" {
|
||||
msg += fmt.Sprintf(" -> [%s](%s)", escapeText(req.parent.User.Name), commentURLPrefix+req.parent.ID)
|
||||
msg += fmt.Sprintf(" -> <a href=\"%s\">%s</a>", commentURLPrefix+req.parent.ID, escapeTelegramText(req.parent.User.Name))
|
||||
}
|
||||
|
||||
msg += fmt.Sprintf("\n\n%s", escapeText(req.Comment.Orig))
|
||||
msg += fmt.Sprintf("\n\n%s", telegramSupportedHTML(req.Comment.Text))
|
||||
|
||||
if req.Comment.ParentID != "" {
|
||||
msg += fmt.Sprintf("\n\n> \"_%s_\"", escapeText(req.parent.Orig))
|
||||
msg += fmt.Sprintf("\n\n \"_%s_\"", telegramSupportedHTML(req.parent.Text))
|
||||
}
|
||||
|
||||
if req.Comment.PostTitle != "" {
|
||||
msg += fmt.Sprintf("\n\n↦ [%s](%s)", escapeText(req.Comment.PostTitle), req.Comment.Locator.URL)
|
||||
msg += fmt.Sprintf("\n\n↦ <a href=\"%s\">%s</a>", req.Comment.Locator.URL, escapeTelegramText(req.Comment.PostTitle))
|
||||
}
|
||||
|
||||
body := telegramMsg{Text: msg, ParseMode: "MarkdownV2"}
|
||||
body := telegramMsg{Text: msg, ParseMode: "HTML"}
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -205,22 +206,24 @@ func buildTelegramMessage(req Request) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func escapeText(text string) string {
|
||||
escSymbols := []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
|
||||
res := html.UnescapeString(text)
|
||||
for _, esc := range escSymbols {
|
||||
res = strings.Replace(res, esc, "\\"+esc, -1)
|
||||
}
|
||||
return res
|
||||
// returns HTML with only tags allowed in Telegram HTML message payload
|
||||
// https://core.telegram.org/bots/api#html-style
|
||||
func telegramSupportedHTML(htmlText string) string {
|
||||
p := bluemonday.NewPolicy()
|
||||
p.AllowElements("b", "strong", "i", "em", "u", "ins", "s", "strike", "del", "a", "code", "pre")
|
||||
p.AllowAttrs("href").OnElements("a")
|
||||
p.AllowAttrs("class").OnElements("code")
|
||||
return p.Sanitize(htmlText)
|
||||
}
|
||||
|
||||
func escapeCode(text string) string {
|
||||
escSymbols := []string{"`", `\`}
|
||||
res := text
|
||||
for _, esc := range escSymbols {
|
||||
res = strings.Replace(res, esc, "\\"+esc, -1)
|
||||
}
|
||||
return res
|
||||
// returns text sanitized of symbols not allowed inside other HTML tags in Telegram HTML message payload
|
||||
// https://core.telegram.org/bots/api#html-style
|
||||
func escapeTelegramText(text string) string {
|
||||
// order is important
|
||||
text = strings.ReplaceAll(text, "&", "&")
|
||||
text = strings.ReplaceAll(text, "<", "<")
|
||||
text = strings.ReplaceAll(text, ">", ">")
|
||||
return text
|
||||
}
|
||||
|
||||
// SendVerification sends user verification message to the specified user
|
||||
@@ -246,11 +249,11 @@ func (t *Telegram) SendVerification(ctx context.Context, req VerificationRequest
|
||||
|
||||
// buildVerificationMessage generates verification telegram message based on given input
|
||||
func (t *Telegram) buildVerificationMessage(user, token, site string) ([]byte, error) {
|
||||
result := fmt.Sprintf("Confirmation for *%s* on site %s\n"+
|
||||
result := fmt.Sprintf("Confirmation for <i>%s</i> on site %s\n"+
|
||||
"Please copy and paste this text into “token” field on comments page to confirm subscription:\n\n\n"+
|
||||
"```%s```",
|
||||
escapeText(user), escapeText(site), escapeCode(token))
|
||||
body := telegramMsg{Text: result, ParseMode: "MarkdownV2"}
|
||||
"<pre>%s</pre>",
|
||||
escapeTelegramText(user), escapeTelegramText(site), escapeTelegramText(token))
|
||||
body := telegramMsg{Text: result, ParseMode: "HTML"}
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -102,9 +101,9 @@ func TestTelegram_Send(t *testing.T) {
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tb)
|
||||
c := store.Comment{Text: "some text", ParentID: "1", ID: "999"}
|
||||
c := store.Comment{Text: "some text", ParentID: "1", ID: "999", Locator: store.Locator{URL: "http://example.org/"}}
|
||||
c.User.Name = "from"
|
||||
cp := store.Comment{Text: "some parent text"}
|
||||
cp := store.Comment{Text: `<p>some parent text with a <a href="http://example.org">link</a> and special text:<br>& < > &</p>`}
|
||||
cp.User.Name = "to"
|
||||
|
||||
err = tb.Send(context.TODO(), Request{Comment: c, parent: cp, Telegrams: []string{"test_user_channel"}})
|
||||
@@ -136,6 +135,15 @@ func TestTelegram_Send(t *testing.T) {
|
||||
tb.apiPrefix = "http://non-existent"
|
||||
err = tb.Send(context.TODO(), Request{Comment: c, parent: cp, Telegrams: []string{"test_user_channel"}})
|
||||
assert.Error(t, err)
|
||||
|
||||
// test buildMessage separately for message text
|
||||
res, err := buildMessage(Request{Comment: c, parent: cp})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `{"text":"\u003ca href=\"http://example.org/#remark42__comment-999\"\u003efrom\u003c/a\u003e -\u003e \u003ca href=\"http://example.org/#remark42__comment-\"\u003eto\u003c/a\u003e\n\n`+
|
||||
`some text\n\n`+
|
||||
` \"_some parent text with a \u003ca href=\"http://example.org\"\u003elink\u003c/a\u003e and special text:\u0026amp; \u0026lt; \u0026gt; \u0026amp;_\"\n\n`+
|
||||
`↦ \u003ca href=\"http://example.org/\"\u003e[test title]\u003c/a\u003e","parse_mode":"HTML"}`,
|
||||
string(res))
|
||||
}
|
||||
|
||||
func TestTelegram_SendVerification(t *testing.T) {
|
||||
@@ -170,7 +178,7 @@ func TestTelegram_SendVerification(t *testing.T) {
|
||||
// test buildVerificationMessage separately for message text
|
||||
res, err := tb.buildVerificationMessage(req.User, req.Token, req.SiteID)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(res), `Confirmation for *test\\_username* on site remark`)
|
||||
assert.Contains(t, string(res), `Confirmation for \u003ci\u003etest_username\u003c/i\u003e on site remark`)
|
||||
assert.Contains(t, string(res), `secret_`)
|
||||
}
|
||||
|
||||
@@ -216,24 +224,3 @@ func mockTelegramServer() *httptest.Server {
|
||||
|
||||
return httptest.NewServer(router)
|
||||
}
|
||||
|
||||
func Test_escapeTitle(t *testing.T) {
|
||||
tbl := []struct {
|
||||
inp string
|
||||
out string
|
||||
}{
|
||||
{"", ""},
|
||||
{"something 123", "something 123"},
|
||||
{"something [123]", "something \\[123\\]"},
|
||||
{"something (123)", "something \\(123\\)"},
|
||||
{"something (123) [aaa]", "something \\(123\\) \\[aaa\\]"},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
assert.Equal(t, tt.out, escapeText(tt.inp))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user