From a6bd611db78a07d5ef3869fa3e3ad99f106d92e6 Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Mon, 25 Sep 2023 21:58:16 +0200 Subject: [PATCH 01/10] add rate limiter with bucket --- discordwebhook.go | 70 ++++++-- examples/basic_example/basic_example.go | 4 +- examples/embedded_image/embedded_image.go | 3 +- ratelimit.go | 197 ++++++++++++++++++++++ 4 files changed, 255 insertions(+), 19 deletions(-) create mode 100644 ratelimit.go diff --git a/discordwebhook.go b/discordwebhook.go index 2c7fed6..4f69d1a 100644 --- a/discordwebhook.go +++ b/discordwebhook.go @@ -3,34 +3,72 @@ package discordwebhook import ( "bytes" "encoding/json" + "errors" "fmt" "io/ioutil" + "log" + "math" "net/http" + "strconv" + "time" ) -func SendMessage(url string, message Message) error { - payload := new(bytes.Buffer) - - err := json.NewEncoder(payload).Encode(message) - if err != nil { - return err +func SendMessage(url string, message Message, r1 *RateLimiter) error { + // Validate parameters + if url == "" { + return errors.New("empty URL") } - resp, err := http.Post(url, "application/json", payload) - if err != nil { - return err - } + for { + bucket := r1.LockBucket(url) + payload := new(bytes.Buffer) + + err := json.NewEncoder(payload).Encode(message) + if err != nil { + return err + } - if resp.StatusCode != 200 && resp.StatusCode != 204 { - defer resp.Body.Close() + // Make the HTTP request + resp, err := http.Post(url, "application/json", payload) - responseBody, err := ioutil.ReadAll(resp.Body) if err != nil { return err } + if err != nil { + log.Printf("HTTP request failed: %v", err) + return err + } - return fmt.Errorf(string(responseBody)) - } + switch resp.StatusCode { + case http.StatusOK, http.StatusNoContent: + // Success + resp.Body.Close() + err = bucket.Release(resp.Header) + return nil + case http.StatusTooManyRequests: + // Rate limit exceeded, retry after backoff duration + resetAfter := resp.Header.Get("X-RateLimit-Reset-After") + parsedAfter, err := strconv.ParseFloat(resetAfter, 64) + if err != nil { + return err + } + + whole, frac := math.Modf(parsedAfter) + resetAt := time.Now().Add(time.Duration(whole) * time.Second).Add(time.Duration(frac*1000) * time.Millisecond).Add(250 * time.Millisecond) + + time.Sleep(time.Until(resetAt)) - return nil + err = bucket.Release(resp.Header) + default: + // Handle other HTTP status codes + defer resp.Body.Close() + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + err = bucket.Release(resp.Header) + return fmt.Errorf("HTTP request failed with status %d, body: \n %s", resp.StatusCode, responseBody) + } + } } diff --git a/examples/basic_example/basic_example.go b/examples/basic_example/basic_example.go index 599c016..f26505c 100644 --- a/examples/basic_example/basic_example.go +++ b/examples/basic_example/basic_example.go @@ -10,15 +10,15 @@ func main() { var username = "BotUser" var content = "This is a test message" var url = "https://discord.com/api/webhooks/..." + r1 := discordwebhook.NewRatelimiter() message := discordwebhook.Message{ Username: &username, Content: &content, } - err := discordwebhook.SendMessage(url, message) + err := discordwebhook.SendMessage(url, message, r1) if err != nil { log.Fatal(err) } } - diff --git a/examples/embedded_image/embedded_image.go b/examples/embedded_image/embedded_image.go index 54c4764..eda1ee1 100644 --- a/examples/embedded_image/embedded_image.go +++ b/examples/embedded_image/embedded_image.go @@ -11,6 +11,7 @@ func main() { var content = "This is a test message" var url = "https://discord.com/api/webhooks/..." var image_url = "https://i.imgur.com/..." + r1 := discordwebhook.NewRatelimiter() image := discordwebhook.Image{ Url: &image_url, @@ -26,7 +27,7 @@ func main() { Embeds: &[]discordwebhook.Embed{embed}, } - err := discordwebhook.SendMessage(url, message) + err := discordwebhook.SendMessage(url, message, r1) if err != nil { log.Fatal(err) } diff --git a/ratelimit.go b/ratelimit.go new file mode 100644 index 0000000..0773821 --- /dev/null +++ b/ratelimit.go @@ -0,0 +1,197 @@ +package discordwebhook + +import ( + "math" + "net/http" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" +) + +// customRateLimit holds information for defining a custom rate limit +type customRateLimit struct { + suffix string + requests int + reset time.Duration +} + +// RateLimiter holds all ratelimit buckets +type RateLimiter struct { + sync.Mutex + global *int64 + buckets map[string]*Bucket + globalRateLimit time.Duration + customRateLimits []*customRateLimit +} + +// NewRatelimiter returns a new RateLimiter +func NewRatelimiter() *RateLimiter { + + return &RateLimiter{ + buckets: make(map[string]*Bucket), + global: new(int64), + customRateLimits: []*customRateLimit{ + { + suffix: "//reactions//", + requests: 1, + reset: 200 * time.Millisecond, + }, + }, + } +} + +// GetBucket retrieves or creates a bucket +func (r *RateLimiter) GetBucket(key string) *Bucket { + r.Lock() + defer r.Unlock() + + if bucket, ok := r.buckets[key]; ok { + return bucket + } + + b := &Bucket{ + Remaining: 1, + Key: key, + global: r.global, + } + + // Check if there is a custom ratelimit set for this bucket ID. + for _, rl := range r.customRateLimits { + if strings.HasSuffix(b.Key, rl.suffix) { + b.customRateLimit = rl + break + } + } + + r.buckets[key] = b + return b +} + +// GetWaitTime returns the duration you should wait for a Bucket +func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration { + // If we ran out of calls and the reset time is still ahead of us + // then we need to take it easy and relax a little + if b.Remaining < minRemaining && b.reset.After(time.Now()) { + return b.reset.Sub(time.Now()) + } + + // Check for global ratelimits + sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) + if now := time.Now(); now.Before(sleepTo) { + return sleepTo.Sub(now) + } + + return 0 +} + +// LockBucket Locks until a request can be made +func (r *RateLimiter) LockBucket(bucketID string) *Bucket { + return r.LockBucketObject(r.GetBucket(bucketID)) +} + +// LockBucketObject Locks an already resolved bucket until a request can be made +func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket { + b.Lock() + + if wait := r.GetWaitTime(b, 1); wait > 0 { + time.Sleep(wait) + } + + b.Remaining-- + return b +} + +// Bucket represents a ratelimit bucket, each bucket gets ratelimited individually (-global ratelimits) +type Bucket struct { + sync.Mutex + Key string + Remaining int + limit int + reset time.Time + global *int64 + + lastReset time.Time + customRateLimit *customRateLimit + Userdata interface{} +} + +// Release unlocks the bucket and reads the headers to update the buckets ratelimit info +// and locks up the whole thing in case if there's a global ratelimit. +func (b *Bucket) Release(headers http.Header) error { + defer b.Unlock() + + // Check if the bucket uses a custom ratelimiter + if rl := b.customRateLimit; rl != nil { + if time.Now().Sub(b.lastReset) >= rl.reset { + b.Remaining = rl.requests - 1 + b.lastReset = time.Now() + } + if b.Remaining < 1 { + b.reset = time.Now().Add(rl.reset) + } + return nil + } + + if headers == nil { + return nil + } + + remaining := headers.Get("X-RateLimit-Remaining") + reset := headers.Get("X-RateLimit-Reset") + global := headers.Get("X-RateLimit-Global") + resetAfter := headers.Get("X-RateLimit-Reset-After") + + // Update global and per bucket reset time if the proper headers are available + // If global is set, then it will block all buckets until after Retry-After + // If Retry-After without global is provided it will use that for the new reset + // time since it's more accurate than X-RateLimit-Reset. + // If Retry-After after is not proided, it will update the reset time from X-RateLimit-Reset + if resetAfter != "" { + parsedAfter, err := strconv.ParseFloat(resetAfter, 64) + if err != nil { + return err + } + + whole, frac := math.Modf(parsedAfter) + resetAt := time.Now().Add(time.Duration(whole) * time.Second).Add(time.Duration(frac*1000) * time.Millisecond) + + // Lock either this single bucket or all buckets + if global != "" { + atomic.StoreInt64(b.global, resetAt.UnixNano()) + } else { + b.reset = resetAt + } + } else if reset != "" { + // Calculate the reset time by using the date header returned from discord + discordTime, err := http.ParseTime(headers.Get("Date")) + if err != nil { + return err + } + + unix, err := strconv.ParseFloat(reset, 64) + if err != nil { + return err + } + + // Calculate the time until reset and add it to the current local time + // some extra time is added because without it i still encountered 429's. + // The added amount is the lowest amount that gave no 429's + // in 1k requests + whole, frac := math.Modf(unix) + delta := time.Unix(int64(whole), 0).Add(time.Duration(frac*1000)*time.Millisecond).Sub(discordTime) + time.Millisecond*250 + b.reset = time.Now().Add(delta) + } + + // Udpate remaining if header is present + if remaining != "" { + parsedRemaining, err := strconv.ParseInt(remaining, 10, 32) + if err != nil { + return err + } + b.Remaining = int(parsedRemaining) + } + + return nil +} From b5b558545c652aeb50164f7759993cb911115449 Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Mon, 25 Sep 2023 21:59:20 +0200 Subject: [PATCH 02/10] add rate limiter --- discordwebhook.go | 6 +- examples/basic_example/basic_example.go | 3 +- examples/embedded_image/embedded_image.go | 3 +- ratelimit.go | 197 ---------------------- 4 files changed, 3 insertions(+), 206 deletions(-) delete mode 100644 ratelimit.go diff --git a/discordwebhook.go b/discordwebhook.go index 4f69d1a..43b77e4 100644 --- a/discordwebhook.go +++ b/discordwebhook.go @@ -13,14 +13,13 @@ import ( "time" ) -func SendMessage(url string, message Message, r1 *RateLimiter) error { +func SendMessage(url string, message Message) error { // Validate parameters if url == "" { return errors.New("empty URL") } for { - bucket := r1.LockBucket(url) payload := new(bytes.Buffer) err := json.NewEncoder(payload).Encode(message) @@ -43,7 +42,6 @@ func SendMessage(url string, message Message, r1 *RateLimiter) error { case http.StatusOK, http.StatusNoContent: // Success resp.Body.Close() - err = bucket.Release(resp.Header) return nil case http.StatusTooManyRequests: // Rate limit exceeded, retry after backoff duration @@ -58,7 +56,6 @@ func SendMessage(url string, message Message, r1 *RateLimiter) error { time.Sleep(time.Until(resetAt)) - err = bucket.Release(resp.Header) default: // Handle other HTTP status codes defer resp.Body.Close() @@ -67,7 +64,6 @@ func SendMessage(url string, message Message, r1 *RateLimiter) error { return err } - err = bucket.Release(resp.Header) return fmt.Errorf("HTTP request failed with status %d, body: \n %s", resp.StatusCode, responseBody) } } diff --git a/examples/basic_example/basic_example.go b/examples/basic_example/basic_example.go index f26505c..0351eee 100644 --- a/examples/basic_example/basic_example.go +++ b/examples/basic_example/basic_example.go @@ -10,14 +10,13 @@ func main() { var username = "BotUser" var content = "This is a test message" var url = "https://discord.com/api/webhooks/..." - r1 := discordwebhook.NewRatelimiter() message := discordwebhook.Message{ Username: &username, Content: &content, } - err := discordwebhook.SendMessage(url, message, r1) + err := discordwebhook.SendMessage(url, message) if err != nil { log.Fatal(err) } diff --git a/examples/embedded_image/embedded_image.go b/examples/embedded_image/embedded_image.go index eda1ee1..54c4764 100644 --- a/examples/embedded_image/embedded_image.go +++ b/examples/embedded_image/embedded_image.go @@ -11,7 +11,6 @@ func main() { var content = "This is a test message" var url = "https://discord.com/api/webhooks/..." var image_url = "https://i.imgur.com/..." - r1 := discordwebhook.NewRatelimiter() image := discordwebhook.Image{ Url: &image_url, @@ -27,7 +26,7 @@ func main() { Embeds: &[]discordwebhook.Embed{embed}, } - err := discordwebhook.SendMessage(url, message, r1) + err := discordwebhook.SendMessage(url, message) if err != nil { log.Fatal(err) } diff --git a/ratelimit.go b/ratelimit.go deleted file mode 100644 index 0773821..0000000 --- a/ratelimit.go +++ /dev/null @@ -1,197 +0,0 @@ -package discordwebhook - -import ( - "math" - "net/http" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" -) - -// customRateLimit holds information for defining a custom rate limit -type customRateLimit struct { - suffix string - requests int - reset time.Duration -} - -// RateLimiter holds all ratelimit buckets -type RateLimiter struct { - sync.Mutex - global *int64 - buckets map[string]*Bucket - globalRateLimit time.Duration - customRateLimits []*customRateLimit -} - -// NewRatelimiter returns a new RateLimiter -func NewRatelimiter() *RateLimiter { - - return &RateLimiter{ - buckets: make(map[string]*Bucket), - global: new(int64), - customRateLimits: []*customRateLimit{ - { - suffix: "//reactions//", - requests: 1, - reset: 200 * time.Millisecond, - }, - }, - } -} - -// GetBucket retrieves or creates a bucket -func (r *RateLimiter) GetBucket(key string) *Bucket { - r.Lock() - defer r.Unlock() - - if bucket, ok := r.buckets[key]; ok { - return bucket - } - - b := &Bucket{ - Remaining: 1, - Key: key, - global: r.global, - } - - // Check if there is a custom ratelimit set for this bucket ID. - for _, rl := range r.customRateLimits { - if strings.HasSuffix(b.Key, rl.suffix) { - b.customRateLimit = rl - break - } - } - - r.buckets[key] = b - return b -} - -// GetWaitTime returns the duration you should wait for a Bucket -func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration { - // If we ran out of calls and the reset time is still ahead of us - // then we need to take it easy and relax a little - if b.Remaining < minRemaining && b.reset.After(time.Now()) { - return b.reset.Sub(time.Now()) - } - - // Check for global ratelimits - sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) - if now := time.Now(); now.Before(sleepTo) { - return sleepTo.Sub(now) - } - - return 0 -} - -// LockBucket Locks until a request can be made -func (r *RateLimiter) LockBucket(bucketID string) *Bucket { - return r.LockBucketObject(r.GetBucket(bucketID)) -} - -// LockBucketObject Locks an already resolved bucket until a request can be made -func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket { - b.Lock() - - if wait := r.GetWaitTime(b, 1); wait > 0 { - time.Sleep(wait) - } - - b.Remaining-- - return b -} - -// Bucket represents a ratelimit bucket, each bucket gets ratelimited individually (-global ratelimits) -type Bucket struct { - sync.Mutex - Key string - Remaining int - limit int - reset time.Time - global *int64 - - lastReset time.Time - customRateLimit *customRateLimit - Userdata interface{} -} - -// Release unlocks the bucket and reads the headers to update the buckets ratelimit info -// and locks up the whole thing in case if there's a global ratelimit. -func (b *Bucket) Release(headers http.Header) error { - defer b.Unlock() - - // Check if the bucket uses a custom ratelimiter - if rl := b.customRateLimit; rl != nil { - if time.Now().Sub(b.lastReset) >= rl.reset { - b.Remaining = rl.requests - 1 - b.lastReset = time.Now() - } - if b.Remaining < 1 { - b.reset = time.Now().Add(rl.reset) - } - return nil - } - - if headers == nil { - return nil - } - - remaining := headers.Get("X-RateLimit-Remaining") - reset := headers.Get("X-RateLimit-Reset") - global := headers.Get("X-RateLimit-Global") - resetAfter := headers.Get("X-RateLimit-Reset-After") - - // Update global and per bucket reset time if the proper headers are available - // If global is set, then it will block all buckets until after Retry-After - // If Retry-After without global is provided it will use that for the new reset - // time since it's more accurate than X-RateLimit-Reset. - // If Retry-After after is not proided, it will update the reset time from X-RateLimit-Reset - if resetAfter != "" { - parsedAfter, err := strconv.ParseFloat(resetAfter, 64) - if err != nil { - return err - } - - whole, frac := math.Modf(parsedAfter) - resetAt := time.Now().Add(time.Duration(whole) * time.Second).Add(time.Duration(frac*1000) * time.Millisecond) - - // Lock either this single bucket or all buckets - if global != "" { - atomic.StoreInt64(b.global, resetAt.UnixNano()) - } else { - b.reset = resetAt - } - } else if reset != "" { - // Calculate the reset time by using the date header returned from discord - discordTime, err := http.ParseTime(headers.Get("Date")) - if err != nil { - return err - } - - unix, err := strconv.ParseFloat(reset, 64) - if err != nil { - return err - } - - // Calculate the time until reset and add it to the current local time - // some extra time is added because without it i still encountered 429's. - // The added amount is the lowest amount that gave no 429's - // in 1k requests - whole, frac := math.Modf(unix) - delta := time.Unix(int64(whole), 0).Add(time.Duration(frac*1000)*time.Millisecond).Sub(discordTime) + time.Millisecond*250 - b.reset = time.Now().Add(delta) - } - - // Udpate remaining if header is present - if remaining != "" { - parsedRemaining, err := strconv.ParseInt(remaining, 10, 32) - if err != nil { - return err - } - b.Remaining = int(parsedRemaining) - } - - return nil -} From 7e516cc62185b0d773786731bb61f2732a774592 Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Sat, 7 Oct 2023 14:36:12 +0200 Subject: [PATCH 03/10] added timestamp --- types.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/types.go b/types.go index e9ba93d..640037a 100644 --- a/types.go +++ b/types.go @@ -1,11 +1,14 @@ package discordwebhook +import "time" + type Message struct { Username *string `json:"username,omitempty"` AvatarUrl *string `json:"avatar_url,omitempty"` Content *string `json:"content,omitempty"` Embeds *[]Embed `json:"embeds,omitempty"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` + Timestamp *time.Time `json:"timestamp,omitempty"` } type Embed struct { From 6ed80fcd188859eef28a7d48c8cf51d8c17f9204 Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Sat, 7 Oct 2023 14:38:17 +0200 Subject: [PATCH 04/10] added timestamp --- types.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/types.go b/types.go index e9ba93d..640037a 100644 --- a/types.go +++ b/types.go @@ -1,11 +1,14 @@ package discordwebhook +import "time" + type Message struct { Username *string `json:"username,omitempty"` AvatarUrl *string `json:"avatar_url,omitempty"` Content *string `json:"content,omitempty"` Embeds *[]Embed `json:"embeds,omitempty"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` + Timestamp *time.Time `json:"timestamp,omitempty"` } type Embed struct { From 2161ed8832a121dc933d44041a08c93a68cba590 Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Sat, 7 Oct 2023 15:00:17 +0200 Subject: [PATCH 05/10] added timestamp fix --- types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types.go b/types.go index 640037a..c2788bb 100644 --- a/types.go +++ b/types.go @@ -8,7 +8,6 @@ type Message struct { Content *string `json:"content,omitempty"` Embeds *[]Embed `json:"embeds,omitempty"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` - Timestamp *time.Time `json:"timestamp,omitempty"` } type Embed struct { @@ -21,6 +20,7 @@ type Embed struct { Thumbnail *Thumbnail `json:"thumbnail,omitempty"` Image *Image `json:"image,omitempty"` Footer *Footer `json:"footer,omitempty"` + Timestamp *time.Time `json:"timestamp,omitempty"` } type Author struct { From e1c5bd7c148eb28c58d02d906721d58e0cf49358 Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Thu, 9 Nov 2023 15:21:24 +0100 Subject: [PATCH 06/10] removed time stamp for pr --- discordwebhook.go | 11 ++++++----- types.go | 3 --- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/discordwebhook.go b/discordwebhook.go index 43b77e4..ec6ee63 100644 --- a/discordwebhook.go +++ b/discordwebhook.go @@ -30,9 +30,6 @@ func SendMessage(url string, message Message) error { // Make the HTTP request resp, err := http.Post(url, "application/json", payload) - if err != nil { - return err - } if err != nil { log.Printf("HTTP request failed: %v", err) return err @@ -51,14 +48,18 @@ func SendMessage(url string, message Message) error { return err } + /* + Calculate the time until reset and add it to the current local time. + Some extra time of 250ms is added because without it I still encountered 429s. + */ whole, frac := math.Modf(parsedAfter) resetAt := time.Now().Add(time.Duration(whole) * time.Second).Add(time.Duration(frac*1000) * time.Millisecond).Add(250 * time.Millisecond) time.Sleep(time.Until(resetAt)) - + resp.Body.Close() default: // Handle other HTTP status codes - defer resp.Body.Close() + resp.Body.Close() responseBody, err := ioutil.ReadAll(resp.Body) if err != nil { return err diff --git a/types.go b/types.go index 640037a..e9ba93d 100644 --- a/types.go +++ b/types.go @@ -1,14 +1,11 @@ package discordwebhook -import "time" - type Message struct { Username *string `json:"username,omitempty"` AvatarUrl *string `json:"avatar_url,omitempty"` Content *string `json:"content,omitempty"` Embeds *[]Embed `json:"embeds,omitempty"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` - Timestamp *time.Time `json:"timestamp,omitempty"` } type Embed struct { From e79cea464bde747d7f69488bd94f6ec927eb3d5e Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Wed, 15 Nov 2023 21:49:14 +0100 Subject: [PATCH 07/10] moved the ratelimited version in its own function --- discordwebhook.go | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/discordwebhook.go b/discordwebhook.go index ec6ee63..a9bc0e6 100644 --- a/discordwebhook.go +++ b/discordwebhook.go @@ -13,7 +13,12 @@ import ( "time" ) -func SendMessage(url string, message Message) error { +/* +SendMessageRateLimitAware is designed for scenarios where there is a need to dispatch numerous webhooks in rapid succession. +Its purpose is to prevent potential bans from Discord by ensuring that the requests are rate-limited, +thus maintaining a responsible and compliant approach to webhook communication. +*/ +func SendMessageRateLimitAware(url string, message Message) error { // Validate parameters if url == "" { return errors.New("empty URL") @@ -69,3 +74,30 @@ func SendMessage(url string, message Message) error { } } } + +func SendMessage(url string, message Message) error { + payload := new(bytes.Buffer) + + err := json.NewEncoder(payload).Encode(message) + if err != nil { + return err + } + + resp, err := http.Post(url, "application/json", payload) + if err != nil { + return err + } + + if resp.StatusCode != 200 && resp.StatusCode != 204 { + defer resp.Body.Close() + + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + return fmt.Errorf(string(responseBody)) + } + + return nil +} From 6e6a846a0659390172fb5719d9badf89fcb5bdb6 Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Wed, 15 Nov 2023 22:03:26 +0100 Subject: [PATCH 08/10] fixed x2 --- discordwebhook.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/discordwebhook.go b/discordwebhook.go index 4f69d1a..68dacf2 100644 --- a/discordwebhook.go +++ b/discordwebhook.go @@ -31,9 +31,6 @@ func SendMessage(url string, message Message, r1 *RateLimiter) error { // Make the HTTP request resp, err := http.Post(url, "application/json", payload) - if err != nil { - return err - } if err != nil { log.Printf("HTTP request failed: %v", err) return err From d70e13fe4599d747400800978fe380d4c16cfdcf Mon Sep 17 00:00:00 2001 From: Lorenzo Fiordiponti Date: Tue, 12 Mar 2024 23:29:44 +0100 Subject: [PATCH 09/10] changed from where it reads the ratelimit --- discordwebhook.go | 46 +++++++++++++++++++------ examples/basic_example/basic_example.go | 10 +++--- types.go | 6 ++++ 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/discordwebhook.go b/discordwebhook.go index a9bc0e6..8bc240a 100644 --- a/discordwebhook.go +++ b/discordwebhook.go @@ -5,11 +5,11 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "log" "math" "net/http" - "strconv" "time" ) @@ -43,28 +43,47 @@ func SendMessageRateLimitAware(url string, message Message) error { switch resp.StatusCode { case http.StatusOK, http.StatusNoContent: // Success - resp.Body.Close() + err := resp.Body.Close() + if err != nil { + return err + } return nil case http.StatusTooManyRequests: // Rate limit exceeded, retry after backoff duration - resetAfter := resp.Header.Get("X-RateLimit-Reset-After") - parsedAfter, err := strconv.ParseFloat(resetAfter, 64) + var response DiscordResponse + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + err = json.Unmarshal(body, &response) if err != nil { return err } /* Calculate the time until reset and add it to the current local time. - Some extra time of 250ms is added because without it I still encountered 429s. + Some extra time of 750ms is added because without it I still encountered 429s. */ - whole, frac := math.Modf(parsedAfter) - resetAt := time.Now().Add(time.Duration(whole) * time.Second).Add(time.Duration(frac*1000) * time.Millisecond).Add(250 * time.Millisecond) - time.Sleep(time.Until(resetAt)) - resp.Body.Close() + if response.RetryAfter != 0 { + + whole, frac := math.Modf(response.RetryAfter) + resetAt := time.Now().Add(time.Duration(whole) * time.Second).Add(time.Duration(frac*1000) * time.Millisecond).Add(750 * time.Millisecond) + time.Sleep(time.Until(resetAt)) + } else { + time.Sleep(5 * time.Second) + } + + err = resp.Body.Close() + if err != nil { + return err + } default: // Handle other HTTP status codes - resp.Body.Close() + err := resp.Body.Close() + if err != nil { + return err + } responseBody, err := ioutil.ReadAll(resp.Body) if err != nil { return err @@ -89,7 +108,12 @@ func SendMessage(url string, message Message) error { } if resp.StatusCode != 200 && resp.StatusCode != 204 { - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + + } + }(resp.Body) responseBody, err := ioutil.ReadAll(resp.Body) if err != nil { diff --git a/examples/basic_example/basic_example.go b/examples/basic_example/basic_example.go index 0351eee..e8cd6cc 100644 --- a/examples/basic_example/basic_example.go +++ b/examples/basic_example/basic_example.go @@ -9,15 +9,17 @@ import ( func main() { var username = "BotUser" var content = "This is a test message" - var url = "https://discord.com/api/webhooks/..." + var url = "https://discord.com/api/webhooks/1178791404290445363/af5LrdlRfzw_h80IEA7LAUfR6xvP3xGzD-Tw-jHiDmNuO7geESWjBIgyrC-pzVCJSxmA" message := discordwebhook.Message{ Username: &username, Content: &content, } + for { + err := discordwebhook.SendMessageRateLimitAware(url, message) + if err != nil { + log.Fatal(err) + } - err := discordwebhook.SendMessage(url, message) - if err != nil { - log.Fatal(err) } } diff --git a/types.go b/types.go index c2788bb..955c379 100644 --- a/types.go +++ b/types.go @@ -53,3 +53,9 @@ type AllowedMentions struct { Users *[]string `json:"users,omitempty"` Roles *[]string `json:"roles,omitempty"` } + +type DiscordResponse struct { + Message string `json:"message"` + RetryAfter float64 `json:"retry_after"` + Global bool `json:"global"` +} From df5023be16c9d965592e4bfe338d7029c2c504a4 Mon Sep 17 00:00:00 2001 From: Fiordi <52550684+TheFiordi@users.noreply.github.com> Date: Tue, 12 Mar 2024 23:32:42 +0100 Subject: [PATCH 10/10] Update basic_example.go --- examples/basic_example/basic_example.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/basic_example/basic_example.go b/examples/basic_example/basic_example.go index e8cd6cc..924dca7 100644 --- a/examples/basic_example/basic_example.go +++ b/examples/basic_example/basic_example.go @@ -9,7 +9,7 @@ import ( func main() { var username = "BotUser" var content = "This is a test message" - var url = "https://discord.com/api/webhooks/1178791404290445363/af5LrdlRfzw_h80IEA7LAUfR6xvP3xGzD-Tw-jHiDmNuO7geESWjBIgyrC-pzVCJSxmA" + var url = "" message := discordwebhook.Message{ Username: &username,