diff --git a/discordwebhook.go b/discordwebhook.go index 2c7fed6..8bc240a 100644 --- a/discordwebhook.go +++ b/discordwebhook.go @@ -3,11 +3,97 @@ package discordwebhook import ( "bytes" "encoding/json" + "errors" "fmt" + "io" "io/ioutil" + "log" + "math" "net/http" + "time" ) +/* +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") + } + + for { + payload := new(bytes.Buffer) + + err := json.NewEncoder(payload).Encode(message) + if err != nil { + return err + } + + // Make the HTTP request + resp, err := http.Post(url, "application/json", payload) + + if err != nil { + log.Printf("HTTP request failed: %v", err) + return err + } + + switch resp.StatusCode { + case http.StatusOK, http.StatusNoContent: + // Success + err := resp.Body.Close() + if err != nil { + return err + } + return nil + case http.StatusTooManyRequests: + // Rate limit exceeded, retry after backoff duration + 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 750ms is added because without it I still encountered 429s. + */ + + 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 + err := resp.Body.Close() + if err != nil { + return err + } + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + return fmt.Errorf("HTTP request failed with status %d, body: \n %s", resp.StatusCode, responseBody) + } + } +} + func SendMessage(url string, message Message) error { payload := new(bytes.Buffer) @@ -22,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 599c016..924dca7 100644 --- a/examples/basic_example/basic_example.go +++ b/examples/basic_example/basic_example.go @@ -9,16 +9,17 @@ import ( func main() { var username = "BotUser" var content = "This is a test message" - var url = "https://discord.com/api/webhooks/..." + var url = "" 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 e9ba93d..955c379 100644 --- a/types.go +++ b/types.go @@ -1,5 +1,7 @@ package discordwebhook +import "time" + type Message struct { Username *string `json:"username,omitempty"` AvatarUrl *string `json:"avatar_url,omitempty"` @@ -18,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 { @@ -50,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"` +}