Compare commits
1 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
6cf3355a36
|
13
tgapi/api.go
13
tgapi/api.go
@@ -102,10 +102,14 @@ type ApiResponse[R any] struct {
|
|||||||
type TelegramRequest[R, P any] struct {
|
type TelegramRequest[R, P any] struct {
|
||||||
method string
|
method string
|
||||||
params P
|
params P
|
||||||
|
chatId int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRequest[R, P any](method string, params P) TelegramRequest[R, P] {
|
func NewRequest[R, P any](method string, params P) TelegramRequest[R, P] {
|
||||||
return TelegramRequest[R, P]{method: method, params: params}
|
return TelegramRequest[R, P]{method, params, 0}
|
||||||
|
}
|
||||||
|
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) {
|
func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, error) {
|
||||||
var zero R
|
var zero R
|
||||||
@@ -165,7 +169,12 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
|
|||||||
if responseData.Parameters.RetryAfter != nil {
|
if responseData.Parameters.RetryAfter != nil {
|
||||||
after = *responseData.Parameters.RetryAfter
|
after = *responseData.Parameters.RetryAfter
|
||||||
}
|
}
|
||||||
api.Limiter.SetGlobalLock(after)
|
if r.chatId > 0 {
|
||||||
|
api.Limiter.SetChatLock(r.chatId, after)
|
||||||
|
} else {
|
||||||
|
api.Limiter.SetGlobalLock(after)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(after) * time.Second)
|
||||||
return r.doRequest(ctx, api)
|
return r.doRequest(ctx, api)
|
||||||
}
|
}
|
||||||
return zero, ErrRateLimit
|
return zero, ErrRateLimit
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type SendMessageP struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) SendMessage(params SendMessageP) (Message, error) {
|
func (api *API) SendMessage(params SendMessageP) (Message, error) {
|
||||||
req := NewRequest[Message, SendMessageP]("sendMessage", params)
|
req := NewRequestWithChatID[Message, SendMessageP]("sendMessage", params, params.ChatID)
|
||||||
return req.Do(api)
|
return req.Do(api)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ type SendMessageDraftP struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) SendMessageDraft(params SendMessageDraftP) (bool, error) {
|
func (api *API) SendMessageDraft(params SendMessageDraftP) (bool, error) {
|
||||||
req := NewRequest[bool]("sendMessageDraft", params)
|
req := NewRequestWithChatID[bool]("sendMessageDraft", params, params.ChatID)
|
||||||
return req.Do(api)
|
return req.Do(api)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
164
utils/limiter.go
164
utils/limiter.go
@@ -20,9 +20,9 @@ type RateLimiter struct {
|
|||||||
|
|
||||||
func NewRateLimiter() *RateLimiter {
|
func NewRateLimiter() *RateLimiter {
|
||||||
return &RateLimiter{
|
return &RateLimiter{
|
||||||
// 30 запросов в секунду (burst=30)
|
globalLimiter: rate.NewLimiter(30, 30),
|
||||||
globalLimiter: rate.NewLimiter(rate.Limit(30), 30),
|
|
||||||
chatLimiters: make(map[int64]*rate.Limiter),
|
chatLimiters: make(map[int64]*rate.Limiter),
|
||||||
|
chatLocks: make(map[int64]time.Time), // инициализация!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,96 +34,122 @@ func (rl *RateLimiter) SetGlobalLock(retryAfter int) {
|
|||||||
defer rl.globalMu.Unlock()
|
defer rl.globalMu.Unlock()
|
||||||
rl.globalLockUntil = time.Now().Add(time.Duration(retryAfter) * time.Second)
|
rl.globalLockUntil = time.Now().Add(time.Duration(retryAfter) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) SetChatLock(chatID int64, retryAfter int) {
|
func (rl *RateLimiter) SetChatLock(chatID int64, retryAfter int) {
|
||||||
|
if retryAfter <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
rl.chatMu.Lock()
|
rl.chatMu.Lock()
|
||||||
defer rl.chatMu.Unlock()
|
defer rl.chatMu.Unlock()
|
||||||
rl.chatLocks[chatID] = time.Now().Add(time.Duration(retryAfter) * time.Second)
|
rl.chatLocks[chatID] = time.Now().Add(time.Duration(retryAfter) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GlobalWait блокирует до возможности сделать глобальный запрос.
|
||||||
func (rl *RateLimiter) GlobalWait(ctx context.Context) error {
|
func (rl *RateLimiter) GlobalWait(ctx context.Context) error {
|
||||||
rl.globalMu.RLock()
|
// Ждём окончания глобальной блокировки, если она есть
|
||||||
until := rl.globalLockUntil
|
if err := rl.waitForGlobalUnlock(ctx); err != nil {
|
||||||
rl.globalMu.RUnlock()
|
|
||||||
|
|
||||||
if !until.IsZero() {
|
|
||||||
if time.Now().Before(until) {
|
|
||||||
// Ждём до окончания блокировки или отмены контекста
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Until(until)):
|
|
||||||
// блокировка снята
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Теперь ждём разрешения rate limiter'а
|
|
||||||
return rl.globalLimiter.Wait(ctx)
|
|
||||||
}
|
|
||||||
func (rl *RateLimiter) Wait(ctx context.Context, chatID int64) error {
|
|
||||||
rl.chatMu.Lock()
|
|
||||||
until, ok := rl.chatLocks[chatID]
|
|
||||||
rl.chatMu.Unlock()
|
|
||||||
if ok && !until.IsZero() {
|
|
||||||
if time.Now().Before(until) {
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Until(until)):
|
|
||||||
// блокировка снята
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := rl.GlobalWait(ctx); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rl.chatMu.Lock()
|
// Ждём разрешения rate limiter'а
|
||||||
chatLimiter, ok := rl.chatLimiters[chatID]
|
return rl.globalLimiter.Wait(ctx)
|
||||||
if !ok {
|
|
||||||
chatLimiter = rate.NewLimiter(rate.Limit(1), 1)
|
|
||||||
rl.chatLimiters[chatID] = chatLimiter
|
|
||||||
}
|
|
||||||
rl.chatMu.Unlock()
|
|
||||||
return chatLimiter.Wait(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait блокирует до возможности сделать запрос для конкретного чата.
|
||||||
|
func (rl *RateLimiter) Wait(ctx context.Context, chatID int64) error {
|
||||||
|
// Ждём окончания блокировки чата
|
||||||
|
if err := rl.waitForChatUnlock(ctx, chatID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Затем глобальной блокировки
|
||||||
|
if err := rl.waitForGlobalUnlock(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Получаем или создаём лимитер для чата
|
||||||
|
limiter := rl.getChatLimiter(chatID)
|
||||||
|
return limiter.Wait(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalAllow неблокирующая проверка глобального запроса.
|
||||||
func (rl *RateLimiter) GlobalAllow() bool {
|
func (rl *RateLimiter) GlobalAllow() bool {
|
||||||
rl.globalMu.RLock()
|
rl.globalMu.RLock()
|
||||||
until := rl.globalLockUntil
|
until := rl.globalLockUntil
|
||||||
rl.globalMu.RUnlock()
|
rl.globalMu.RUnlock()
|
||||||
|
|
||||||
if !until.IsZero() {
|
if !until.IsZero() && time.Now().Before(until) {
|
||||||
if time.Now().Before(until) {
|
return false
|
||||||
// Ждём до окончания блокировки или отмены контекста
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Until(until)):
|
|
||||||
rl.globalLimiter.Allow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return rl.globalLimiter.Allow()
|
return rl.globalLimiter.Allow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow неблокирующая проверка запроса для чата.
|
||||||
func (rl *RateLimiter) Allow(chatID int64) bool {
|
func (rl *RateLimiter) Allow(chatID int64) bool {
|
||||||
rl.chatMu.Lock()
|
// Проверяем глобальную блокировку
|
||||||
until, ok := rl.chatLocks[chatID]
|
rl.globalMu.RLock()
|
||||||
rl.chatMu.Unlock()
|
globalUntil := rl.globalLockUntil
|
||||||
if ok && !until.IsZero() {
|
rl.globalMu.RUnlock()
|
||||||
if time.Now().Before(until) {
|
if !globalUntil.IsZero() && time.Now().Before(globalUntil) {
|
||||||
select {
|
return false
|
||||||
case <-time.After(time.Until(until)):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем блокировку чата
|
||||||
|
rl.chatMu.Lock()
|
||||||
|
chatUntil, ok := rl.chatLocks[chatID]
|
||||||
|
rl.chatMu.Unlock()
|
||||||
|
if ok && !chatUntil.IsZero() && time.Now().Before(chatUntil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем глобальный лимитер
|
||||||
if !rl.globalLimiter.Allow() {
|
if !rl.globalLimiter.Allow() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
rl.chatMu.Lock()
|
|
||||||
chatLimiter, ok := rl.chatLimiters[chatID]
|
// Проверяем лимитер чата
|
||||||
if !ok {
|
limiter := rl.getChatLimiter(chatID)
|
||||||
chatLimiter = rate.NewLimiter(rate.Limit(1), 1)
|
return limiter.Allow()
|
||||||
rl.chatLimiters[chatID] = chatLimiter
|
}
|
||||||
}
|
|
||||||
rl.chatMu.Unlock()
|
// Внутренние вспомогательные методы
|
||||||
return chatLimiter.Allow()
|
|
||||||
|
func (rl *RateLimiter) waitForGlobalUnlock(ctx context.Context) error {
|
||||||
|
rl.globalMu.RLock()
|
||||||
|
until := rl.globalLockUntil
|
||||||
|
rl.globalMu.RUnlock()
|
||||||
|
|
||||||
|
if until.IsZero() || time.Now().After(until) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Until(until)):
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) waitForChatUnlock(ctx context.Context, chatID int64) error {
|
||||||
|
rl.chatMu.Lock()
|
||||||
|
until, ok := rl.chatLocks[chatID]
|
||||||
|
rl.chatMu.Unlock()
|
||||||
|
|
||||||
|
if !ok || until.IsZero() || time.Now().After(until) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Until(until)):
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) getChatLimiter(chatID int64) *rate.Limiter {
|
||||||
|
rl.chatMu.Lock()
|
||||||
|
defer rl.chatMu.Unlock()
|
||||||
|
if lim, ok := rl.chatLimiters[chatID]; ok {
|
||||||
|
return lim
|
||||||
|
}
|
||||||
|
lim := rate.NewLimiter(1, 1) // 1 запрос/сек
|
||||||
|
rl.chatLimiters[chatID] = lim
|
||||||
|
return lim
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
const (
|
const (
|
||||||
VersionString = "1.0.0-beta.7"
|
VersionString = "1.0.0-beta.8"
|
||||||
VersionMajor = 1
|
VersionMajor = 1
|
||||||
VersionMinor = 0
|
VersionMinor = 0
|
||||||
VersionPatch = 0
|
VersionPatch = 0
|
||||||
Beta = 7
|
Beta = 8
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user