Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 92 additions & 1 deletion discordwebhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 {
Expand Down
11 changes: 6 additions & 5 deletions examples/basic_example/basic_example.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

9 changes: 9 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package discordwebhook

import "time"

type Message struct {
Username *string `json:"username,omitempty"`
AvatarUrl *string `json:"avatar_url,omitempty"`
Expand All @@ -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 {
Expand Down Expand Up @@ -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"`
}