return updates, err
}
+// call sends one call of method with the JSON params and decodes its result
+// into result, when result is not nil. The call can take up to timeout. Its
+// request setup errors name the method. Network and answer errors give a safe
+// cause, so the caller can add the operation without exposing the request URL.
+func (b *TelegramAPI) call(ctx context.Context, timeout time.Duration, method string, params, result any) error {
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ body, err := json.Marshal(params)
+ if err != nil {
+ return fmt.Errorf("%s: encode request: %w", method, err)
+ }
+
+ request, err := http.NewRequestWithContext(ctx, http.MethodPost, b.base+"/bot"+b.token+"/"+method, bytes.NewReader(body))
+ if err != nil {
+ return fmt.Errorf("%s: make request: %s", method, b.redact(err))
+ }
+
+ request.Header.Set("Content-Type", "application/json")
+
+ return b.answer(request, timeout, result)
+}
+
+// redact returns the text of err without a URL and without the token. A
+// *url.Error holds the URL of the call, which holds the token, so only its
+// cause is kept.
+func (b *TelegramAPI) redact(err error) string {
+ var urlError *url.Error
+
+ if errors.As(err, &urlError) {
+ err = urlError.Err
+ }
+
+ return strings.ReplaceAll(err.Error(), b.token, "TOKEN")
+}
+
+// answer reads the bounded Bot API response without keeping its request URL.
+func (b *TelegramAPI) answer(request *http.Request, timeout time.Duration, result any) error {
+ response, err := b.http.Do(request)
+ if err != nil {
+ return b.networkError("no answer", err, timeout)
+ }
+
+ data, err := io.ReadAll(io.LimitReader(response.Body, telegramAnswerBytes))
+ err = errors.Join(err, response.Body.Close())
+ if err != nil {
+ return b.networkError("answer cut off", err, timeout)
+ }
+
+ var answer TelegramAnswer
+
+ err = json.Unmarshal(data, &answer)
+ if err != nil && response.StatusCode != http.StatusOK {
+ return &TelegramError{
+ Status: response.StatusCode,
+ Description: http.StatusText(response.StatusCode),
+ }
+ }
+
+ if err != nil {
+ return b.networkError("invalid JSON", err, timeout)
+ }
+
+ if !answer.OK {
+ return &TelegramError{
+ Status: response.StatusCode,
+ Description: b.redact(errors.New(answer.Description)),
+ RetryAfter: time.Duration(answer.Parameters.RetryAfter) * time.Second,
+ Refused: true,
+ }
+ }
+
+ if result == nil {
+ return nil
+ }
+
+ err = json.Unmarshal(answer.Result, result)
+ if err != nil {
+ return b.networkError("invalid result", err, timeout)
+ }
+
+ return nil
+}
+
+// networkError returns the TelegramNetworkError of err, which happened at the
+// step that what names. When the call used all of its time timeout, the cause
+// is "no answer in TIMEOUT", because the Go text of a deadline does not tell
+// how long the wait was.
+func (b *TelegramAPI) networkError(what string, err error, timeout time.Duration) error {
+ if errors.Is(err, context.DeadlineExceeded) {
+ return &TelegramNetworkError{
+ Cause: "no answer in " + timeout.String(),
+ }
+ }
+
+ return &TelegramNetworkError{
+ Cause: what + ": " + b.redact(err),
+ }
+}
+
// GetFile downloads bounded media through the authenticated file endpoint.
// Errors never include its token or URL, and the body is limited before
// storage.
}, nil)
}
-// call sends one call of method with the JSON params and decodes its result
-// into result, when result is not nil. The call can take up to timeout. Its
-// request setup errors name the method. Network and answer errors give a safe
-// cause, so the caller can add the operation without exposing the request URL.
-func (b *TelegramAPI) call(ctx context.Context, timeout time.Duration, method string, params, result any) error {
- ctx, cancel := context.WithTimeout(ctx, timeout)
- defer cancel()
-
- body, err := json.Marshal(params)
- if err != nil {
- return fmt.Errorf("%s: encode request: %w", method, err)
- }
-
- request, err := http.NewRequestWithContext(ctx, http.MethodPost, b.base+"/bot"+b.token+"/"+method, bytes.NewReader(body))
- if err != nil {
- return fmt.Errorf("%s: make request: %s", method, b.redact(err))
- }
-
- request.Header.Set("Content-Type", "application/json")
-
- return b.answer(request, timeout, result)
-}
-
-// answer reads the bounded Bot API response without keeping its request URL.
-func (b *TelegramAPI) answer(request *http.Request, timeout time.Duration, result any) error {
- response, err := b.http.Do(request)
- if err != nil {
- return b.networkError("no answer", err, timeout)
- }
-
- data, err := io.ReadAll(io.LimitReader(response.Body, telegramAnswerBytes))
- err = errors.Join(err, response.Body.Close())
- if err != nil {
- return b.networkError("answer cut off", err, timeout)
- }
-
- var answer TelegramAnswer
-
- err = json.Unmarshal(data, &answer)
- if err != nil && response.StatusCode != http.StatusOK {
- return &TelegramError{
- Status: response.StatusCode,
- Description: http.StatusText(response.StatusCode),
- }
- }
-
- if err != nil {
- return b.networkError("invalid JSON", err, timeout)
- }
-
- if !answer.OK {
- return &TelegramError{
- Status: response.StatusCode,
- Description: b.redact(errors.New(answer.Description)),
- RetryAfter: time.Duration(answer.Parameters.RetryAfter) * time.Second,
- Refused: true,
- }
- }
-
- if result == nil {
- return nil
- }
-
- err = json.Unmarshal(answer.Result, result)
- if err != nil {
- return b.networkError("invalid result", err, timeout)
- }
-
- return nil
-}
-
-// networkError returns the TelegramNetworkError of err, which happened at the
-// step that what names. When the call used all of its time timeout, the cause
-// is "no answer in TIMEOUT", because the Go text of a deadline does not tell
-// how long the wait was.
-func (b *TelegramAPI) networkError(what string, err error, timeout time.Duration) error {
- if errors.Is(err, context.DeadlineExceeded) {
- return &TelegramNetworkError{
- Cause: "no answer in " + timeout.String(),
- }
- }
-
- return &TelegramNetworkError{
- Cause: what + ": " + b.redact(err),
- }
-}
-
-// redact returns the text of err without a URL and without the token. A
-// *url.Error holds the URL of the call, which holds the token, so only its
-// cause is kept.
-func (b *TelegramAPI) redact(err error) string {
- var urlError *url.Error
-
- if errors.As(err, &urlError) {
- err = urlError.Err
- }
-
- return strings.ReplaceAll(err.Error(), b.token, "TOKEN")
-}
-
func (e *TelegramNetworkError) Error() string {
return e.Cause
}
return fmt.Sprintf("%d %s", e.Status, e.Description)
}
+// telegramRejected 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 telegramRejected(err error) bool {
+ var answer *TelegramError
+
+ return errors.As(err, &answer) && answer.Refused && (answer.Status == http.StatusBadRequest || answer.Status == http.StatusNotFound)
+}
+
// telegramRetryable tells if a later try of a failed Bot API call can work:
// after a network error, HTTP 429, or a server error. It returns the wait that
// the Bot API asks for, or 0.
return false, 0
}
-
-// telegramRejected 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 telegramRejected(err error) bool {
- var answer *TelegramError
-
- return errors.As(err, &answer) && answer.Refused && (answer.Status == http.StatusBadRequest || answer.Status == http.StatusNotFound)
-}