(new): v1.2 release
Golang lint / lint (push) Successful in 11m32s

This commit is contained in:
2026-08-19 14:58:25 +03:00
parent f03a081ed6
commit 29b208eeec
79 changed files with 7301 additions and 2060 deletions
+61 -10
View File
@@ -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) {