]> git.giacomolli.uk - onechat/blobdiff - botapi.go
Limit the time of each Telegram send call
[onechat] / botapi.go
index 9086fdc45f42b082e4f50fef2d5f74dd84606279..d02aff04b539507225a0ab858822abb98fd38adf 100644 (file)
--- a/botapi.go
+++ b/botapi.go
@@ -21,6 +21,13 @@ const (
        // take before the bot gives it up.
        pollSlack = 20 * time.Second
 
        // 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
 )
        // 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
 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 {
 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
 }
 
 // botError is an answer of the Bot API that tells of a failure. RetryAfter is
@@ -86,9 +95,10 @@ type botUser struct {
 // newBotAPI returns the client of the Bot API at base with token.
 func newBotAPI(base, token string) *botAPI {
        return &botAPI{
 // 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 +119,24 @@ func (b *botAPI) GetUpdates(ctx context.Context, offset int64) ([]botUpdate, err
        return updates, err
 }
 
        return updates, err
 }
 
-// SendMessage sends text to chat as plain text, with no parse mode.
+// 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 {
 func (b *botAPI) SendMessage(ctx context.Context, chat int64, text string) error {
+       ctx, cancel := context.WithTimeout(ctx, b.sendTimeout)
+       defer cancel()
+
        return b.call(ctx, "sendMessage", map[string]any{
                "chat_id": chat,
                "text":    text,
        }, nil)
 }
 
        return b.call(ctx, "sendMessage", map[string]any{
                "chat_id": chat,
                "text":    text,
        }, 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 {
 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",
        return b.call(ctx, "sendChatAction", map[string]any{
                "chat_id": chat,
                "action":  "typing",