From 2150a74096460a7bceb639cc4bf745b760e9cc67 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 16 Sep 2025 21:30:59 +0700 Subject: [PATCH 01/10] feat: adapt to v0 --- .travis.yml | 2 +- bots.go | 173 +++++++++++++++++++------------------------ bots_test.go | 9 ++- client.go | 31 +++++++- client_test.go | 49 +++++++++++- errors.go | 11 +-- go.mod | 8 +- users.go | 100 ------------------------- users_test.go | 24 ------ util.go | 16 ++-- voter.go | 12 +++ webhook.go | 98 +++++++++++++----------- webhook_test.go | 50 ------------- webhook_vote_test.go | 61 +++++++++++++++ weekend_test.go | 3 +- widget_general.go | 46 ------------ widget_large.go | 80 -------------------- widget_large_test.go | 32 -------- widget_small.go | 107 -------------------------- widget_small_test.go | 33 --------- 20 files changed, 309 insertions(+), 636 deletions(-) delete mode 100644 users.go delete mode 100644 users_test.go create mode 100644 voter.go delete mode 100644 webhook_test.go create mode 100644 webhook_vote_test.go delete mode 100644 widget_general.go delete mode 100644 widget_large.go delete mode 100644 widget_large_test.go delete mode 100644 widget_small.go delete mode 100644 widget_small_test.go diff --git a/.travis.yml b/.travis.yml index 3be45e7..36e83af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.15 + - 1.21 - tip os: diff --git a/bots.go b/bots.go index 75ef7cf..e87f42c 100644 --- a/bots.go +++ b/bots.go @@ -3,30 +3,33 @@ package dbl import ( "bytes" "encoding/json" + "fmt" "strconv" "strings" "time" ) +type BotReviews struct { + // The bot's average review score out of 5 + Score float64 `json:"averageScore"` + + // The bot's review count + Count int `json:"count"` +} + type Bot struct { - // The id of the bot + // The Top.gg id of the bot ID string `json:"id"` + // The Discord id of the bot + ClientID string `json:"clientid"` + // The username of the bot Username string `json:"username"` - // The discriminator of the bot - Discriminator string `json:"discriminator"` - - // The avatar hash of the bot's avatar (may be empty) + // The bot's avatar url Avatar string `json:"avatar"` - // The cdn hash of the bot's avatar if the bot has none - DefAvatar string `json:"defAvatar"` - - // The library of the bot - Library string `json:"lib"` - // The prefix of the bot Prefix string `json:"prefix"` @@ -54,32 +57,23 @@ type Bot struct { // The custom bot invite url of the bot (may be empty) Invite string `json:"invite"` - // The date when the bot was approved + // The date when the bot was submitted Date time.Time `json:"date"` - // The certified status of the bot - CertifiedBot bool `json:"certifiedBot"` - - // The vanity url of the bot (deprecated) (may be empty) + // The vanity url of the bot (may be empty) Vanity string `json:"vanity"` - // The monthly amount of upvotes the bot has (undocumented) + // The monthly amount of votes the bot has MonthlyPoints int `json:"monthlyPoints"` - // The amount of upvotes the bot has + // The amount of votes the bot has Points int `json:"points"` - // The GuildID for the donate bot (undocumented) (may be empty) - DonateBotGuildID string `json:"donatebotguildid"` - - // The amount of servers the bot is in (undocumented) + // The amount of servers the bot is in ServerCount int `json:"server_count"` - // Server affiliation ("Servers this bot is in" field) (undocumented) - GuildAffiliation []string `json:"guilds"` - - // The amount of servers the bot is in per shard. Always present but can be empty (undocumented) - Shards []int `json:"shards"` + // The bot's reviews + Review *BotReviews `json:"reviews"` } type GetBotsPayload struct { @@ -91,10 +85,7 @@ type GetBotsPayload struct { // Default 0 Offset int - // Field search filter - Search map[string]string - - // The field to sort by. Prefix with "-" to reverse the order + // The field to sort by descending, valid field names are "id", "date", and "monthlyPoints". Sort string // A list of fields to show @@ -122,29 +113,12 @@ type GetBotsResult struct { type BotStats struct { // The amount of servers the bot is in (may be empty) ServerCount int `json:"server_count"` - - // The amount of servers the bot is in per shard. Always present but can be empty - Shards []int `json:"shards"` - - // The amount of shards a bot has (may be empty) - ShardCount int `json:"shard_count"` } type checkResponse struct { Voted int `json:"voted"` } -type BotStatsPayload struct { - // The amount of servers the bot is in per shard. - Shards []int `json:"shards"` - - // The zero-indexed id of the shard posting. Makes server_count set the shard specific server count (optional) - ShardID int `json:"shard_id"` - - // The amount of shards the bot has (optional) - ShardCount int `json:"shard_count"` -} - // Information about different bots with an optional filter parameter // // Use nil if no option is passed @@ -159,29 +133,28 @@ func (c *Client) GetBots(filter *GetBotsPayload) (*GetBotsResult, error) { req, err := c.createRequest("GET", "bots", nil) - if filter != nil { + if err != nil { + return nil, err + } else if filter != nil { q := req.URL.Query() - if filter.Limit != 0 { - q.Add("limit", strconv.Itoa(filter.Limit)) + if filter.Limit > 0 { + q.Add("limit", strconv.Itoa(min(filter.Limit, 500))) + } else { + q.Add("limit", "50") } - if filter.Offset != 0 { + if filter.Offset >= 0 { q.Add("offset", strconv.Itoa(filter.Offset)) } - if len(filter.Search) != 0 { - tStack := make([]string, 0) - - for f, v := range filter.Search { - tStack = append(tStack, f+": "+v) - } - - q.Add("search", strings.Join(tStack, " ")) - } - if filter.Sort != "" { - q.Add("sort", filter.Sort) + switch filter.Sort { + case "id", "date", "monthlyPoints": + q.Add("sort", filter.Sort) + default: + return nil, ErrInvalidRequest + } } if len(filter.Fields) != 0 { @@ -253,12 +226,10 @@ func (c *Client) GetBot(botID string) (*Bot, error) { return bot, nil } -// Use this endpoint to see who have upvoted your bot -// -// Requires authentication +// Fetches your project's recent 100 unique voters // -// IF YOU HAVE OVER 1000 VOTES PER MONTH YOU HAVE TO USE THE WEBHOOKS AND CAN NOT USE THIS -func (c *Client) GetVotes(botID string) ([]*User, error) { +// # Requires authentication +func (c *Client) GetVoters(page int) ([]*Voter, error) { if c.token == "" { return nil, ErrRequireAuthentication } @@ -267,12 +238,22 @@ func (c *Client) GetVotes(botID string) ([]*User, error) { return nil, ErrLocalRatelimit } - req, err := c.createRequest("GET", "bots/"+botID+"/votes", nil) + if page <= 0 { + return nil, ErrInvalidRequest + } + + req, err := c.createRequest("GET", fmt.Sprintf("bots/%s/votes", c.id), nil) if err != nil { return nil, err } + q := req.URL.Query() + + q.Add("page", strconv.Itoa(page)) + + req.URL.RawQuery = q.Encode() + res, err := c.httpClient.Do(req) if err != nil { @@ -285,21 +266,21 @@ func (c *Client) GetVotes(botID string) ([]*User, error) { return nil, err } - users := make([]*User, 0) + voters := make([]*Voter, 0) - err = json.Unmarshal(body, users) + err = json.Unmarshal(body, &voters) if err != nil { return nil, err } - return users, nil + return voters, nil } -// Use this endpoint to see who have upvoted your bot in the past 24 hours. It is safe to use this even if you have over 1k votes. +// Use this endpoint to see who have upvoted for your project in the past 12 hours. It is safe to use this even if you have over 1k votes. // // Requires authentication -func (c *Client) HasUserVoted(botID, userID string) (bool, error) { +func (c *Client) HasUserVoted(userID string) (bool, error) { if c.token == "" { return false, ErrRequireAuthentication } @@ -308,7 +289,7 @@ func (c *Client) HasUserVoted(botID, userID string) (bool, error) { return false, ErrLocalRatelimit } - req, err := c.createRequest("GET", "bots/"+botID+"/check", nil) + req, err := c.createRequest("GET", "bots/check", nil) if err != nil { return false, err @@ -343,51 +324,45 @@ func (c *Client) HasUserVoted(botID, userID string) (bool, error) { return cr.Voted == 1, nil } -// Information about a specific bot's stats -func (c *Client) GetBotStats(botID string) (*BotStats, error) { +// Information about your bot's server count +func (c *Client) GetBotServerCount() (int, error) { if c.token == "" { - return nil, ErrRequireAuthentication + return 0, ErrRequireAuthentication } if !c.limiter.Allow() { - return nil, ErrLocalRatelimit + return 0, ErrLocalRatelimit } - req, err := c.createRequest("GET", "bots/"+botID+"/stats", nil) + req, err := c.createRequest("GET", "bots/stats", nil) if err != nil { - return nil, err + return 0, err } res, err := c.httpClient.Do(req) if err != nil { - return nil, err + return 0, err } body, err := c.readBody(res) if err != nil { - return nil, err + return 0, err } botStats := &BotStats{} err = json.Unmarshal(body, botStats) - if err != nil { - return nil, err - } - - return botStats, nil + return botStats.ServerCount, err } -// Post your bot's stats -// -// Requires authentication +// Post your bot's server count // -// If your bot is unsharded, pass in server count as the only item in the slice -func (c *Client) PostBotStats(botID string, payload *BotStatsPayload) error { +// # Requires authentication +func (c *Client) PostBotServerCount(serverCount int) error { if c.token == "" { return ErrRequireAuthentication } @@ -396,19 +371,25 @@ func (c *Client) PostBotStats(botID string, payload *BotStatsPayload) error { return ErrLocalRatelimit } - encoded, err := json.Marshal(payload) + if serverCount <= 0 { + return ErrInvalidRequest + } + + encoded, err := json.Marshal(&BotStats{ + ServerCount: serverCount, + }) if err != nil { return err } - req, err := c.createRequest("POST", "bots/"+botID+"/stats", bytes.NewBuffer(encoded)) + req, err := c.createRequest("POST", "bots/stats", bytes.NewBuffer(encoded)) if err != nil { return err } - req.Header.Set("Authorization", c.token) + req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Content-Type", "application/json") res, err := c.httpClient.Do(req) diff --git a/bots_test.go b/bots_test.go index 924db3d..b8b0181 100644 --- a/bots_test.go +++ b/bots_test.go @@ -1,6 +1,7 @@ package dbl import ( + "log" "os" "testing" @@ -13,7 +14,7 @@ const ( ) func TestBots(t *testing.T) { - client, err := NewClient(os.Getenv("apikey")) + client, err := NewClient(os.Getenv("TOPGG_TOKEN")) assert.Nil(t, err, "Client should be created w/o error") @@ -21,6 +22,10 @@ func TestBots(t *testing.T) { Limit: fetchLimit, }) + if err != nil { + log.Fatal(err) + } + assert.Nil(t, err, "Request should be successful (API depended)") assert.Equal(t, fetchLimit, len(bots.Results), "Results array size should match request limit") @@ -29,7 +34,7 @@ func TestBots(t *testing.T) { } func TestBot(t *testing.T) { - client, err := NewClient(os.Getenv("apikey")) + client, err := NewClient(os.Getenv("TOPGG_TOKEN")) assert.Nil(t, err, "Client should be created w/o error") diff --git a/client.go b/client.go index 3e2782b..f55a207 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,11 @@ package dbl import ( + "encoding/base64" + "encoding/json" "fmt" "net/http" + "strings" "sync" "time" @@ -32,22 +35,44 @@ type Client struct { limiter *rate.Limiter httpClient HTTPClient token string + id string +} + +type tokenStructure struct { + Id string `json:"id"` } // NewClient returns a new *Client after applying the options provided. func NewClient(token string, options ...OptionFunc) (*Client, error) { + tokenSections := strings.Split(token, ".") + + if len(tokenSections) != 3 { + return nil, ErrRequireAuthentication + } + + decodedTokenSection, err := base64.RawURLEncoding.DecodeString(tokenSections[1]) + + if err != nil { + return nil, ErrRequireAuthentication + } + + innerTokenStructure := &tokenStructure{} + + if err = json.Unmarshal(decodedTokenSection, innerTokenStructure); err != nil { + return nil, ErrRequireAuthentication + } + client := &Client{ limiter: rate.NewLimiter(1, 60), httpClient: &http.Client{Timeout: defaultTimeout}, token: token, + id: innerTokenStructure.Id, } for _, optionFunc := range options { if optionFunc == nil { return nil, fmt.Errorf("invalid nil dbl.Client option func") - } - - if err := optionFunc(client); err != nil { + } else if err := optionFunc(client); err != nil { return nil, fmt.Errorf("error running dbl.Client option func: %w", err) } } diff --git a/client_test.go b/client_test.go index 5ae6495..79cf75f 100644 --- a/client_test.go +++ b/client_test.go @@ -3,8 +3,11 @@ package dbl import ( "log" "net/http" + "os" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestNewClient(t *testing.T) { @@ -12,14 +15,52 @@ func TestNewClient(t *testing.T) { httpClient := &http.Client{} - _, err := NewClient( - "token", + client, err := NewClient( + os.Getenv("TOPGG_TOKEN"), HTTPClientOption(httpClient), // Setting a custom HTTP client. Default is *http.Client with default timeout. TimeoutOption(clientTimeout), // Setting timeout option. Default is 3 seconds ) + if err != nil { - log.Fatalf("Error creating new Discord Bot List client: %s", err) + log.Fatalf("Error creating new Top.gg client: %s", err) } - // ... + _, err = client.GetBotServerCount() + + assert.Nil(t, err, "GetBotServerCount should succeed") + + err = client.PostBotServerCount(2) + + assert.Nil(t, err, "PostBotServerCount should succeed") + + time.Sleep(1 * time.Second) + _, err = client.GetBot("264811613708746752") + + assert.Nil(t, err, "GetBot should succeed") + + getBotsPayload := GetBotsPayload{ + Limit: 50, + Offset: 0, + Sort: "id", + } + + time.Sleep(1 * time.Second) + _, err = client.GetBots(&getBotsPayload) + + assert.Nil(t, err, "GetBots should succeed") + + time.Sleep(1 * time.Second) + _, err = client.GetVoters(1) + + assert.Nil(t, err, "GetVoters should succeed") + + time.Sleep(1 * time.Second) + _, err = client.HasUserVoted("8226924471638491136") + + assert.Nil(t, err, "HasUserVoted should succeed") + + time.Sleep(1 * time.Second) + _, err = client.IsMultiplierActive() + + assert.Nil(t, err, "IsMultiplierActive should succeed") } diff --git a/errors.go b/errors.go index 00b853c..8235d95 100644 --- a/errors.go +++ b/errors.go @@ -3,9 +3,10 @@ package dbl import "errors" var ( - ErrRequestFailed = errors.New("Remote request failed with non 200 status code") - ErrLocalRatelimit = errors.New("Exceeded local rate limit") - ErrRemoteRatelimit = errors.New("Exceeded remote rate limit") - ErrUnauthorizedRequest = errors.New("Unauthorized request") - ErrRequireAuthentication = errors.New("Endpoint requires valid token") + ErrRequestFailed = errors.New("remote request failed with non 200 status code") + ErrLocalRatelimit = errors.New("exceeded local rate limit") + ErrRemoteRatelimit = errors.New("exceeded remote rate limit") + ErrUnauthorizedRequest = errors.New("unauthorized request") + ErrRequireAuthentication = errors.New("endpoint requires valid token") + ErrInvalidRequest = errors.New("invalid attempted request") ) diff --git a/go.mod b/go.mod index 13d86fc..1edee17 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,14 @@ module github.com/top-gg/go-dbl -go 1.15 +go 1.21 require ( github.com/stretchr/testify v1.5.1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 ) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) diff --git a/users.go b/users.go deleted file mode 100644 index a5d32e2..0000000 --- a/users.go +++ /dev/null @@ -1,100 +0,0 @@ -package dbl - -import ( - "encoding/json" -) - -type User struct { - // The id of the user - ID string `json:"id"` - - // The username of the user - Username string `json:"username"` - - // The discriminator of the user - Discriminator string `json:"discriminator"` - - // The avatar hash of the user's avatar (may be empty) - Avatar string `json:"avatar"` - - // The cdn hash of the user's avatar if the user has none - DefAvatar string `json:"defAvatar"` - - // The bio of the user - Biography string `json:"bio"` - - // The banner image url of the user (may be empty) - Banner string `json:"banner"` - - Social *Social `json:"social"` - - // The custom hex color of the user (may be empty) - Color string `json:"color"` - - // The supporter status of the user - Supporter bool `json:"supporter"` - - // The certified status of the user - CertifiedDeveloper bool `json:"certifiedDev"` - - // The mod status of the user - Moderator bool `json:"mod"` - - // The website moderator status of the user - WebsiteModerator bool `json:"webMod"` - - // The admin status of the user - Admin bool `json:"admin"` -} - -type Social struct { - // The youtube channel id of the user (may be empty) - Youtube string `json:"youtube"` - - // The reddit username of the user (may be empty) - Reddit string `json:"reddit"` - - // The twitter username of the user (may be empty) - Twitter string `json:"twitter"` - - // The instagram username of the user (may be empty) - Instagram string `json:"instagram"` - - // The github username of the user (may be empty) - Github string `json:"github"` -} - -// Information about a particular user -func (c *Client) GetUser(UserID string) (*User, error) { - if c.token == "" { - return nil, ErrRequireAuthentication - } - - req, err := c.createRequest("GET", "users/"+UserID, nil) - - if err != nil { - return nil, err - } - - res, err := c.httpClient.Do(req) - - if err != nil { - return nil, err - } - - body, err := c.readBody(res) - - if err != nil { - return nil, err - } - - user := &User{} - - err = json.Unmarshal(body, user) - - if err != nil { - return nil, err - } - - return user, nil -} diff --git a/users_test.go b/users_test.go deleted file mode 100644 index 023ea2d..0000000 --- a/users_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package dbl - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -const ( - testUserID = "105122038586286080" -) - -func TestUsers(t *testing.T) { - client, err := NewClient(os.Getenv("apikey")) - - assert.Nil(t, err, "Client should be created w/o error") - - user, err := client.GetUser(testUserID) - - assert.Nil(t, err, "Unable to get user data") - - assert.Equal(t, testUserID, user.ID, "Request & response user ID should match") -} diff --git a/util.go b/util.go index 01d5c72..4adf8af 100644 --- a/util.go +++ b/util.go @@ -3,7 +3,6 @@ package dbl import ( "encoding/json" "io" - "io/ioutil" "net/http" ) @@ -14,15 +13,18 @@ type ratelimitResponse struct { func (c *Client) readBody(res *http.Response) ([]byte, error) { defer res.Body.Close() - if res.StatusCode == 401 { + switch res.StatusCode { + case 400: + return nil, ErrInvalidRequest + case 401: return nil, ErrUnauthorizedRequest - } - - if res.StatusCode != 200 { + case 200: + break + default: return nil, ErrRequestFailed } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return nil, err @@ -54,7 +56,7 @@ func (c *Client) createRequest(method, endpoint string, body io.Reader) (*http.R return nil, err } - req.Header.Set("Authorization", c.token) + req.Header.Set("Authorization", "Bearer "+c.token) return req, nil } diff --git a/voter.go b/voter.go new file mode 100644 index 0000000..71aec03 --- /dev/null +++ b/voter.go @@ -0,0 +1,12 @@ +package dbl + +type Voter struct { + // The id of the voter + ID string `json:"id"` + + // The username of the voter + Username string `json:"username"` + + // The voter's avatar url + Avatar string `json:"avatar"` +} diff --git a/webhook.go b/webhook.go index fc01bf4..4557bfa 100644 --- a/webhook.go +++ b/webhook.go @@ -2,28 +2,29 @@ package dbl import ( "encoding/json" - "io/ioutil" + "io" "net/http" "net/url" ) -type ListenerFunc func(*WebhookPayload) +type WebhookListenerFunc func([]byte) type WebhookListener struct { token string - handler ListenerFunc + path string + handler WebhookListenerFunc mux *http.ServeMux } -type WebhookPayload struct { - // ID of the bot that received a vote - Bot string +type WebhookVotePayload struct { + // ID of the project that received a vote + ReceiverId string // ID of the user who voted - User string + VoterId string - // The type of the vote (should always be "upvote" except when using the test button it's "test") - Type string + // Whether this vote is just a test done from the page settings + IsTest bool // Whether the weekend multiplier is in effect, meaning users votes count as two IsWeekend bool @@ -32,28 +33,55 @@ type WebhookPayload struct { Query url.Values } -type wPayload struct { - // ID of the bot that received a vote - Bot string `json:"bot"` +type wVotePayload struct { + Bot *string `json:"bot"` + Server *string `json:"guild"` + User string `json:"user"` + Type string `json:"type"` + IsWeekend *bool `json:"isWeekend"` + Query string `json:"query"` +} - // ID of the user who voted - User string `json:"user"` +func NewWebhookVotePayload(data []byte) (*WebhookVotePayload, error) { + p := &wVotePayload{} - // The type of the vote (should always be "upvote" except when using the test button it's "test") - Type string `json:"type"` + if err := json.Unmarshal(data, p); err != nil { + return nil, err + } - // Whether the weekend multiplier is in effect, meaning users votes count as two - IsWeekend bool `json:"isWeekend"` + m, err := url.ParseQuery(p.Query) - // Query string params found on the /bot/:ID/vote page. Example: ?a=1&b=2 - Query string `json:"query"` + if err != nil { + return nil, err + } + + receiverId := p.Bot + + if receiverId == nil { + receiverId = p.Server + } + + isWeekend := false + + if p.IsWeekend != nil { + isWeekend = *p.IsWeekend + } + + return &WebhookVotePayload{ + ReceiverId: *receiverId, + VoterId: p.User, + IsTest: p.Type == "test", + IsWeekend: isWeekend, + Query: m, + }, nil } // Create a new webhook listener -func NewListener(token string, handler func(*WebhookPayload)) *WebhookListener { +func NewWebhookListener(token string, path string, handler func([]byte)) *WebhookListener { return &WebhookListener{ token: token, - handler: ListenerFunc(handler), + path: path, + handler: WebhookListenerFunc(handler), } } @@ -62,12 +90,12 @@ func NewListener(token string, handler func(*WebhookPayload)) *WebhookListener { func (wl *WebhookListener) Serve(addr string) error { wl.mux = http.NewServeMux() - wl.mux.HandleFunc("/", wl.handlePayload) + wl.mux.HandleFunc(wl.path, wl.handleRequest) return http.ListenAndServe(addr, wl.mux) } -func (wl *WebhookListener) handlePayload(w http.ResponseWriter, r *http.Request) { +func (wl *WebhookListener) handleRequest(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) @@ -80,19 +108,7 @@ func (wl *WebhookListener) handlePayload(w http.ResponseWriter, r *http.Request) return } - body, err := ioutil.ReadAll(r.Body) - - if err != nil { - return - } - - p := &wPayload{} - - if err = json.Unmarshal(body, p); err != nil { - return - } - - m, err := url.ParseQuery(p.Query) + body, err := io.ReadAll(r.Body) if err != nil { return @@ -100,11 +116,5 @@ func (wl *WebhookListener) handlePayload(w http.ResponseWriter, r *http.Request) w.WriteHeader(http.StatusNoContent) - wl.handler(&WebhookPayload{ - Bot: p.Bot, - User: p.User, - Type: p.Type, - IsWeekend: p.IsWeekend, - Query: m, - }) + wl.handler(body) } diff --git a/webhook_test.go b/webhook_test.go deleted file mode 100644 index 2647c9c..0000000 --- a/webhook_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package dbl - -import ( - "bytes" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -const ( - testToken = "wblAV@d!Od9uL761Rz23BEQC$#YCJdQ0nDlZUEfnDxY" -) - -var ( - testPayload = []byte(`{"bot":"441751906428256277","user":"105122038586286080","type":"upvote","isWeekend":false,"query":""}`) - testListener = NewListener(testToken, func(p *WebhookPayload) {}) -) - -func TestHookMethod(t *testing.T) { - rec := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/", bytes.NewBuffer(testPayload)) - - testListener.handlePayload(rec, req) - - assert.Equal(t, http.StatusMethodNotAllowed, rec.Code, "GET method should not be allowed") -} - -func TestHookAuthentication(t *testing.T) { - rec := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(testPayload)) - - testListener.handlePayload(rec, req) - - assert.Equal(t, http.StatusUnauthorized, rec.Code, "Unauthorized request should not be processed") -} - -func TestWebhookProcessing(t *testing.T) { - rec := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(testPayload)) - req.Header.Set("Authorization", testToken) - - testListener.handlePayload(rec, req) - - assert.Equal(t, http.StatusNoContent, rec.Code, "Request should succeed w/o content") -} diff --git a/webhook_vote_test.go b/webhook_vote_test.go new file mode 100644 index 0000000..f2aa075 --- /dev/null +++ b/webhook_vote_test.go @@ -0,0 +1,61 @@ +package dbl + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + testToken = "wblAV@d!Od9uL761Rz23BEQC$#YCJdQ0nDlZUEfnDxY" +) + +var ( + testPayload = []byte(`{"bot":"441751906428256277","user":"105122038586286080","type":"upvote","isWeekend":false,"query":""}`) + testListener = NewWebhookListener(testToken, "/votes", func(payload []byte) { + vote, err := NewWebhookVotePayload(payload) + + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Unable to parse webhook payload: %s", err) + return + } + + fmt.Printf("A user with the ID of %s has voted us on Top.gg!", vote.VoterId) + }) +) + +func TestWebhookVoteHookMethod(t *testing.T) { + rec := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/votes", bytes.NewBuffer(testPayload)) + + testListener.handleRequest(rec, req) + + assert.Equal(t, http.StatusMethodNotAllowed, rec.Code, "GET method should not be allowed") +} + +func TestWebhookVoteHookAuthentication(t *testing.T) { + rec := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, "/votes", bytes.NewBuffer(testPayload)) + + testListener.handleRequest(rec, req) + + assert.Equal(t, http.StatusUnauthorized, rec.Code, "Unauthorized request should not be processed") +} + +func TestWebhookVoteProcessing(t *testing.T) { + rec := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, "/votes", bytes.NewBuffer(testPayload)) + req.Header.Set("Authorization", "Bearer "+testToken) + + testListener.handleRequest(rec, req) + + assert.Equal(t, http.StatusNoContent, rec.Code, "Request should succeed w/o content") +} diff --git a/weekend_test.go b/weekend_test.go index 2950f5e..3c4eac7 100644 --- a/weekend_test.go +++ b/weekend_test.go @@ -1,13 +1,14 @@ package dbl import ( + "os" "testing" "github.com/stretchr/testify/assert" ) func TestWeekend(t *testing.T) { - client, err := NewClient("Unauthenticated request") + client, err := NewClient(os.Getenv("TOPGG_TOKEN")) assert.Nil(t, err, "Client should be created w/o error") diff --git a/widget_general.go b/widget_general.go deleted file mode 100644 index b29afb9..0000000 --- a/widget_general.go +++ /dev/null @@ -1,46 +0,0 @@ -package dbl - -import ( - "net/url" - "strconv" -) - -type Extension int - -const ( - SVG Extension = iota - PNG -) - -type Widget interface { - BotID() Widget - Generate() string - Extension() Widget -} - -type WidgetData struct { - botID string - values url.Values - extension Extension -} - -func (w *WidgetData) setValue(key string, value int64) { - w.values.Add(key, strconv.FormatInt(value, 16)) -} - -func (e Extension) Ext() string { - switch e { - case SVG: - { - return ".svg" - } - case PNG: - { - return ".png" - } - default: - { - return ".svg" - } - } -} diff --git a/widget_large.go b/widget_large.go deleted file mode 100644 index 2552fe6..0000000 --- a/widget_large.go +++ /dev/null @@ -1,80 +0,0 @@ -package dbl - -import ( - "net/url" -) - -type LargeWidget struct { - Widget - data WidgetData -} - -func NewLargeWidget() *LargeWidget { - return &LargeWidget{ - data: WidgetData{ - values: url.Values(make(map[string][]string)), - }, - } -} - -func (w *LargeWidget) BotID(id string) *LargeWidget { - w.data.botID = id - - return w -} - -func (w *LargeWidget) Extension(extension Extension) *LargeWidget { - w.data.extension = extension - - return w -} - -func (w *LargeWidget) TopColor(color int64) *LargeWidget { - w.data.setValue("topcolor", color) - - return w -} - -func (w *LargeWidget) MiddleColor(color int64) *LargeWidget { - w.data.setValue("middlecolor", color) - - return w -} - -func (w *LargeWidget) UsernameColor(color int64) *LargeWidget { - w.data.setValue("usernamecolor", color) - - return w -} - -func (w *LargeWidget) CertifiedColor(color int64) *LargeWidget { - w.data.setValue("certifiedcolor", color) - - return w -} - -func (w *LargeWidget) DataColor(color int64) *LargeWidget { - w.data.setValue("datacolor", color) - - return w -} - -func (w *LargeWidget) LabelColor(color int64) *LargeWidget { - w.data.setValue("labelcolor", color) - - return w -} - -func (w *LargeWidget) HighlightColor(color int64) *LargeWidget { - w.data.setValue("highlightcolor", color) - - return w -} - -func (w *LargeWidget) Generate() string { - u, _ := url.Parse(BaseURL + "widgets/" + w.data.botID + w.data.extension.Ext()) - - u.RawQuery = w.data.values.Encode() - - return u.String() -} diff --git a/widget_large_test.go b/widget_large_test.go deleted file mode 100644 index 1233f38..0000000 --- a/widget_large_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package dbl - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -var ( - expectedLargeWidget = BaseURL + "widgets/574652751745777665.png?certifiedcolor=343434&topcolor=fffff" - expectedLargeWidgetNoExt = BaseURL + "widgets/574652751745777665.svg?datacolor=309839" -) - -func TestWidgetLarge(t *testing.T) { - w := NewLargeWidget(). - BotID(testBotID). - Extension(PNG). - TopColor(0xfffff). - CertifiedColor(0x343434). - Generate() - - assert.Equal(t, expectedLargeWidget, w) -} - -func TestWidgetLargeNoExt(t *testing.T) { - w := NewLargeWidget(). - BotID(testBotID). - DataColor(0x309839). - Generate() - - assert.Equal(t, expectedLargeWidgetNoExt, w) -} diff --git a/widget_small.go b/widget_small.go deleted file mode 100644 index dc6c5f4..0000000 --- a/widget_small.go +++ /dev/null @@ -1,107 +0,0 @@ -package dbl - -import "net/url" - -type WidgetType int - -const ( - WidgetStatus WidgetType = iota - WidgetUpvotes - WidgetServers - WidgetLib -) - -func (t WidgetType) String() string { - switch t { - case WidgetStatus: - { - return "status" - } - case WidgetUpvotes: - { - return "upvotes" - } - case WidgetServers: - { - return "servers" - } - case WidgetLib: - { - return "lib" - } - default: - { - return "status" - } - } -} - -type SmallWidget struct { - Widget - data WidgetData - wType WidgetType -} - -func NewSmallWidget() *SmallWidget { - return &SmallWidget{ - data: WidgetData{ - values: url.Values(make(map[string][]string)), - }, - } -} - -func (w *SmallWidget) BotID(id string) *SmallWidget { - w.data.botID = id - - return w -} - -func (w *SmallWidget) Extension(extension Extension) *SmallWidget { - w.data.extension = extension - - return w -} - -func (w *SmallWidget) WidgetType(wType WidgetType) *SmallWidget { - w.wType = wType - - return w -} - -func (w *SmallWidget) AvatarBackground(color int64) *SmallWidget { - w.data.setValue("avatarbg", color) - - return w -} - -func (w *SmallWidget) LeftColor(color int64) *SmallWidget { - w.data.setValue("leftcolor", color) - - return w -} - -func (w *SmallWidget) RightColor(color int64) *SmallWidget { - w.data.setValue("rightcolor", color) - - return w -} - -func (w *SmallWidget) LeftTextColor(color int64) *SmallWidget { - w.data.setValue("lefttextcolor", color) - - return w -} - -func (w *SmallWidget) RightTextColor(color int64) *SmallWidget { - w.data.setValue("righttextcolor", color) - - return w -} - -func (w *SmallWidget) Generate() string { - u, _ := url.Parse(BaseURL + "widgets/" + w.wType.String() + "/" + w.data.botID + w.data.extension.Ext()) - - u.RawQuery = w.data.values.Encode() - - return u.String() -} diff --git a/widget_small_test.go b/widget_small_test.go deleted file mode 100644 index 8f997f6..0000000 --- a/widget_small_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package dbl - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -var ( - expectedSmallWidget = BaseURL + "widgets/servers/574652751745777665.svg?leftcolor=555798&righttextcolor=f12350" - expectedSmallWidgetNoType = BaseURL + "widgets/status/574652751745777665.svg?avatarbg=987230&lefttextcolor=123890" -) - -func TestWidgetSmall(t *testing.T) { - w := NewSmallWidget(). - BotID(testBotID). - WidgetType(WidgetServers). - LeftColor(0x555798). - RightTextColor(0xf12350). - Generate() - - assert.Equal(t, expectedSmallWidget, w) -} - -func TestWidgetSmallNoType(t *testing.T) { - w := NewSmallWidget(). - BotID(testBotID). - AvatarBackground(0x987230). - LeftTextColor(0x123890). - Generate() - - assert.Equal(t, expectedSmallWidgetNoType, w) -} From 00aae19d8be565239bbfd59cf054d1c63b903588 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 16 Sep 2025 21:36:48 +0700 Subject: [PATCH 02/10] meta: update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 7d38c09..d160875 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 RumbleFrog +Copyright (c) 2018-2025 RumbleFrog Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 6fe6dc28889d442fabdba1b5f1cb7a039e5f78b0 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 18 Sep 2025 22:59:47 +0700 Subject: [PATCH 03/10] revert: revert go 1.21 requirement --- .travis.yml | 2 +- bots.go | 6 +++++- go.mod | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 36e83af..3be45e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.21 + - 1.15 - tip os: diff --git a/bots.go b/bots.go index e87f42c..4d19878 100644 --- a/bots.go +++ b/bots.go @@ -138,8 +138,12 @@ func (c *Client) GetBots(filter *GetBotsPayload) (*GetBotsResult, error) { } else if filter != nil { q := req.URL.Query() + if filter.Limit > 500 { + filter.Limit = 500 + } + if filter.Limit > 0 { - q.Add("limit", strconv.Itoa(min(filter.Limit, 500))) + q.Add("limit", strconv.Itoa(filter.Limit)) } else { q.Add("limit", "50") } diff --git a/go.mod b/go.mod index 1edee17..1758621 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/top-gg/go-dbl -go 1.21 +go 1.15 require ( github.com/stretchr/testify v1.5.1 From 9a763a5ca60610299930e4945dbbb18f9889d1c1 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 18 Sep 2025 23:57:43 +0700 Subject: [PATCH 04/10] revert: revert almost all breaking changes --- bots.go | 115 ++++++++++++++++++------------------------- client_test.go | 16 +++--- go.mod | 6 --- users.go | 62 +++++++++++++++++++++++ util.go | 9 ++++ webhook_vote_test.go | 2 +- weekend.go | 4 -- 7 files changed, 130 insertions(+), 84 deletions(-) create mode 100644 users.go diff --git a/bots.go b/bots.go index 4d19878..7ca8e0f 100644 --- a/bots.go +++ b/bots.go @@ -85,6 +85,9 @@ type GetBotsPayload struct { // Default 0 Offset int + // [Deprecated since API v0] Field search filter + Search map[string]string + // The field to sort by descending, valid field names are "id", "date", and "monthlyPoints". Sort string @@ -113,24 +116,36 @@ type GetBotsResult struct { type BotStats struct { // The amount of servers the bot is in (may be empty) ServerCount int `json:"server_count"` + + // [Deprecated since API v0] The amount of servers the bot is in per shard. Always present but can be empty + Shards []int `json:"shards"` + + // [Deprecated since API v0] The amount of shards a bot has (may be empty) + ShardCount int `json:"shard_count"` } type checkResponse struct { Voted int `json:"voted"` } +type BotStatsPayload struct { + // The amount of servers the bot is in, must not be zero. + ServerCount int `json:"server_count"` + + // [Deprecated since API v0] The amount of servers the bot is in per shard. + Shards []int `json:"shards"` + + // [Deprecated since API v0] The zero-indexed id of the shard posting. Makes server_count set the shard specific server count (optional) + ShardID int `json:"shard_id"` + + // [Deprecated since API v0] The amount of shards the bot has (optional) + ShardCount int `json:"shard_count"` +} + // Information about different bots with an optional filter parameter // // Use nil if no option is passed func (c *Client) GetBots(filter *GetBotsPayload) (*GetBotsResult, error) { - if c.token == "" { - return nil, ErrRequireAuthentication - } - - if !c.limiter.Allow() { - return nil, ErrLocalRatelimit - } - req, err := c.createRequest("GET", "bots", nil) if err != nil { @@ -193,14 +208,6 @@ func (c *Client) GetBots(filter *GetBotsPayload) (*GetBotsResult, error) { // Information about a specific bot func (c *Client) GetBot(botID string) (*Bot, error) { - if c.token == "" { - return nil, ErrRequireAuthentication - } - - if !c.limiter.Allow() { - return nil, ErrLocalRatelimit - } - req, err := c.createRequest("GET", "bots/"+botID, nil) if err != nil { @@ -233,15 +240,7 @@ func (c *Client) GetBot(botID string) (*Bot, error) { // Fetches your project's recent 100 unique voters // // # Requires authentication -func (c *Client) GetVoters(page int) ([]*Voter, error) { - if c.token == "" { - return nil, ErrRequireAuthentication - } - - if !c.limiter.Allow() { - return nil, ErrLocalRatelimit - } - +func (c *Client) GetVotes(page int) ([]*User, error) { if page <= 0 { return nil, ErrInvalidRequest } @@ -270,29 +269,23 @@ func (c *Client) GetVoters(page int) ([]*Voter, error) { return nil, err } - voters := make([]*Voter, 0) + users := make([]*User, 0) - err = json.Unmarshal(body, &voters) + err = json.Unmarshal(body, &users) if err != nil { return nil, err } - return voters, nil + return users, nil } // Use this endpoint to see who have upvoted for your project in the past 12 hours. It is safe to use this even if you have over 1k votes. // -// Requires authentication -func (c *Client) HasUserVoted(userID string) (bool, error) { - if c.token == "" { - return false, ErrRequireAuthentication - } - - if !c.limiter.Allow() { - return false, ErrLocalRatelimit - } - +// # Requires authentication +// +// [Deprecated since API v0]: The _botID argument is no longer used. +func (c *Client) HasUserVoted(_botID, userID string) (bool, error) { req, err := c.createRequest("GET", "bots/check", nil) if err != nil { @@ -328,59 +321,52 @@ func (c *Client) HasUserVoted(userID string) (bool, error) { return cr.Voted == 1, nil } -// Information about your bot's server count -func (c *Client) GetBotServerCount() (int, error) { - if c.token == "" { - return 0, ErrRequireAuthentication - } - - if !c.limiter.Allow() { - return 0, ErrLocalRatelimit - } - +// Information about a specific bot's stats +// +// [Deprecated since API v0]: The _botID argument is no longer used. +func (c *Client) GetBotStats(_botID string) (*BotStats, error) { req, err := c.createRequest("GET", "bots/stats", nil) if err != nil { - return 0, err + return nil, err } res, err := c.httpClient.Do(req) if err != nil { - return 0, err + return nil, err } body, err := c.readBody(res) if err != nil { - return 0, err + return nil, err } botStats := &BotStats{} err = json.Unmarshal(body, botStats) - return botStats.ServerCount, err + if err != nil { + return nil, err + } + + return botStats, nil } // Post your bot's server count // // # Requires authentication -func (c *Client) PostBotServerCount(serverCount int) error { - if c.token == "" { - return ErrRequireAuthentication - } - - if !c.limiter.Allow() { - return ErrLocalRatelimit - } - - if serverCount <= 0 { +// +// [Deprecated since API v0]: The _botID argument is no longer used. +func (c *Client) PostBotStats(_botID string, payload *BotStatsPayload) error { + if payload.ServerCount <= 0 { return ErrInvalidRequest } encoded, err := json.Marshal(&BotStats{ - ServerCount: serverCount, + ServerCount: payload.ServerCount, + Shards: []int{}, }) if err != nil { @@ -393,9 +379,6 @@ func (c *Client) PostBotServerCount(serverCount int) error { return err } - req.Header.Set("Authorization", "Bearer "+c.token) - req.Header.Set("Content-Type", "application/json") - res, err := c.httpClient.Do(req) if err != nil { diff --git a/client_test.go b/client_test.go index 79cf75f..60b0bf4 100644 --- a/client_test.go +++ b/client_test.go @@ -25,13 +25,15 @@ func TestNewClient(t *testing.T) { log.Fatalf("Error creating new Top.gg client: %s", err) } - _, err = client.GetBotServerCount() + _, err = client.GetBotStats("") - assert.Nil(t, err, "GetBotServerCount should succeed") + assert.Nil(t, err, "GetBotStats should succeed") - err = client.PostBotServerCount(2) + err = client.PostBotStats("", &BotStatsPayload{ + ServerCount: 2, + }) - assert.Nil(t, err, "PostBotServerCount should succeed") + assert.Nil(t, err, "PostBotStats should succeed") time.Sleep(1 * time.Second) _, err = client.GetBot("264811613708746752") @@ -50,12 +52,12 @@ func TestNewClient(t *testing.T) { assert.Nil(t, err, "GetBots should succeed") time.Sleep(1 * time.Second) - _, err = client.GetVoters(1) + _, err = client.GetVotes(1) - assert.Nil(t, err, "GetVoters should succeed") + assert.Nil(t, err, "GetVotes should succeed") time.Sleep(1 * time.Second) - _, err = client.HasUserVoted("8226924471638491136") + _, err = client.HasUserVoted("", "8226924471638491136") assert.Nil(t, err, "HasUserVoted should succeed") diff --git a/go.mod b/go.mod index 1758621..13d86fc 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,3 @@ require ( github.com/stretchr/testify v1.5.1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 ) - -require ( - github.com/davecgh/go-spew v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect -) diff --git a/users.go b/users.go new file mode 100644 index 0000000..1567bc5 --- /dev/null +++ b/users.go @@ -0,0 +1,62 @@ +package dbl + +type User struct { + // The id of the user + ID string `json:"id"` + + // The username of the user + Username string `json:"username"` + + // [Deprecated since API v0]: The discriminator of the user + Discriminator string `json:"discriminator"` + + // The avatar hash of the user's avatar (may be empty) + Avatar string `json:"avatar"` + + // [Deprecated since API v0]: The cdn hash of the user's avatar if the user has none + DefAvatar string `json:"defAvatar"` + + // [Deprecated since API v0]: The bio of the user + Biography string `json:"bio"` + + // [Deprecated since API v0]: The banner image url of the user (may be empty) + Banner string `json:"banner"` + + // [Deprecated since API v0]: The user's socials + Social *Social `json:"social"` + + // [Deprecated since API v0]: The custom hex color of the user (may be empty) + Color string `json:"color"` + + // [Deprecated since API v0]: The supporter status of the user + Supporter bool `json:"supporter"` + + // [Deprecated since API v0]: The certified status of the user + CertifiedDeveloper bool `json:"certifiedDev"` + + // [Deprecated since API v0]: The mod status of the user + Moderator bool `json:"mod"` + + // [Deprecated since API v0]: The website moderator status of the user + WebsiteModerator bool `json:"webMod"` + + // [Deprecated since API v0]: The admin status of the user + Admin bool `json:"admin"` +} + +type Social struct { + // [Deprecated since API v0]: The youtube channel id of the user (may be empty) + Youtube string `json:"youtube"` + + // [Deprecated since API v0]: The reddit username of the user (may be empty) + Reddit string `json:"reddit"` + + // [Deprecated since API v0]: The twitter username of the user (may be empty) + Twitter string `json:"twitter"` + + // [Deprecated since API v0]: The instagram username of the user (may be empty) + Instagram string `json:"instagram"` + + // [Deprecated since API v0]: The github username of the user (may be empty) + Github string `json:"github"` +} diff --git a/util.go b/util.go index 4adf8af..a2bf44b 100644 --- a/util.go +++ b/util.go @@ -50,6 +50,14 @@ func (c *Client) readBody(res *http.Response) ([]byte, error) { } func (c *Client) createRequest(method, endpoint string, body io.Reader) (*http.Request, error) { + if c.token == "" { + return nil, ErrRequireAuthentication + } + + if !c.limiter.Allow() { + return nil, ErrLocalRatelimit + } + req, err := http.NewRequest(method, BaseURL+endpoint, body) if err != nil { @@ -57,6 +65,7 @@ func (c *Client) createRequest(method, endpoint string, body io.Reader) (*http.R } req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("Content-Type", "application/json") return req, nil } diff --git a/webhook_vote_test.go b/webhook_vote_test.go index f2aa075..a998f10 100644 --- a/webhook_vote_test.go +++ b/webhook_vote_test.go @@ -53,7 +53,7 @@ func TestWebhookVoteProcessing(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/votes", bytes.NewBuffer(testPayload)) - req.Header.Set("Authorization", "Bearer "+testToken) + req.Header.Set("Authorization", testToken) testListener.handleRequest(rec, req) diff --git a/weekend.go b/weekend.go index c4cd515..2627a85 100644 --- a/weekend.go +++ b/weekend.go @@ -10,10 +10,6 @@ type weekendResponse struct { // Check if the multiplier is live for the weekend func (c *Client) IsMultiplierActive() (bool, error) { - if c.token == "" { - return false, ErrRequireAuthentication - } - req, err := c.createRequest("GET", "weekend", nil) if err != nil { From 0e7b301f5fa8025dc9d158bb23e441eee9977ce1 Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 19 Sep 2025 14:51:32 +0700 Subject: [PATCH 05/10] feat: Reviews, not BotReviews --- bots.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bots.go b/bots.go index 7ca8e0f..75c6ddd 100644 --- a/bots.go +++ b/bots.go @@ -9,11 +9,11 @@ import ( "time" ) -type BotReviews struct { - // The bot's average review score out of 5 +type Reviews struct { + // The project's average review score out of 5 Score float64 `json:"averageScore"` - // The bot's review count + // The project's review count Count int `json:"count"` } @@ -73,7 +73,7 @@ type Bot struct { ServerCount int `json:"server_count"` // The bot's reviews - Review *BotReviews `json:"reviews"` + Review *Reviews `json:"reviews"` } type GetBotsPayload struct { From e3b752f519e603e82eed790d54d6b2f888d02c7b Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 19 Sep 2025 15:14:54 +0700 Subject: [PATCH 06/10] feat: add more constraints --- bots.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bots.go b/bots.go index 75c6ddd..f2f1a3e 100644 --- a/bots.go +++ b/bots.go @@ -153,17 +153,21 @@ func (c *Client) GetBots(filter *GetBotsPayload) (*GetBotsResult, error) { } else if filter != nil { q := req.URL.Query() - if filter.Limit > 500 { - filter.Limit = 500 - } - if filter.Limit > 0 { + if filter.Limit > 500 { + filter.Limit = 500 + } + q.Add("limit", strconv.Itoa(filter.Limit)) } else { q.Add("limit", "50") } if filter.Offset >= 0 { + if filter.Offset > 499 { + filter.Offset = 499 + } + q.Add("offset", strconv.Itoa(filter.Offset)) } From ff6ecd76ab0ff9396a228d774cbce6fe13916445 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 30 Sep 2025 15:59:05 +0700 Subject: [PATCH 07/10] revert: revert breaking changes --- bots.go | 36 +++++++--- users.go | 69 +++++++++++++----- webhook.go | 94 +++++++++++-------------- webhook_vote_test.go => webhook_test.go | 30 +++----- 4 files changed, 133 insertions(+), 96 deletions(-) rename webhook_vote_test.go => webhook_test.go (51%) diff --git a/bots.go b/bots.go index f2f1a3e..1aa448b 100644 --- a/bots.go +++ b/bots.go @@ -27,9 +27,18 @@ type Bot struct { // The username of the bot Username string `json:"username"` + // The discriminator of the bot + Discriminator string `json:"-"` + // The bot's avatar url Avatar string `json:"avatar"` + // The cdn hash of the bot's avatar if the bot has none + DefAvatar string `json:"-"` + + // The library of the bot + Library string `json:"-"` + // The prefix of the bot Prefix string `json:"prefix"` @@ -69,9 +78,18 @@ type Bot struct { // The amount of votes the bot has Points int `json:"points"` + // The GuildID for the donate bot (undocumented) (may be empty) + DonateBotGuildID string `json:"-"` + // The amount of servers the bot is in ServerCount int `json:"server_count"` + // Server affiliation ("Servers this bot is in" field) (undocumented) + GuildAffiliation []string `json:"-"` + + // The amount of servers the bot is in per shard. Always present but can be empty (undocumented) + Shards []int `json:"-"` + // The bot's reviews Review *Reviews `json:"reviews"` } @@ -85,7 +103,7 @@ type GetBotsPayload struct { // Default 0 Offset int - // [Deprecated since API v0] Field search filter + // Field search filter Search map[string]string // The field to sort by descending, valid field names are "id", "date", and "monthlyPoints". @@ -117,10 +135,10 @@ type BotStats struct { // The amount of servers the bot is in (may be empty) ServerCount int `json:"server_count"` - // [Deprecated since API v0] The amount of servers the bot is in per shard. Always present but can be empty + // The amount of servers the bot is in per shard. Always present but can be empty Shards []int `json:"shards"` - // [Deprecated since API v0] The amount of shards a bot has (may be empty) + // The amount of shards a bot has (may be empty) ShardCount int `json:"shard_count"` } @@ -132,13 +150,13 @@ type BotStatsPayload struct { // The amount of servers the bot is in, must not be zero. ServerCount int `json:"server_count"` - // [Deprecated since API v0] The amount of servers the bot is in per shard. + // The amount of servers the bot is in per shard. Shards []int `json:"shards"` - // [Deprecated since API v0] The zero-indexed id of the shard posting. Makes server_count set the shard specific server count (optional) + // The zero-indexed id of the shard posting. Makes server_count set the shard specific server count (optional) ShardID int `json:"shard_id"` - // [Deprecated since API v0] The amount of shards the bot has (optional) + // The amount of shards the bot has (optional) ShardCount int `json:"shard_count"` } @@ -288,7 +306,7 @@ func (c *Client) GetVotes(page int) ([]*User, error) { // // # Requires authentication // -// [Deprecated since API v0]: The _botID argument is no longer used. +// The _botID argument is no longer used. func (c *Client) HasUserVoted(_botID, userID string) (bool, error) { req, err := c.createRequest("GET", "bots/check", nil) @@ -327,7 +345,7 @@ func (c *Client) HasUserVoted(_botID, userID string) (bool, error) { // Information about a specific bot's stats // -// [Deprecated since API v0]: The _botID argument is no longer used. +// The _botID argument is no longer used. func (c *Client) GetBotStats(_botID string) (*BotStats, error) { req, err := c.createRequest("GET", "bots/stats", nil) @@ -362,7 +380,7 @@ func (c *Client) GetBotStats(_botID string) (*BotStats, error) { // // # Requires authentication // -// [Deprecated since API v0]: The _botID argument is no longer used. +// The _botID argument is no longer used. func (c *Client) PostBotStats(_botID string, payload *BotStatsPayload) error { if payload.ServerCount <= 0 { return ErrInvalidRequest diff --git a/users.go b/users.go index 1567bc5..3e4bc94 100644 --- a/users.go +++ b/users.go @@ -1,5 +1,7 @@ package dbl +import "encoding/json" + type User struct { // The id of the user ID string `json:"id"` @@ -7,56 +9,91 @@ type User struct { // The username of the user Username string `json:"username"` - // [Deprecated since API v0]: The discriminator of the user + // The discriminator of the user Discriminator string `json:"discriminator"` // The avatar hash of the user's avatar (may be empty) Avatar string `json:"avatar"` - // [Deprecated since API v0]: The cdn hash of the user's avatar if the user has none + // The cdn hash of the user's avatar if the user has none DefAvatar string `json:"defAvatar"` - // [Deprecated since API v0]: The bio of the user + // The bio of the user Biography string `json:"bio"` - // [Deprecated since API v0]: The banner image url of the user (may be empty) + // The banner image url of the user (may be empty) Banner string `json:"banner"` - // [Deprecated since API v0]: The user's socials + // The user's socials Social *Social `json:"social"` - // [Deprecated since API v0]: The custom hex color of the user (may be empty) + // The custom hex color of the user (may be empty) Color string `json:"color"` - // [Deprecated since API v0]: The supporter status of the user + // The supporter status of the user Supporter bool `json:"supporter"` - // [Deprecated since API v0]: The certified status of the user + // The certified status of the user CertifiedDeveloper bool `json:"certifiedDev"` - // [Deprecated since API v0]: The mod status of the user + // The mod status of the user Moderator bool `json:"mod"` - // [Deprecated since API v0]: The website moderator status of the user + // The website moderator status of the user WebsiteModerator bool `json:"webMod"` - // [Deprecated since API v0]: The admin status of the user + // The admin status of the user Admin bool `json:"admin"` } type Social struct { - // [Deprecated since API v0]: The youtube channel id of the user (may be empty) + // The youtube channel id of the user (may be empty) Youtube string `json:"youtube"` - // [Deprecated since API v0]: The reddit username of the user (may be empty) + // The reddit username of the user (may be empty) Reddit string `json:"reddit"` - // [Deprecated since API v0]: The twitter username of the user (may be empty) + // The twitter username of the user (may be empty) Twitter string `json:"twitter"` - // [Deprecated since API v0]: The instagram username of the user (may be empty) + // The instagram username of the user (may be empty) Instagram string `json:"instagram"` - // [Deprecated since API v0]: The github username of the user (may be empty) + // The github username of the user (may be empty) Github string `json:"github"` } + +// Information about a particular user +func (c *Client) GetUser(UserID string) (*User, error) { + if c.token == "" { + return nil, ErrRequireAuthentication + } + + req, err := c.createRequest("GET", "users/"+UserID, nil) + + if err != nil { + return nil, err + } + + res, err := c.httpClient.Do(req) + + if err != nil { + return nil, err + } + + body, err := c.readBody(res) + + if err != nil { + return nil, err + } + + user := &User{} + + err = json.Unmarshal(body, user) + + if err != nil { + return nil, err + } + + return user, nil +} diff --git a/webhook.go b/webhook.go index 4557bfa..e6ee21b 100644 --- a/webhook.go +++ b/webhook.go @@ -7,24 +7,23 @@ import ( "net/url" ) -type WebhookListenerFunc func([]byte) +type ListenerFunc func(*WebhookPayload) type WebhookListener struct { token string - path string - handler WebhookListenerFunc + handler ListenerFunc mux *http.ServeMux } -type WebhookVotePayload struct { - // ID of the project that received a vote - ReceiverId string +type WebhookPayload struct { + // ID of the bot that received a vote + Bot string // ID of the user who voted - VoterId string + User string - // Whether this vote is just a test done from the page settings - IsTest bool + // The type of the vote (should always be "upvote" except when using the test button it's "test") + Type string // Whether the weekend multiplier is in effect, meaning users votes count as two IsWeekend bool @@ -33,55 +32,28 @@ type WebhookVotePayload struct { Query url.Values } -type wVotePayload struct { - Bot *string `json:"bot"` - Server *string `json:"guild"` - User string `json:"user"` - Type string `json:"type"` - IsWeekend *bool `json:"isWeekend"` - Query string `json:"query"` -} - -func NewWebhookVotePayload(data []byte) (*WebhookVotePayload, error) { - p := &wVotePayload{} - - if err := json.Unmarshal(data, p); err != nil { - return nil, err - } - - m, err := url.ParseQuery(p.Query) +type wPayload struct { + // ID of the bot that received a vote + Bot string `json:"bot"` - if err != nil { - return nil, err - } - - receiverId := p.Bot - - if receiverId == nil { - receiverId = p.Server - } + // ID of the user who voted + User string `json:"user"` - isWeekend := false + // The type of the vote (should always be "upvote" except when using the test button it's "test") + Type string `json:"type"` - if p.IsWeekend != nil { - isWeekend = *p.IsWeekend - } + // Whether the weekend multiplier is in effect, meaning users votes count as two + IsWeekend bool `json:"isWeekend"` - return &WebhookVotePayload{ - ReceiverId: *receiverId, - VoterId: p.User, - IsTest: p.Type == "test", - IsWeekend: isWeekend, - Query: m, - }, nil + // Query string params found on the /bot/:ID/vote page. Example: ?a=1&b=2 + Query string `json:"query"` } // Create a new webhook listener -func NewWebhookListener(token string, path string, handler func([]byte)) *WebhookListener { +func NewListener(token string, handler func(*WebhookPayload)) *WebhookListener { return &WebhookListener{ token: token, - path: path, - handler: WebhookListenerFunc(handler), + handler: ListenerFunc(handler), } } @@ -90,12 +62,12 @@ func NewWebhookListener(token string, path string, handler func([]byte)) *Webhoo func (wl *WebhookListener) Serve(addr string) error { wl.mux = http.NewServeMux() - wl.mux.HandleFunc(wl.path, wl.handleRequest) + wl.mux.HandleFunc("/", wl.handlePayload) return http.ListenAndServe(addr, wl.mux) } -func (wl *WebhookListener) handleRequest(w http.ResponseWriter, r *http.Request) { +func (wl *WebhookListener) handlePayload(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) @@ -114,7 +86,25 @@ func (wl *WebhookListener) handleRequest(w http.ResponseWriter, r *http.Request) return } + p := &wPayload{} + + if err = json.Unmarshal(body, p); err != nil { + return + } + + m, err := url.ParseQuery(p.Query) + + if err != nil { + m = nil + } + w.WriteHeader(http.StatusNoContent) - wl.handler(body) + wl.handler(&WebhookPayload{ + Bot: p.Bot, + User: p.User, + Type: p.Type, + IsWeekend: p.IsWeekend, + Query: m, + }) } diff --git a/webhook_vote_test.go b/webhook_test.go similarity index 51% rename from webhook_vote_test.go rename to webhook_test.go index a998f10..f272ea1 100644 --- a/webhook_vote_test.go +++ b/webhook_test.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" "testing" "github.com/stretchr/testify/assert" @@ -17,45 +16,38 @@ const ( var ( testPayload = []byte(`{"bot":"441751906428256277","user":"105122038586286080","type":"upvote","isWeekend":false,"query":""}`) - testListener = NewWebhookListener(testToken, "/votes", func(payload []byte) { - vote, err := NewWebhookVotePayload(payload) - - if err != nil { - fmt.Fprintf(os.Stderr, "Error: Unable to parse webhook payload: %s", err) - return - } - - fmt.Printf("A user with the ID of %s has voted us on Top.gg!", vote.VoterId) + testListener = NewListener(testToken, func(p *WebhookPayload) { + fmt.Printf("A user with the ID of %s has voted us on Top.gg!", p.User) }) ) -func TestWebhookVoteHookMethod(t *testing.T) { +func TestHookMethod(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/votes", bytes.NewBuffer(testPayload)) + req := httptest.NewRequest(http.MethodGet, "/", bytes.NewBuffer(testPayload)) - testListener.handleRequest(rec, req) + testListener.handlePayload(rec, req) assert.Equal(t, http.StatusMethodNotAllowed, rec.Code, "GET method should not be allowed") } -func TestWebhookVoteHookAuthentication(t *testing.T) { +func TestHookAuthentication(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/votes", bytes.NewBuffer(testPayload)) + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(testPayload)) - testListener.handleRequest(rec, req) + testListener.handlePayload(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code, "Unauthorized request should not be processed") } -func TestWebhookVoteProcessing(t *testing.T) { +func TestWebhookProcessing(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/votes", bytes.NewBuffer(testPayload)) + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(testPayload)) req.Header.Set("Authorization", testToken) - testListener.handleRequest(rec, req) + testListener.handlePayload(rec, req) assert.Equal(t, http.StatusNoContent, rec.Code, "Request should succeed w/o content") } From 1bc2a4476b93039c8aaeaada99b6a9836c4c16d0 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 30 Sep 2025 16:49:18 +0700 Subject: [PATCH 08/10] doc: mark getuser as deprecated --- users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users.go b/users.go index 3e4bc94..8157a6d 100644 --- a/users.go +++ b/users.go @@ -63,7 +63,7 @@ type Social struct { Github string `json:"github"` } -// Information about a particular user +// [Deprecated] Information about a particular user func (c *Client) GetUser(UserID string) (*User, error) { if c.token == "" { return nil, ErrRequireAuthentication From 10d1947af1af604efb6e181e2400378d2ffe9b4c Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 30 Sep 2025 19:10:04 +0700 Subject: [PATCH 09/10] doc: fix documentation --- bots.go | 2 +- users.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bots.go b/bots.go index 1aa448b..efea0f1 100644 --- a/bots.go +++ b/bots.go @@ -54,7 +54,7 @@ type Bot struct { // The website url of the bot (may be empty) Website string `json:"website"` - // The support server invite code of the bot (may be empty) + // The support server url of the bot (may be empty) Support string `json:"support"` // The link to the github repo of the bot (may be empty) diff --git a/users.go b/users.go index 8157a6d..072da29 100644 --- a/users.go +++ b/users.go @@ -12,7 +12,7 @@ type User struct { // The discriminator of the user Discriminator string `json:"discriminator"` - // The avatar hash of the user's avatar (may be empty) + // The avatar url of the user (may be empty) Avatar string `json:"avatar"` // The cdn hash of the user's avatar if the user has none From 9d2ef99b4dab36c920f150f9dfc3175ccd31cb6a Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Wed, 28 Jan 2026 03:28:13 +0700 Subject: [PATCH 10/10] meta: bump license year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d160875..142ac7b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2025 RumbleFrog +Copyright (c) 2018-2026 RumbleFrog & Top.gg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal