X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/a345df4e4bbf9dbaf3da1ce548896eef8f515de3..b64d7548bd9f61921bdc650228642e77c73b0777:/botapi.go?ds=sidebyside diff --git a/botapi.go b/botapi.go index d02aff0..2da945e 100644 --- a/botapi.go +++ b/botapi.go @@ -21,11 +21,12 @@ const ( // 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. @@ -38,7 +39,7 @@ var errBotNetwork = errors.New("no valid answer from 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 @@ -47,12 +48,15 @@ type botAPI struct { } // 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. @@ -87,6 +91,17 @@ type botChat struct { 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"` @@ -119,15 +134,37 @@ func (b *botAPI) GetUpdates(ctx context.Context, offset int64) ([]botUpdate, err 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) } @@ -188,6 +225,7 @@ func (b *botAPI) call(ctx context.Context, method string, params, result any) er Status: response.StatusCode, Description: b.redact(errors.New(answer.Description)), RetryAfter: time.Duration(answer.Parameters.RetryAfter) * time.Second, + Refused: true, } } @@ -234,3 +272,12 @@ func retryable(err error) (bool, time.Duration) { 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) +}