(refactor): API initialism names (tests): logger format coverage (doc): updated public names
This commit is contained in:
+47
-27
@@ -19,7 +19,10 @@ type APIOpts struct {
|
||||
token string
|
||||
client *http.Client
|
||||
useTestServer bool
|
||||
apiUrl string
|
||||
apiURL string
|
||||
|
||||
logFormat utils.LogFormat
|
||||
logFormatter *sneklog.Formatter
|
||||
|
||||
limiter *utils.RateLimiter
|
||||
dropOverflowLimit bool
|
||||
@@ -32,7 +35,7 @@ func NewAPIOpts(token string) *APIOpts {
|
||||
token: token,
|
||||
client: nil,
|
||||
useTestServer: false,
|
||||
apiUrl: "https://api.telegram.org",
|
||||
apiURL: "https://api.telegram.org",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,15 +55,24 @@ func (opts *APIOpts) UseTestServer(use bool) *APIOpts {
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetAPIUrl overrides the default Telegram API URL.
|
||||
// SetAPIURL overrides the default Telegram API URL.
|
||||
// Useful for self-hosted bots or proxies.
|
||||
func (opts *APIOpts) SetAPIUrl(apiUrl string) *APIOpts {
|
||||
if apiUrl != "" {
|
||||
opts.apiUrl = apiUrl
|
||||
func (opts *APIOpts) SetAPIURL(apiURL string) *APIOpts {
|
||||
if apiURL != "" {
|
||||
opts.apiURL = apiURL
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func (opts *APIOpts) SetLogFormat(format utils.LogFormat) *APIOpts {
|
||||
opts.logFormat = format
|
||||
return opts
|
||||
}
|
||||
func (opts *APIOpts) SetLogFormatter(formatter *sneklog.Formatter) *APIOpts {
|
||||
opts.logFormatter = formatter
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetLimiter sets a rate limiter to enforce Telegram's API limits.
|
||||
// Recommended: use utils.NewRateLimiter() for correct per-chat and global throttling.
|
||||
func (opts *APIOpts) SetLimiter(limiter *utils.RateLimiter) *APIOpts {
|
||||
@@ -87,7 +99,10 @@ type API struct {
|
||||
client *http.Client
|
||||
logger *sneklog.Logger
|
||||
useTestServer bool
|
||||
apiUrl string
|
||||
apiURL string
|
||||
|
||||
logFormat utils.LogFormat
|
||||
logFormatter *sneklog.Formatter
|
||||
|
||||
pool *workerPool
|
||||
Limiter *utils.RateLimiter
|
||||
@@ -97,12 +112,13 @@ type API struct {
|
||||
// NewAPI creates a new API client from options.
|
||||
// Always call Close() when done to release resources.
|
||||
func NewAPI(opts *APIOpts) *API {
|
||||
l := utils.CreateLogger("API", utils.GetLoggerLevel())
|
||||
if opts == nil {
|
||||
l.Errorln("Set API options")
|
||||
_ = l.Close()
|
||||
return nil
|
||||
}
|
||||
logger := utils.CreateLogger(
|
||||
"API", utils.GetLoggerLevel(),
|
||||
opts.logFormat, opts.logFormatter,
|
||||
)
|
||||
|
||||
client := opts.client
|
||||
if client == nil {
|
||||
@@ -113,11 +129,15 @@ func NewAPI(opts *APIOpts) *API {
|
||||
pool.start()
|
||||
|
||||
return &API{
|
||||
token: opts.token,
|
||||
client: client,
|
||||
logger: l,
|
||||
useTestServer: opts.useTestServer,
|
||||
apiUrl: opts.apiUrl,
|
||||
token: opts.token,
|
||||
client: client,
|
||||
logger: logger,
|
||||
useTestServer: opts.useTestServer,
|
||||
apiURL: opts.apiURL,
|
||||
|
||||
logFormat: opts.logFormat,
|
||||
logFormatter: opts.logFormatter,
|
||||
|
||||
pool: pool,
|
||||
Limiter: opts.limiter,
|
||||
dropOverflowLimit: opts.dropOverflowLimit,
|
||||
@@ -147,9 +167,9 @@ type ResponseParameters struct {
|
||||
RetryAfter *int `json:"retry_after,omitempty"`
|
||||
}
|
||||
|
||||
// ApiResponse is the standard Telegram Bot API response structure.
|
||||
// TelegramResponse is the standard Telegram Bot API response structure.
|
||||
// Generic over Result type R.
|
||||
type ApiResponse[R any] struct {
|
||||
type TelegramResponse[R any] struct {
|
||||
Ok bool `json:"ok"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Result R `json:"result,omitempty"`
|
||||
@@ -166,7 +186,7 @@ type ApiResponse[R any] struct {
|
||||
type TelegramRequest[R, P any] struct {
|
||||
method string
|
||||
params P
|
||||
chatId int64
|
||||
chatID int64
|
||||
}
|
||||
|
||||
// NewRequest creates a low-level TelegramRequest with no associated chat ID.
|
||||
@@ -176,8 +196,8 @@ func NewRequest[R, P any](method string, params P) TelegramRequest[R, P] {
|
||||
|
||||
// NewRequestWithChatID creates a low-level TelegramRequest with an associated chat ID.
|
||||
// The chat ID is used for per-chat rate limiting.
|
||||
func NewRequestWithChatID[R, P any](method string, params P, chatId int64) TelegramRequest[R, P] {
|
||||
return TelegramRequest[R, P]{method, params, chatId}
|
||||
func NewRequestWithChatID[R, P any](method string, params P, chatID int64) TelegramRequest[R, P] {
|
||||
return TelegramRequest[R, P]{method, params, chatID}
|
||||
}
|
||||
|
||||
func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, error) {
|
||||
@@ -191,7 +211,7 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
|
||||
if api.useTestServer {
|
||||
methodPrefix = "/test"
|
||||
}
|
||||
url := fmt.Sprintf("%s/bot%s%s/%s", api.apiUrl, api.token, methodPrefix, r.method)
|
||||
url := fmt.Sprintf("%s/bot%s%s/%s", api.apiURL, api.token, methodPrefix, r.method)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("failed to create request: %w", err)
|
||||
@@ -204,7 +224,7 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
|
||||
for {
|
||||
// Apply rate limiting before making the request
|
||||
if api.Limiter != nil {
|
||||
if err := api.Limiter.Check(ctx, api.dropOverflowLimit, r.chatId); err != nil {
|
||||
if err := api.Limiter.Check(ctx, api.dropOverflowLimit, r.chatID); err != nil {
|
||||
return zero, err
|
||||
}
|
||||
}
|
||||
@@ -235,12 +255,12 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
|
||||
// Handle rate limiting (429)
|
||||
if response.ErrorCode == 429 && response.Parameters != nil && response.Parameters.RetryAfter != nil {
|
||||
after := *response.Parameters.RetryAfter
|
||||
api.logger.Warnf("Rate limited by Telegram, retry after %d seconds (chat: %d)", after, r.chatId)
|
||||
api.logger.Warnf("Rate limited by Telegram, retry after %d seconds (chat: %d)", after, r.chatID)
|
||||
|
||||
// Apply cooldown to global or chat-specific limiter
|
||||
if api.Limiter != nil {
|
||||
if r.chatId > 0 {
|
||||
api.Limiter.SetChatLock(r.chatId, after)
|
||||
if r.chatID > 0 {
|
||||
api.Limiter.SetChatLock(r.chatID, after)
|
||||
} else {
|
||||
api.Limiter.SetGlobalLock(after)
|
||||
}
|
||||
@@ -302,8 +322,8 @@ func readBody(body io.ReadCloser) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Internal helper that parses a typed Telegram API response body.
|
||||
func parseBody[R any](data []byte) (ApiResponse[R], error) {
|
||||
var resp ApiResponse[R]
|
||||
func parseBody[R any](data []byte) (TelegramResponse[R], error) {
|
||||
var resp TelegramResponse[R]
|
||||
err := json.Unmarshal(data, &resp)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ func TestAPILeavesAcceptEncodingToHTTPTransport(t *testing.T) {
|
||||
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIUrl("https://example.test").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(client),
|
||||
)
|
||||
defer func() {
|
||||
@@ -77,7 +77,7 @@ func TestAPICloseClosesIdleConnections(t *testing.T) {
|
||||
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIUrl("https://example.test").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(&http.Client{Transport: transport}),
|
||||
)
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@ package tgapi
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrRateLimit reports that a request exceeded the configured rate limiter.
|
||||
var ErrRateLimit = errors.New("rate limit exceeded")
|
||||
|
||||
// ErrPoolUnexpected reports an unexpected result type returned from the worker pool.
|
||||
var ErrPoolUnexpected = errors.New("unexpected response from pool")
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ func (api *API) SendChatActionWithContext(ctx context.Context, params SendChatAc
|
||||
// See https://core.telegram.org/bots/api#setmessagereaction
|
||||
type SetMessageReaction struct {
|
||||
ChatID int64 `json:"chat_id"`
|
||||
MessageId int `json:"message_id"`
|
||||
MessageID int `json:"message_id"`
|
||||
Reaction []ReactionType `json:"reaction"`
|
||||
IsBig bool `json:"is_big,omitempty"`
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ type Message struct {
|
||||
SenderBusinessBot *User `json:"sender_business_bot,omitempty"`
|
||||
SenderTag string `json:"sender_tag,omitempty"`
|
||||
Date int `json:"date"`
|
||||
BusinessConnectionId string `json:"business_connection_id,omitempty"`
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
Chat *Chat `json:"chat,omitempty"`
|
||||
ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"`
|
||||
|
||||
@@ -121,7 +121,7 @@ type Message struct {
|
||||
HasProtectedContent bool `json:"has_protected_content,omitempty"`
|
||||
IsFromOffline bool `json:"is_from_offline,omitempty"`
|
||||
IsPaidPost bool `json:"is_paid_post,omitempty"`
|
||||
MediaGroupId string `json:"media_group_id,omitempty"`
|
||||
MediaGroupID string `json:"media_group_id,omitempty"`
|
||||
AuthorSignature string `json:"author_signature,omitempty"`
|
||||
PaidStarCount int `json:"paid_star_count,omitempty"`
|
||||
|
||||
@@ -316,8 +316,8 @@ const (
|
||||
MessageEntityCashtag MessageEntityType = "cashtag"
|
||||
// MessageEntityBotCommand identifies a bot command entity.
|
||||
MessageEntityBotCommand MessageEntityType = "bot_command"
|
||||
// MessageEntityUrl identifies a URL entity.
|
||||
MessageEntityUrl MessageEntityType = "url"
|
||||
// MessageEntityURL identifies a URL entity.
|
||||
MessageEntityURL MessageEntityType = "url"
|
||||
// MessageEntityEmail identifies an email entity.
|
||||
MessageEntityEmail MessageEntityType = "email"
|
||||
// MessageEntityPhoneNumber identifies a phone number entity.
|
||||
@@ -537,7 +537,7 @@ const (
|
||||
// ChatActionUploadVideoNote tells Telegram the bot is uploading a video note.
|
||||
ChatActionUploadVideoNote ChatActionType = "upload_video_note"
|
||||
// ChatActionUploadVideoNone is a deprecated alias for ChatActionUploadVideoNote.
|
||||
ChatActionUploadVideoNone ChatActionType = ChatActionUploadVideoNote
|
||||
ChatActionUploadVideoNone = ChatActionUploadVideoNote
|
||||
)
|
||||
|
||||
// MessageReactionUpdated represents a change of a reaction on a message.
|
||||
|
||||
+1
-1
@@ -256,7 +256,7 @@ func (api *API) openFileByLink(ctx context.Context, link string) (io.ReadCloser,
|
||||
if api.useTestServer {
|
||||
methodPrefix = "/test"
|
||||
}
|
||||
u := fmt.Sprintf("%s/file/bot%s%s/%s", api.apiUrl, api.token, methodPrefix, link)
|
||||
u := fmt.Sprintf("%s/file/bot%s%s/%s", api.apiURL, api.token, methodPrefix, link)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestGetFileByLinkUsesConfiguredAPIURL(t *testing.T) {
|
||||
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIUrl("https://example.test").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(client),
|
||||
)
|
||||
defer func() {
|
||||
@@ -47,7 +47,7 @@ func TestGetFileByLinkUsesConfiguredAPIURL(t *testing.T) {
|
||||
func TestOpenFileByLinkStreamsResponseBody(t *testing.T) {
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIUrl("https://example.test").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(&http.Client{
|
||||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
@@ -94,7 +94,7 @@ func TestGetFileByLinkReturnsHTTPStatusError(t *testing.T) {
|
||||
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIUrl("https://example.test").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(client),
|
||||
)
|
||||
defer func() {
|
||||
@@ -131,7 +131,7 @@ func TestGetUpdatesOmitsAllowedUpdatesWhenEmpty(t *testing.T) {
|
||||
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIUrl("https://example.test").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(client),
|
||||
)
|
||||
defer func() {
|
||||
@@ -174,7 +174,7 @@ func TestSetChatMenuButtonSendsStructuredMenuButton(t *testing.T) {
|
||||
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIUrl("https://example.test").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(client),
|
||||
)
|
||||
defer func() {
|
||||
|
||||
+13
-12
@@ -70,12 +70,13 @@ type Uploader struct {
|
||||
|
||||
// NewUploader creates a multipart uploader bound to an API client.
|
||||
func NewUploader(api *API) *Uploader {
|
||||
logger := utils.CreateLogger("UPLOADER", utils.GetLoggerLevel())
|
||||
if api == nil {
|
||||
logger.Errorln("api is nil")
|
||||
_ = logger.Close()
|
||||
return nil
|
||||
}
|
||||
logger := utils.CreateLogger(
|
||||
"UPLOADER", utils.GetLoggerLevel(),
|
||||
api.logFormat, api.logFormatter,
|
||||
)
|
||||
return &Uploader{api, logger}
|
||||
}
|
||||
|
||||
@@ -97,18 +98,18 @@ type UploaderRequest[R, P any] struct {
|
||||
method string
|
||||
files []UploaderFile
|
||||
params P
|
||||
chatId int64
|
||||
chatID int64
|
||||
}
|
||||
|
||||
// NewUploaderRequest creates a low-level multipart upload request with no associated chat ID.
|
||||
func NewUploaderRequest[R, P any](method string, params P, files ...UploaderFile) UploaderRequest[R, P] {
|
||||
return UploaderRequest[R, P]{method: method, files: files, params: params, chatId: 0}
|
||||
return UploaderRequest[R, P]{method: method, files: files, params: params, chatID: 0}
|
||||
}
|
||||
|
||||
// NewUploaderRequestWithChatID creates a low-level multipart upload request with an associated chat ID.
|
||||
// The chat ID is used for per-chat rate limiting.
|
||||
func NewUploaderRequestWithChatID[R, P any](method string, params P, chatId int64, files ...UploaderFile) UploaderRequest[R, P] {
|
||||
return UploaderRequest[R, P]{method: method, files: files, params: params, chatId: chatId}
|
||||
func NewUploaderRequestWithChatID[R, P any](method string, params P, chatID int64, files ...UploaderFile) UploaderRequest[R, P] {
|
||||
return UploaderRequest[R, P]{method: method, files: files, params: params, chatID: chatID}
|
||||
}
|
||||
|
||||
func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R, error) {
|
||||
@@ -118,11 +119,11 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
|
||||
if up.api.useTestServer {
|
||||
methodPrefix = "/test"
|
||||
}
|
||||
url := fmt.Sprintf("%s/bot%s%s/%s", up.api.apiUrl, up.api.token, methodPrefix, r.method)
|
||||
url := fmt.Sprintf("%s/bot%s%s/%s", up.api.apiURL, up.api.token, methodPrefix, r.method)
|
||||
|
||||
for {
|
||||
if up.api.Limiter != nil {
|
||||
if err := up.api.Limiter.Check(ctx, up.api.dropOverflowLimit, r.chatId); err != nil {
|
||||
if err := up.api.Limiter.Check(ctx, up.api.dropOverflowLimit, r.chatID); err != nil {
|
||||
return zero, err
|
||||
}
|
||||
}
|
||||
@@ -161,10 +162,10 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
|
||||
if !response.Ok {
|
||||
if response.ErrorCode == 429 && response.Parameters != nil && response.Parameters.RetryAfter != nil {
|
||||
after := *response.Parameters.RetryAfter
|
||||
up.logger.Warnf("Rate limited, retry after %d seconds (chat: %d)", after, r.chatId)
|
||||
up.logger.Warnf("Rate limited, retry after %d seconds (chat: %d)", after, r.chatID)
|
||||
if up.api.Limiter != nil {
|
||||
if r.chatId > 0 {
|
||||
up.api.Limiter.SetChatLock(r.chatId, after)
|
||||
if r.chatID > 0 {
|
||||
up.api.Limiter.SetChatLock(r.chatID, after)
|
||||
} else {
|
||||
up.api.Limiter.SetGlobalLock(after)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestUploaderEncodesJSONFieldsAndLeavesAcceptEncodingToHTTPTransport(t *test
|
||||
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIUrl("https://example.test").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(client),
|
||||
)
|
||||
defer func() {
|
||||
|
||||
Reference in New Issue
Block a user