X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/d206de3a2e839ffbda9cbdb10a3ed93c4915fce3..f80b058ae0550a56b4e62f8e65c2a74349b970e4:/botapi.go?ds=inline diff --git a/botapi.go b/botapi.go index 9086fdc..f48465c 100644 --- a/botapi.go +++ b/botapi.go @@ -21,6 +21,13 @@ 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 = 30 * time.Second + // botAnswerBytes is the most bytes of one answer of the Bot API. botAnswerBytes = 16 << 20 ) @@ -30,11 +37,13 @@ const ( 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. +// error of botAPI holds a URL, a net/http error, or the token. SendTimeout +// limits each sendMessage and sendChatAction call. type botAPI struct { - base string - token string - http *http.Client + base string + token string + http *http.Client + sendTimeout time.Duration } // botError is an answer of the Bot API that tells of a failure. RetryAfter is @@ -78,6 +87,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"` @@ -86,9 +106,10 @@ type botUser struct { // newBotAPI returns the client of the Bot API at base with token. func newBotAPI(base, token string) *botAPI { return &botAPI{ - base: base, - token: token, - http: &http.Client{}, + base: base, + token: token, + http: &http.Client{}, + sendTimeout: sendTimeout, } } @@ -109,16 +130,31 @@ 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. -func (b *botAPI) SendMessage(ctx context.Context, chat int64, text string) error { - return b.call(ctx, "sendMessage", map[string]any{ +// 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() + + params := map[string]any{ "chat_id": chat, "text": text, - }, nil) + } + + if len(entities) > 0 { + params["entities"] = entities + } + + return b.call(ctx, "sendMessage", params, nil) } -// SendTyping shows the typing action in chat for about five seconds. +// SendTyping shows the typing action in chat for about five seconds. A call +// that takes longer than sendTimeout fails with errBotNetwork. func (b *botAPI) SendTyping(ctx context.Context, chat int64) error { + ctx, cancel := context.WithTimeout(ctx, b.sendTimeout) + defer cancel() + return b.call(ctx, "sendChatAction", map[string]any{ "chat_id": chat, "action": "typing",