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:
Dmitry Verkhoturov
2021-10-24 14:05:06 -05:00
committed by Umputun
parent f817fd38ed
commit 498073c509
2 changed files with 42 additions and 52 deletions
+30 -27
View File
@@ -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, "&", "&amp;")
text = strings.ReplaceAll(text, "<", "&lt;")
text = strings.ReplaceAll(text, ">", "&gt;")
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
+12 -25
View File
@@ -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))
})
}
}