-// 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
-// errors tell the cause in plain words, with no method name, so that a log line
-// can say first what the call was for.
-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")
-}
-