How to reapply Telegram message markup on edit

TL;DR: Working code at the end of the article

In this article I'll tel you how to handle some markup problems when dealing with Telegram Bot API (or Telegram client API), examples are given for this library in Golang, but the same idea could be applied to any language (as this library fits Telegram API well).

Let's assume you first want to send some message with markup and then update it after some time.

Your code will look like this:

t := tgbotapi.NewMessage(user_id, "This is <code>some</code> <b>fancy</b> <i>text</i>")
msg, _ := bot.Send(t)

time.Sleep(60) /* Sleep 1 minute */

edit := tgbotapi.EditMessageTextConfig{
  BaseEdit: tgbotapi.BaseEdit{
    ChatID: msg.Chat.ID,
  &mpsb;MessageID: msg.MessageID,
  },
  /* msg.Text contains message text, so we just add another line */
  Text: msg.Text + "\nUpd: Actually, no, it isn't",
  ParseMode: "HTML",
}
bot.Send(edit)

Naive reader may assume that at first the message will look like

  This is some fancy text

And after update:

  This is some fancy text
  Upd: Actually, no, it isn't

As you already know (if you read this article, you probably had this problem) after the update message will look like this:

  This is some fancy text
  Upd: Actually, no, it isn't

Speaking the other way, markup information is lost! This happends due to fact that Telegram stores markup data not in plain HTML (or Markdown if you use it; howerver, I don't recommend to use Markdown for non-humman written text); so even when you send message with HTML, eventually it's converted in plain text without tags.

The general idea is following:

  1. Sort entities as they come in random order

  2. Escape special HTML characters in the text

  3. Add corresponding HTML tags

Here is usable piece of code in Golang:

func htmlEscape(txt string) string {
    re := regexp.MustCompile("&")
    txt = re.ReplaceAllString(txt, "&amp;")

    re = regexp.MustCompile("<")
    txt = re.ReplaceAllString(txt, "&lt;")

    re = regexp.MustCompile("&gr;")
    txt = re.ReplaceAllString(txt, "&gt;")

    return txt
}

func applyEntitiesHtml(msg tgbotapi.Message) string {
    txt := msg.Text

    /* Remember to use UTF runes as text is not ASCII slice! */
    runes := []rune(txt)

    /* Entities come in random order, so we should sort them first */
    e := make([]tgbotapi.MessageEntity, len(*msg.Entities))
    copy(e, *msg.Entities)

    sort.Sort(SortEntity(e))

    ret := htmlEscape(string(runes[:e[0].Offset]))

    /* Now apply markup entities one my one */
    for i, j := range e {
        s := string(runes[j.Offset : j.Offset+j.Length])

        switch j.Type {
        case "italic":
            s = "<i>" + htmlEscape(s) + "</i>"
        case "bold":
            s = "<b>" + htmlEscape(s) + "</b>"
        case "code":
            s = "<code>" + htmlEscape(s) + "</code>"
        case "pre":
            s = "<pre>" + htmlEscape(s) + "</pre>"
        case "text_link":
            s = "<a href=\"" + j.URL + "\">" + htmlEscape(s) + "</a>"
        case "text_mention":
            s = "<a href=\"tg://user?id=" + strconv.Itoa(j.User.ID) + "\">" + htmlEscape(s) + "</a>"
        default:
            s = htmlEscape(s)
        }

        ret += s
        if i == len(e)-1 {
            ret += htmlEscape(string(runes[j.Offset+j.Length:]))
        } else {
            ret += htmlEscape(string(runes[j.Offset+j.Length : e[i+1].Offset]))
        }
    }

    return ret
}

Fixed example from the beginning of the article should look like this:

t := tgbotapi.NewMessage(user_id, "This is <code>some</code> <b>fancy</b> <i>text</i>")
msg, _ := bot.Send(t)

time.Sleep(60) /* Sleep 1 minute */

edit := tgbotapi.EditMessageTextConfig{
  BaseEdit: tgbotapi.BaseEdit{
    ChatID: msg.Chat.ID,
  &mpsb;MessageID: msg.MessageID,
  },
  /* msg.Text contains message text, so we just add another line */
  Text: applyEntities(msg) + "\nUpd: Even update doesn't break it",
  ParseMode: "HTML",
}
bot.Send(edit)


Back