+61
-10
@@ -13,6 +13,12 @@ import (
|
||||
"git.scuroneko.dev/scuroneko/sneklog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxRetries = 3
|
||||
maximumResponseSize = 10 << 20
|
||||
minimumRetryDelay = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// APIOpts holds configuration options for initializing the Telegram API client.
|
||||
// Use the provided setter methods to build options — do not construct directly.
|
||||
type APIOpts struct {
|
||||
@@ -26,6 +32,7 @@ type APIOpts struct {
|
||||
|
||||
limiter *utils.RateLimiter
|
||||
dropOverflowLimit bool
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
// NewAPIOpts creates a new APIOpts with default values.
|
||||
@@ -36,6 +43,7 @@ func NewAPIOpts(token string) *APIOpts {
|
||||
client: nil,
|
||||
useTestServer: false,
|
||||
apiURL: "https://api.telegram.org",
|
||||
maxRetries: defaultMaxRetries,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +99,16 @@ func (opts *APIOpts) SetDropRateLimitOverflow(b bool) *APIOpts {
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetMaxRetries sets the maximum number of retries after Telegram returns 429.
|
||||
// A non-positive value disables automatic retries. The default is 3.
|
||||
func (opts *APIOpts) SetMaxRetries(maxRetries int) *APIOpts {
|
||||
if maxRetries < 0 {
|
||||
maxRetries = 0
|
||||
}
|
||||
opts.maxRetries = maxRetries
|
||||
return opts
|
||||
}
|
||||
|
||||
// API is the main Telegram Bot API client for JSON requests.
|
||||
//
|
||||
// Use API methods when sending JSON payloads (for example with file_id, URL, or other
|
||||
@@ -107,9 +125,11 @@ type API struct {
|
||||
logFormat utils.LogFormat
|
||||
logFormatter *sneklog.Formatter
|
||||
|
||||
pool *workerPool
|
||||
pool *workerPool
|
||||
// Limiter is the optional rate limiter applied before requests are sent.
|
||||
Limiter *utils.RateLimiter
|
||||
dropOverflowLimit bool
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
// NewAPI creates a new API client from options.
|
||||
@@ -145,6 +165,7 @@ func NewAPI(opts *APIOpts) *API {
|
||||
pool: pool,
|
||||
Limiter: opts.limiter,
|
||||
dropOverflowLimit: opts.dropOverflowLimit,
|
||||
maxRetries: opts.maxRetries,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,18 +188,29 @@ func (api *API) GetLogger() *sneklog.Logger {
|
||||
|
||||
// ResponseParameters contains Telegram API response metadata (e.g., retry_after, migrate_to_chat_id).
|
||||
type ResponseParameters struct {
|
||||
// MigrateToChatID Optional. The group has been migrated to a supergroup with the specified identifier. This
|
||||
// number may have more than 32 significant bits and some programming languages may have difficulty/silent
|
||||
// defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or
|
||||
// double-precision float type are safe for storing this identifier.
|
||||
MigrateToChatID *int64 `json:"migrate_to_chat_id,omitempty"`
|
||||
RetryAfter *int `json:"retry_after,omitempty"`
|
||||
// RetryAfter Optional. In case of exceeding flood control, the number of seconds left to wait before the
|
||||
// request can be repeated
|
||||
RetryAfter *int `json:"retry_after,omitempty"`
|
||||
}
|
||||
|
||||
// TelegramResponse is the standard Telegram Bot API response structure.
|
||||
// Generic over Result type R.
|
||||
type TelegramResponse[R any] struct {
|
||||
Ok bool `json:"ok"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Result R `json:"result,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
Parameters *ResponseParameters `json:"parameters,omitempty"`
|
||||
// Ok reports whether the request succeeded.
|
||||
Ok bool `json:"ok"`
|
||||
// Description contains a human-readable result description when supplied by Telegram.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Result contains the method-specific result for a successful response.
|
||||
Result R `json:"result,omitempty"`
|
||||
// ErrorCode is the Telegram API error code for an unsuccessful response.
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
// Parameters contains additional recovery metadata for an unsuccessful response.
|
||||
Parameters *ResponseParameters `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
// TelegramRequest is a low-level Telegram API request wrapper.
|
||||
@@ -225,6 +257,7 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("Laniakea/%s", utils.VersionString))
|
||||
|
||||
retries := 0
|
||||
for {
|
||||
// Apply rate limiting before making the request
|
||||
if api.Limiter != nil {
|
||||
@@ -279,12 +312,16 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
|
||||
if r.method == "getUpdates" {
|
||||
return zero, responseErr
|
||||
}
|
||||
if retries >= api.maxRetries {
|
||||
return zero, fmt.Errorf("%w after %d retries: %w", ErrRetryLimit, retries, responseErr)
|
||||
}
|
||||
retries++
|
||||
|
||||
// Wait and retry
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return zero, ctx.Err()
|
||||
case <-time.After(time.Duration(after) * time.Second):
|
||||
case <-time.After(retryDelay(after)):
|
||||
continue // retry request
|
||||
}
|
||||
}
|
||||
@@ -330,8 +367,22 @@ func (r TelegramRequest[R, P]) Do(api *API) (R, error) {
|
||||
}
|
||||
|
||||
func readBody(body io.ReadCloser) ([]byte, error) {
|
||||
reader := io.LimitReader(body, 10<<20) // 10 MB
|
||||
return io.ReadAll(reader)
|
||||
data, err := io.ReadAll(io.LimitReader(body, maximumResponseSize+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) > maximumResponseSize {
|
||||
return nil, ErrResponseTooLarge
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func retryDelay(retryAfter int) time.Duration {
|
||||
delay := time.Duration(retryAfter) * time.Second
|
||||
if delay < minimumRetryDelay {
|
||||
return minimumRetryDelay
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
func parseBody[R any](data []byte) (TelegramResponse[R], error) {
|
||||
|
||||
Reference in New Issue
Block a user