// take before the bot gives it up.
pollSlack = 20 * time.Second
- // sendTimeout is how long a sendMessage or sendChatAction call can
- // take. A connection that stops without a close, such as after a
- // network change, then fails the call, and the sender tries again,
- // instead of a wait until TCP gives up. The client has no timeout of
- // its own, because the long poll of getUpdates needs a longer limit.
+ // sendTimeout is how long a sendMessage, sendRichMessage, or
+ // sendChatAction call can take. A connection that stops without a
+ // close, such as after a network change, then fails the call, and the
+ // sender tries again, instead of a wait until TCP gives up. The client
+ // has no timeout of its own, because the long poll of getUpdates needs
+ // a longer limit.
sendTimeout = 30 * time.Second
// botAnswerBytes is the most bytes of one answer of the Bot API.
// botAPI calls the Telegram Bot API. The bot token is a part of each URL, so no
// error of botAPI holds a URL, a net/http error, or the token. SendTimeout
-// limits each sendMessage and sendChatAction call.
+// limits each sendMessage, sendRichMessage, and sendChatAction call.
type botAPI struct {
base string
token string
}
// botError is an answer of the Bot API that tells of a failure. RetryAfter is
-// the wait that the Bot API asks for after HTTP 429.
+// the wait that the Bot API asks for after HTTP 429. Refused tells that the
+// answer is a Bot API answer with ok false, not a page of a proxy or of a
+// server error.
type botError struct {
Method string
Status int
Description string
RetryAfter time.Duration
+ Refused bool
}
// botAnswer is the envelope of each answer of the Bot API.
Type string `json:"type"`
}
+// botEntity is a message entity of sendMessage: a part of the text with a
+// format, such as bold or a link. Offset and Length count UTF-16 code units.
+// URL is the target of a text_link, and Language the language of a pre.
+type botEntity struct {
+ Type string `json:"type"`
+ Offset int `json:"offset"`
+ Length int `json:"length"`
+ URL string `json:"url,omitempty"`
+ Language string `json:"language,omitempty"`
+}
+
// botUser is the sender of a message.
type botUser struct {
ID int64 `json:"id"`
return updates, err
}
-// SendMessage sends text to chat as plain text, with no parse mode. A call that
-// takes longer than sendTimeout fails with errBotNetwork.
-func (b *botAPI) SendMessage(ctx context.Context, chat int64, text string) error {
+// SendMessage sends text to chat with the format of entities, with no parse
+// mode, so the text needs no escaping. With no entities, the text is plain. A
+// call that takes longer than sendTimeout fails with errBotNetwork.
+func (b *botAPI) SendMessage(ctx context.Context, chat int64, text string, entities []botEntity) error {
ctx, cancel := context.WithTimeout(ctx, b.sendTimeout)
defer cancel()
- return b.call(ctx, "sendMessage", map[string]any{
+ params := map[string]any{
"chat_id": chat,
"text": text,
+ }
+
+ if len(entities) > 0 {
+ params["entities"] = entities
+ }
+
+ return b.call(ctx, "sendMessage", params, nil)
+}
+
+// SendRichMessage sends markdown to chat as a rich message, which Telegram
+// renders itself, tables included. A call that takes longer than sendTimeout
+// fails with errBotNetwork.
+func (b *botAPI) SendRichMessage(ctx context.Context, chat int64, markdown string) error {
+ ctx, cancel := context.WithTimeout(ctx, b.sendTimeout)
+ defer cancel()
+
+ return b.call(ctx, "sendRichMessage", map[string]any{
+ "chat_id": chat,
+ "rich_message": map[string]any{
+ "markdown": markdown,
+ },
}, nil)
}
Status: response.StatusCode,
Description: b.redact(errors.New(answer.Description)),
RetryAfter: time.Duration(answer.Parameters.RetryAfter) * time.Second,
+ Refused: true,
}
}
return false, 0
}
+
+// rejected tells if the Bot API refused a call with ok false and HTTP 400 or
+// 404. Then the call did nothing: 400 refuses the request, and 404 tells that
+// the method is not there. An error page with no Bot API answer does not tell
+// this, so it is not a refusal.
+func rejected(err error) bool {
+ var answer *botError
+ return errors.As(err, &answer) && answer.Refused && (answer.Status == http.StatusBadRequest || answer.Status == http.StatusNotFound)
+}