From d05a7cf1b7fc2b29d9b228525bda725a379ad580 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sun, 9 Mar 2025 12:34:03 +0700 Subject: [PATCH 01/13] feat: adapt to Top.gg API v0 --- README.md | 11 ++--- bots.go | 120 +++++++++++++++++++++++++------------------------- errors.go | 11 ++--- go.mod | 14 ++++-- go.sum | 17 ++++--- users.go | 100 ----------------------------------------- users_test.go | 24 ---------- util.go | 14 +++--- voter.go | 12 +++++ webhook.go | 4 +- 10 files changed, 113 insertions(+), 214 deletions(-) delete mode 100644 users.go delete mode 100644 users_test.go create mode 100644 voter.go diff --git a/README.md b/README.md index 5dffc18..69f7cb9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -## Go DBL +## Go Top.gg [![Build Status](https://travis-ci.com/rumblefrog/go-dbl.svg?branch=master)](https://travis-ci.com/rumblefrog/go-dbl) [![Go Report Card](https://goreportcard.com/badge/github.com/DiscordBotList/go-dbl)](https://goreportcard.com/report/github.com/DiscordBotList/go-dbl) [![GoDoc](https://godoc.org/github.com/DiscordBotList/go-dbl?status.svg)](https://godoc.org/github.com/DiscordBotList/go-dbl) -An API wrapper for [Discord Bots](https://top.gg/) +An API wrapper for [Top.gg](https://top.gg/) Godoc is available here: https://godoc.org/github.com/DiscordBotList/go-dbl @@ -12,7 +12,7 @@ Godoc is available here: https://godoc.org/github.com/DiscordBotList/go-dbl ## Table of Contents -- [Go DBL](#go-dbl) +- [Go Top.gg](#go-topgg) - [Table of Contents](#table-of-contents) - [Guides](#guides) - [Installing](#installing) @@ -49,9 +49,10 @@ func main() { log.Fatalf("Error creating new Discord Bot List client: %s", err) } - err = dblClient.PostBotStats("botID", &dbl.BotStatsPayload{ - Shards: []int{2500}, // If non-sharded, just pass total server count as the only integer element + err = dblClient.PostBotStats(&dbl.BotStatsPayload{ + ServerCount: 2500 }) + if err != nil { log.Printf("Error sending bot stats to Discord Bot List: %s", err) } diff --git a/bots.go b/bots.go index 75ef7cf..1e24f48 100644 --- a/bots.go +++ b/bots.go @@ -8,25 +8,27 @@ import ( "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 +56,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 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 { @@ -94,7 +87,7 @@ type GetBotsPayload struct { // 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,12 +115,6 @@ 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 { @@ -135,14 +122,8 @@ type checkResponse struct { } 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"` + // The amount of servers the bot is in + ServerCount int `json:"server_count"` } // Information about different bots with an optional filter parameter @@ -159,7 +140,9 @@ 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 { @@ -181,7 +164,12 @@ func (c *Client) GetBots(filter *GetBotsPayload) (*GetBotsResult, error) { } 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 { @@ -255,10 +243,10 @@ func (c *Client) GetBot(botID string) (*Bot, error) { // Use this endpoint to see who have upvoted your bot // -// Requires authentication +// # Requires authentication // // 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) { +func (c *Client) GetVotes(page int) ([]*Voter, error) { if c.token == "" { return nil, ErrRequireAuthentication } @@ -267,12 +255,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", "bots/votes", 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 +283,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. // // 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 +306,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,8 +341,8 @@ 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 stats +func (c *Client) GetBotStats() (*BotStats, error) { if c.token == "" { return nil, ErrRequireAuthentication } @@ -353,7 +351,7 @@ func (c *Client) GetBotStats(botID string) (*BotStats, error) { return nil, ErrLocalRatelimit } - req, err := c.createRequest("GET", "bots/"+botID+"/stats", nil) + req, err := c.createRequest("GET", "bots/stats", nil) if err != nil { return nil, err @@ -384,10 +382,8 @@ func (c *Client) GetBotStats(botID string) (*BotStats, error) { // Post your bot's stats // -// Requires authentication -// -// 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) PostBotStats(payload *BotStatsPayload) error { if c.token == "" { return ErrRequireAuthentication } @@ -396,13 +392,17 @@ func (c *Client) PostBotStats(botID string, payload *BotStatsPayload) error { return ErrLocalRatelimit } + if payload.ServerCount <= 0 { + return ErrInvalidRequest + } + encoded, err := json.Marshal(payload) 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 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..768a7a0 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,16 @@ module github.com/top-gg/go-dbl -go 1.15 +go 1.23.0 + +toolchain go1.24.1 + +require ( + github.com/stretchr/testify v1.10.0 + golang.org/x/time v0.11.0 +) require ( - github.com/stretchr/testify v1.5.1 - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fc557f0..eec0319 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,12 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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..45d560b 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 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..bff7743 100644 --- a/webhook.go +++ b/webhook.go @@ -2,7 +2,7 @@ package dbl import ( "encoding/json" - "io/ioutil" + "io" "net/http" "net/url" ) @@ -80,7 +80,7 @@ func (wl *WebhookListener) handlePayload(w http.ResponseWriter, r *http.Request) return } - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) if err != nil { return From ca5b8336b9b4b40a3e938b48e48561a4ee3d9d20 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sun, 9 Mar 2025 12:35:34 +0700 Subject: [PATCH 02/13] meta: bump license year --- 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 ab84290e017781395fe2a6e6b090888821caafee Mon Sep 17 00:00:00 2001 From: null8626 Date: Sun, 9 Mar 2025 12:39:55 +0700 Subject: [PATCH 03/13] fix: update outdated URLs --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 69f7cb9..294df70 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ ## Go Top.gg -[![Build Status](https://travis-ci.com/rumblefrog/go-dbl.svg?branch=master)](https://travis-ci.com/rumblefrog/go-dbl) -[![Go Report Card](https://goreportcard.com/badge/github.com/DiscordBotList/go-dbl)](https://goreportcard.com/report/github.com/DiscordBotList/go-dbl) -[![GoDoc](https://godoc.org/github.com/DiscordBotList/go-dbl?status.svg)](https://godoc.org/github.com/DiscordBotList/go-dbl) +[![Go Report Card](https://goreportcard.com/badge/github.com/top-gg/go-dbl)](https://goreportcard.com/report/github.com/top-gg/go-dbl) +[![GoDoc](https://godoc.org/github.com/top-gg/go-dbl?status.svg)](https://godoc.org/github.com/top-gg/go-dbl) An API wrapper for [Top.gg](https://top.gg/) -Godoc is available here: https://godoc.org/github.com/DiscordBotList/go-dbl +Godoc is available here: https://godoc.org/github.com/top-gg/go-dbl From 5dbc22f540573687e7054e677e33bb5a1f4eaac0 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Sun, 9 Mar 2025 15:40:41 +0700 Subject: [PATCH 04/13] doc: add (may be empty) to vanity --- bots.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bots.go b/bots.go index 1e24f48..e8c3aa5 100644 --- a/bots.go +++ b/bots.go @@ -59,7 +59,7 @@ type Bot struct { // The date when the bot was submitted Date time.Time `json:"date"` - // The vanity url of the bot + // The vanity url of the bot (may be empty) Vanity string `json:"vanity"` // The monthly amount of votes the bot has From fdcdae2527dd2703dc7afce2aea0622c8dcd987c Mon Sep 17 00:00:00 2001 From: null8626 Date: Sun, 6 Apr 2025 13:12:59 +0700 Subject: [PATCH 05/13] feat: add autoposter --- bots.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/bots.go b/bots.go index e8c3aa5..3ef7c9e 100644 --- a/bots.go +++ b/bots.go @@ -117,13 +117,16 @@ type BotStats struct { ServerCount int `json:"server_count"` } -type checkResponse struct { - Voted int `json:"voted"` +type Autoposter struct { + stopChannel chan bool + // The channel on which errors are delivered after every attempted post API request. + Posted chan error } -type BotStatsPayload struct { - // The amount of servers the bot is in - ServerCount int `json:"server_count"` +type AutoposterCallback func() *BotStats + +type checkResponse struct { + Voted int `json:"voted"` } // Information about different bots with an optional filter parameter @@ -383,7 +386,7 @@ func (c *Client) GetBotStats() (*BotStats, error) { // Post your bot's stats // // # Requires authentication -func (c *Client) PostBotStats(payload *BotStatsPayload) error { +func (c *Client) PostBotStats(payload *BotStats) error { if c.token == "" { return ErrRequireAuthentication } @@ -423,3 +426,44 @@ func (c *Client) PostBotStats(payload *BotStatsPayload) error { return nil } + +// Automates your bot's stats posting +// +// # Requires authentication +func (c *Client) StartAutoposter(delay int, callback AutoposterCallback) (*Autoposter, error) { + if c.token == "" { + return nil, ErrRequireAuthentication + } + + if delay < 900 { + delay = 900 + } + + stopChannel := make(chan bool) + postedChannel := make(chan error) + + go func() { + ticker := time.NewTicker(time.Duration(delay) * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-stopChannel: + close(postedChannel) + return + case <-ticker.C: + postedChannel <- c.PostBotStats(callback()) + } + } + }() + + return &Autoposter{ + stopChannel: stopChannel, + Posted: postedChannel, + }, nil +} + +// Stops the autoposter +func (a *Autoposter) Stop() { + a.stopChannel <- true +} From 48653e8aacb833f6bd03aff399a13e67bc69849b Mon Sep 17 00:00:00 2001 From: null8626 Date: Sun, 6 Apr 2025 13:15:05 +0700 Subject: [PATCH 06/13] feat: BotStatsPayload is now just BotStats --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 294df70..8e83d72 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ func main() { log.Fatalf("Error creating new Discord Bot List client: %s", err) } - err = dblClient.PostBotStats(&dbl.BotStatsPayload{ + err = dblClient.PostBotStats(&dbl.BotStats{ ServerCount: 2500 }) From 88fb731786a45c9185a51ea01bca7d0ecf0fc24f Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 18 Apr 2025 18:16:18 +0700 Subject: [PATCH 07/13] fix: add default limit --- bots.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bots.go b/bots.go index 3ef7c9e..f55d67b 100644 --- a/bots.go +++ b/bots.go @@ -149,7 +149,9 @@ func (c *Client) GetBots(filter *GetBotsPayload) (*GetBotsResult, error) { q := req.URL.Query() if filter.Limit != 0 { - q.Add("limit", strconv.Itoa(filter.Limit)) + q.Add("limit", strconv.Itoa(max(filter.Limit, 500))) + } else { + q.Add("limit", "50") } if filter.Offset != 0 { From e2bd1da5e4f667551a74f99069d500a5ebaf6681 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 16 Jun 2025 17:38:41 +0700 Subject: [PATCH 08/13] feat: fully adapt to v0 --- bots.go | 26 +++++++------------------- bots_test.go | 5 +++++ client.go | 31 ++++++++++++++++++++++++++++--- client_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- go.mod | 2 +- go.sum | 4 ++-- weekend_test.go | 3 ++- 7 files changed, 91 insertions(+), 29 deletions(-) diff --git a/bots.go b/bots.go index f55d67b..ab653dd 100644 --- a/bots.go +++ b/bots.go @@ -3,6 +3,7 @@ package dbl import ( "bytes" "encoding/json" + "fmt" "strconv" "strings" "time" @@ -84,9 +85,6 @@ type GetBotsPayload struct { // Default 0 Offset int - // Field search filter - Search map[string]string - // The field to sort by descending, valid field names are "id", "date", and "monthlyPoints". Sort string @@ -148,26 +146,16 @@ func (c *Client) GetBots(filter *GetBotsPayload) (*GetBotsResult, error) { } else if filter != nil { q := req.URL.Query() - if filter.Limit != 0 { - q.Add("limit", strconv.Itoa(max(filter.Limit, 500))) + 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 != "" { switch filter.Sort { case "id", "date", "monthlyPoints": @@ -264,7 +252,7 @@ func (c *Client) GetVotes(page int) ([]*Voter, error) { return nil, ErrInvalidRequest } - req, err := c.createRequest("GET", "bots/votes", nil) + req, err := c.createRequest("GET", fmt.Sprintf("bots/%s/votes", c.id), nil) if err != nil { return nil, err @@ -437,8 +425,8 @@ func (c *Client) StartAutoposter(delay int, callback AutoposterCallback) (*Autop return nil, ErrRequireAuthentication } - if delay < 900 { - delay = 900 + if delay < 900000 { + delay = 900000 } stopChannel := make(chan bool) diff --git a/bots_test.go b/bots_test.go index 924db3d..1f36d5d 100644 --- a/bots_test.go +++ b/bots_test.go @@ -1,6 +1,7 @@ package dbl import ( + "log" "os" "testing" @@ -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") 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..69a4d25 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,54 @@ func TestNewClient(t *testing.T) { httpClient := &http.Client{} - _, err := NewClient( - "token", + client, err := NewClient( + os.Getenv("apikey"), 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) } - // ... + _, err = client.GetBotStats() + + assert.Nil(t, err, "GetBotStats should succeed") + + err = client.PostBotStats(&BotStats{ + ServerCount: 2, + }) + + assert.Nil(t, err, "PostBotStats 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.GetVotes(1) + + assert.Nil(t, err, "GetVotes should succeed") + + time.Sleep(1 * time.Second) + _, err = client.HasUserVoted("661200758510977084") + + 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/go.mod b/go.mod index 768a7a0..3bc8905 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.1 require ( github.com/stretchr/testify v1.10.0 - golang.org/x/time v0.11.0 + golang.org/x/time v0.12.0 ) require ( diff --git a/go.sum b/go.sum index eec0319..bb90ab4 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/weekend_test.go b/weekend_test.go index 2950f5e..96cad97 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("apikey")) assert.Nil(t, err, "Client should be created w/o error") From 21e0d2a42caf91cd4820e24bd04e6dfa29ea9943 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 17 Jun 2025 16:16:51 +0700 Subject: [PATCH 09/13] feat: add widgets --- const.go | 2 +- widget.go | 6 +++ widget_general.go | 46 ------------------- widget_large.go | 80 -------------------------------- widget_large_test.go | 32 ------------- widget_small.go | 107 ------------------------------------------- widget_small_test.go | 33 ------------- 7 files changed, 7 insertions(+), 299 deletions(-) create mode 100644 widget.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/const.go b/const.go index 474a4e2..2aec960 100644 --- a/const.go +++ b/const.go @@ -1,5 +1,5 @@ package dbl const ( - BaseURL = "https://top.gg/api/" + BaseURL = "https://top.gg/api/v1/" ) diff --git a/widget.go b/widget.go new file mode 100644 index 0000000..cd0f454 --- /dev/null +++ b/widget.go @@ -0,0 +1,6 @@ +package dbl + +// Generates a large widget URL. +func LargeWidget(id string) string { + return BaseURL + "widgets/large/" + id +} 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 8d61471f7b8a2093c1abd59bcf392347d809747f Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 18 Jun 2025 20:50:40 +0700 Subject: [PATCH 10/13] feat: add small widgets --- README.md | 2 +- widget.go | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8e83d72..2b22977 100644 --- a/README.md +++ b/README.md @@ -131,4 +131,4 @@ func handleVote(payload *dbl.WebhookPayload) { ### More details -For more details, Godoc and tests are available +For more details, Godoc and tests are available. \ No newline at end of file diff --git a/widget.go b/widget.go index cd0f454..98ec07f 100644 --- a/widget.go +++ b/widget.go @@ -1,6 +1,28 @@ package dbl +type WidgetType string + +const ( + DiscordBotWidget WidgetType = "discord/bot" + DiscordServerWidget WidgetType = "discord/server" +) + // Generates a large widget URL. -func LargeWidget(id string) string { - return BaseURL + "widgets/large/" + id +func LargeWidget(ty WidgetType, id string) string { + return BaseURL + "widgets/large/" + string(ty) + "/" + id +} + +// Generates a small widget URL for displaying votes. +func VotesWidget(ty WidgetType, id string) string { + return BaseURL + "widgets/small/votes/" + string(ty) + "/" + id +} + +// Generates a small widget URL for displaying an entity's owner. +func OwnerWidget(ty WidgetType, id string) string { + return BaseURL + "widgets/small/owner/" + string(ty) + "/" + id +} + +// Generates a small widget URL for displaying social stats. +func SocialWidget(ty WidgetType, id string) string { + return BaseURL + "widgets/small/social/" + string(ty) + "/" + id } From f6f6ab685a8684fec37636d5a723d88376a7b277 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 23 Jun 2025 23:40:51 +0700 Subject: [PATCH 11/13] docs: readme overhaul --- README.md | 259 ++++++++++++++++++++++++++++--------------- bots.go | 40 ++++--- bots_test.go | 4 +- client_test.go | 18 ++- webhook.go | 94 +++++++++------- webhook_test.go | 50 --------- webhook_vote_test.go | 61 ++++++++++ weekend_test.go | 2 +- 8 files changed, 314 insertions(+), 214 deletions(-) delete mode 100644 webhook_test.go create mode 100644 webhook_vote_test.go diff --git a/README.md b/README.md index 2b22977..e2afeee 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,217 @@ -## Go Top.gg +# Top.gg Go SDK -[![Go Report Card](https://goreportcard.com/badge/github.com/top-gg/go-dbl)](https://goreportcard.com/report/github.com/top-gg/go-dbl) -[![GoDoc](https://godoc.org/github.com/top-gg/go-dbl?status.svg)](https://godoc.org/github.com/top-gg/go-dbl) +The community-maintained Go library for Top.gg. -An API wrapper for [Top.gg](https://top.gg/) +## Installation -Godoc is available here: https://godoc.org/github.com/top-gg/go-dbl +```sh +$ go get -u github.com/top-gg/go-dbl +``` + +## Setting up - - -## Table of Contents +### With defaults -- [Go Top.gg](#go-topgg) -- [Table of Contents](#table-of-contents) -- [Guides](#guides) - - [Installing](#installing) - - [Posting Stats](#posting-stats) - - [Setting options](#setting-options) - - [Ratelimits](#ratelimits) - - [Webhook](#webhook) - - [More details](#more-details) +```go +client, err := dbl.NewClient(os.Getenv("TOPGG_TOKEN")) + +if err != nil { + log.Fatalf("Error creating new Top.gg client: %s", err) +} +``` - +### With explicit options -## Guides +```go +clientTimeout := 5 * time.Second +httpClient := &http.Client{} -### Installing +client, err := dbl.NewClient( + os.Getenv("TOPGG_TOKEN"), + dbl.HTTPClientOption(httpClient), + dbl.TimeoutOption(clientTimeout), +) -```bash -go get -u github.com/top-gg/go-dbl +if err != nil { + log.Fatalf("Error creating new Top.gg client: %s", err) +} ``` -### Posting Stats +## Usage + +### Getting a bot ```go -package main +bot, err := client.GetBot("574652751745777665") -import ( - "log" +if err != nil { + log.Fatalf("Unable to get a bot: %s", err) +} +``` - "github.com/top-gg/go-dbl" -) +### Getting several bots -func main() { - dblClient, err := dbl.NewClient("token") - if err != nil { - log.Fatalf("Error creating new Discord Bot List client: %s", err) - } - - err = dblClient.PostBotStats(&dbl.BotStats{ - ServerCount: 2500 - }) - - if err != nil { - log.Printf("Error sending bot stats to Discord Bot List: %s", err) - } - - // ... +```go +bots, err := client.GetBots(&GetBotsPayload{ + Limit: 20, +}) + +if err != nil { + log.Fatalf("Unable to get several bots: %s", err) } ``` -### Setting options +### Getting your bot's voters + +#### First page ```go -package main +firstPageVoters, err := client.GetVoters(1) -import ( - "log" - "net/http" - "time" +if err != nil { + log.Fatalf("Unable to get voters: %s", err) +} +``` - "github.com/top-gg/go-dbl" -) +#### Subsequent pages + +```go +secondPageVoters, err := client.GetVoters(2) -const clientTimeout = 5 * time.Second +if err != nil { + log.Fatalf("Unable to get voters: %s", err) +} +``` -func main() { - httpClient := &http.Client{} - - dblClient, err := dbl.NewClient( - "token", - dbl.HTTPClientOption(httpClient), // Setting a custom HTTP client. Default is *http.Client with default timeout. - dbl.TimeoutOption(clientTimeout), // Setting timeout option. Default is 3 seconds - ) - if err != nil { - log.Fatalf("Error creating new Discord Bot List client: %s", err) - } - - // ... +### Check if a user has voted for your bot + +```go +has_voted, err := client.HasUserVoted("661200758510977084") + +if err != nil { + log.Fatalf("Unable to check if a user has voted: %s", err) } ``` -### Ratelimits +### Getting your bot's server count + +```go +serverCount, err := client.GetServerCount() + +if err != nil { + log.Fatalf("Unable to get server count: %s", err) +} +``` -There's a local token bucket rate limiter, allowing for 60 requests a minute (single/burst) +### Posting your bot's server count -Upon reaching the local rate limit, `ErrLocalRatelimit` error will be returned +```go +err := client.PostServerCount(bot.GetServerCount()) -If remote rate limit is exceeded, `ErrRemoteRatelimit` error will be returned and `RetryAfter` in client fields will be updated with the retry time +if err != nil { + log.Fatalf("Unable to post server count: %s", err) +} +``` -### Webhook +### Automatically posting your bot's server count every few minutes + +```go +// Posts once every 30 minutes +autoposter, err := client.StartAutoposter(1800000, func() int { + return bot.GetServerCount() +}) + +if err != nil { + log.Fatalf("Unable to start autoposter: %s", err) +} + +go func() { + for { + post_err := <-autoposter.Posted + + if post_err != nil { + log.Fatalf("Unable to post server count: %s", post_err) + } + } +}() + +// ... + +autoposter.Stop() // Optional +``` + +### Checking if the weekend vote multiplier is active + +```go +multiplierActive, err := client.IsMultiplierActive() + +if err != nil { + log.Fatalf("Unable to check weekend vote multiplier: %s", err) +} +``` + +### Generating widget URLs + +#### Large + +```go +widgetUrl := dbl.LargeWidget(dbl.DiscordBotWidget, "574652751745777665") +``` + +#### Votes + +```go +widgetUrl := dbl.VotesWidget(dbl.DiscordBotWidget, "574652751745777665") +``` + +#### Owner + +```go +widgetUrl := dbl.OwnerWidget(dbl.DiscordBotWidget, "574652751745777665") +``` + +#### Social + +```go +widgetUrl := dbl.SocialWidget(dbl.DiscordBotWidget, "574652751745777665") +``` + +### Webhooks + +#### Being notified whenever someone voted for your bot ```go package main import ( - "errors" - "log" - "net/http" + "errors" + "fmt" + "log" + "os" + "net/http" - "github.com/top-gg/go-dbl" + "github.com/top-gg/go-dbl" ) -const listenerPort = ":9090" - func main() { - listener := dbl.NewListener("token", handleVote) + listener := dbl.NewWebhookListener(os.Getenv("MY_TOPGG_WEBHOOK_SECRET"), "/votes", handleVote) - // Serve is a blocking call - err := listener.Serve(listenerPort) - if !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("HTTP server error: %s", err) - } -} + // Serve is a blocking call + err := listener.Serve(":8080") -func handleVote(payload *dbl.WebhookPayload) { - // perform on payload + if !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("HTTP server error: %s", err) + } } -``` -### More details +func handleVote(payload []byte) { + vote, err := dbl.NewWebhookVotePayload(payload) -For more details, Godoc and tests are available. \ No newline at end of file + 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) +} +``` \ No newline at end of file diff --git a/bots.go b/bots.go index ab653dd..6ff1669 100644 --- a/bots.go +++ b/bots.go @@ -121,7 +121,7 @@ type Autoposter struct { Posted chan error } -type AutoposterCallback func() *BotStats +type AutoposterCallback func() int type checkResponse struct { Voted int `json:"voted"` @@ -234,12 +234,12 @@ func (c *Client) GetBot(botID string) (*Bot, error) { return bot, nil } -// Use this endpoint to see who have upvoted your bot +// Fetches your bot's recent 100 unique voters // // # Requires authentication // // IF YOU HAVE OVER 1000 VOTES PER MONTH YOU HAVE TO USE THE WEBHOOKS AND CAN NOT USE THIS -func (c *Client) GetVotes(page int) ([]*Voter, error) { +func (c *Client) GetVoters(page int) ([]*Voter, error) { if c.token == "" { return nil, ErrRequireAuthentication } @@ -334,49 +334,45 @@ func (c *Client) HasUserVoted(userID string) (bool, error) { return cr.Voted == 1, nil } -// Information about your bot's stats -func (c *Client) GetBotStats() (*BotStats, error) { +// Information about your bot's server count +func (c *Client) GetServerCount() (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/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 +// Post your bot's server count // // # Requires authentication -func (c *Client) PostBotStats(payload *BotStats) error { +func (c *Client) PostServerCount(serverCount int) error { if c.token == "" { return ErrRequireAuthentication } @@ -385,11 +381,13 @@ func (c *Client) PostBotStats(payload *BotStats) error { return ErrLocalRatelimit } - if payload.ServerCount <= 0 { + if serverCount <= 0 { return ErrInvalidRequest } - encoded, err := json.Marshal(payload) + encoded, err := json.Marshal(&BotStats{ + ServerCount: serverCount, + }) if err != nil { return err @@ -417,7 +415,7 @@ func (c *Client) PostBotStats(payload *BotStats) error { return nil } -// Automates your bot's stats posting +// Automates your bot's server count posting // // # Requires authentication func (c *Client) StartAutoposter(delay int, callback AutoposterCallback) (*Autoposter, error) { @@ -442,7 +440,7 @@ func (c *Client) StartAutoposter(delay int, callback AutoposterCallback) (*Autop close(postedChannel) return case <-ticker.C: - postedChannel <- c.PostBotStats(callback()) + postedChannel <- c.PostServerCount(callback()) } } }() diff --git a/bots_test.go b/bots_test.go index 1f36d5d..b8b0181 100644 --- a/bots_test.go +++ b/bots_test.go @@ -14,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") @@ -34,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_test.go b/client_test.go index 69a4d25..96d6421 100644 --- a/client_test.go +++ b/client_test.go @@ -16,24 +16,22 @@ func TestNewClient(t *testing.T) { httpClient := &http.Client{} client, err := NewClient( - os.Getenv("apikey"), + 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.GetBotStats() + _, err = client.GetServerCount() - assert.Nil(t, err, "GetBotStats should succeed") + assert.Nil(t, err, "GetServerCount should succeed") - err = client.PostBotStats(&BotStats{ - ServerCount: 2, - }) + err = client.PostServerCount(2) - assert.Nil(t, err, "PostBotStats should succeed") + assert.Nil(t, err, "PostServerCount should succeed") time.Sleep(1 * time.Second) _, err = client.GetBot("264811613708746752") @@ -52,9 +50,9 @@ func TestNewClient(t *testing.T) { assert.Nil(t, err, "GetBots should succeed") time.Sleep(1 * time.Second) - _, err = client.GetVotes(1) + _, err = client.GetVoters(1) - assert.Nil(t, err, "GetVotes should succeed") + assert.Nil(t, err, "GetVoters should succeed") time.Sleep(1 * time.Second) _, err = client.HasUserVoted("661200758510977084") diff --git a/webhook.go b/webhook.go index bff7743..1c89619 100644 --- a/webhook.go +++ b/webhook.go @@ -7,23 +7,24 @@ import ( "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 bot/server 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) @@ -86,25 +114,7 @@ func (wl *WebhookListener) handlePayload(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 { - return - } - 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..a998f10 --- /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", 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 96cad97..3c4eac7 100644 --- a/weekend_test.go +++ b/weekend_test.go @@ -8,7 +8,7 @@ import ( ) func TestWeekend(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") From d59123ba140d3d063612947312912c92ae9a8dca Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 19 Jul 2025 19:50:06 +0700 Subject: [PATCH 12/13] doc: add chapters --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index e2afeee..d97c67e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,23 @@ The community-maintained Go library for Top.gg. +## Chapters + +- [Installation](#installation) +- [Setting up](#setting-up) +- [Usage](#usage) + - [Getting a bot](#getting-a-bot) + - [Getting several bots](#getting-several-bots) + - [Getting your bot's voters](#getting-your-bots-voters) + - [Check if a user has voted for your bot](#check-if-a-user-has-voted-for-your-bot) + - [Getting your bot's server count](#getting-your-bots-server-count) + - [Posting your bot's server count](#posting-your-bots-server-count) + - [Automatically posting your bot's server count every few minutes](#automatically-posting-your-bots-server-count-every-few-minutes) + - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) + - [Generating widget URLs](#generating-widget-urls) + - [Webhooks](#webhooks) + - [Being notified whenever someone voted for your bot](#being-notified-whenever-someone-voted-for-your-bot) + ## Installation ```sh From 6262ae25e49c0e526f911413b872eb8412f164d8 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 16 Sep 2025 16:50:34 +0700 Subject: [PATCH 13/13] feat: change method names in anticipation of v1 --- autoposter_test.go | 27 +++++++++++++++++++++++++++ bots.go | 14 ++++++-------- client_test.go | 10 +++++----- const.go | 2 +- util.go | 2 +- webhook.go | 2 +- webhook_vote_test.go | 2 +- widget.go | 10 +++++----- 8 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 autoposter_test.go diff --git a/autoposter_test.go b/autoposter_test.go new file mode 100644 index 0000000..17110cf --- /dev/null +++ b/autoposter_test.go @@ -0,0 +1,27 @@ +package dbl + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAutoposter(t *testing.T) { + client, err := NewClient(os.Getenv("TOPGG_TOKEN")) + + assert.Nil(t, err, "Client should be created w/o error") + + autoposter, err := client.StartAutoposter(900000, func() int { + return 2 + }) + + assert.Nil(t, err, "Autoposter should be created w/o error") + + for i := 0; i < 3; i++ { + err := <-autoposter.Posted + assert.Nil(t, err, "Posting should not error") + } + + autoposter.Stop() +} diff --git a/bots.go b/bots.go index 6ff1669..f9f06fa 100644 --- a/bots.go +++ b/bots.go @@ -234,11 +234,9 @@ func (c *Client) GetBot(botID string) (*Bot, error) { return bot, nil } -// Fetches your bot's recent 100 unique voters +// Fetches your project's recent 100 unique voters // // # Requires authentication -// -// IF YOU HAVE OVER 1000 VOTES PER MONTH YOU HAVE TO USE THE WEBHOOKS AND CAN NOT USE THIS func (c *Client) GetVoters(page int) ([]*Voter, error) { if c.token == "" { return nil, ErrRequireAuthentication @@ -287,7 +285,7 @@ func (c *Client) GetVoters(page int) ([]*Voter, error) { 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(userID string) (bool, error) { @@ -335,7 +333,7 @@ func (c *Client) HasUserVoted(userID string) (bool, error) { } // Information about your bot's server count -func (c *Client) GetServerCount() (int, error) { +func (c *Client) getBotServerCount() (int, error) { if c.token == "" { return 0, ErrRequireAuthentication } @@ -372,7 +370,7 @@ func (c *Client) GetServerCount() (int, error) { // Post your bot's server count // // # Requires authentication -func (c *Client) PostServerCount(serverCount int) error { +func (c *Client) postBotServerCount(serverCount int) error { if c.token == "" { return ErrRequireAuthentication } @@ -399,7 +397,7 @@ func (c *Client) PostServerCount(serverCount int) error { 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) @@ -440,7 +438,7 @@ func (c *Client) StartAutoposter(delay int, callback AutoposterCallback) (*Autop close(postedChannel) return case <-ticker.C: - postedChannel <- c.PostServerCount(callback()) + postedChannel <- c.postBotServerCount(callback()) } } }() diff --git a/client_test.go b/client_test.go index 96d6421..5631c94 100644 --- a/client_test.go +++ b/client_test.go @@ -25,13 +25,13 @@ func TestNewClient(t *testing.T) { log.Fatalf("Error creating new Top.gg client: %s", err) } - _, err = client.GetServerCount() + _, err = client.getBotServerCount() - assert.Nil(t, err, "GetServerCount should succeed") + assert.Nil(t, err, "getBotServerCount should succeed") - err = client.PostServerCount(2) + err = client.postBotServerCount(2) - assert.Nil(t, err, "PostServerCount should succeed") + assert.Nil(t, err, "postBotServerCount should succeed") time.Sleep(1 * time.Second) _, err = client.GetBot("264811613708746752") @@ -55,7 +55,7 @@ func TestNewClient(t *testing.T) { assert.Nil(t, err, "GetVoters should succeed") time.Sleep(1 * time.Second) - _, err = client.HasUserVoted("661200758510977084") + _, err = client.HasUserVoted("8226924471638491136") assert.Nil(t, err, "HasUserVoted should succeed") diff --git a/const.go b/const.go index 2aec960..474a4e2 100644 --- a/const.go +++ b/const.go @@ -1,5 +1,5 @@ package dbl const ( - BaseURL = "https://top.gg/api/v1/" + BaseURL = "https://top.gg/api/" ) diff --git a/util.go b/util.go index 45d560b..4adf8af 100644 --- a/util.go +++ b/util.go @@ -56,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/webhook.go b/webhook.go index 1c89619..4557bfa 100644 --- a/webhook.go +++ b/webhook.go @@ -17,7 +17,7 @@ type WebhookListener struct { } type WebhookVotePayload struct { - // ID of the bot/server that received a vote + // ID of the project that received a vote ReceiverId string // ID of the user who voted diff --git a/webhook_vote_test.go b/webhook_vote_test.go index a998f10..f2aa075 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", testToken) + req.Header.Set("Authorization", "Bearer "+testToken) testListener.handleRequest(rec, req) diff --git a/widget.go b/widget.go index 98ec07f..a2554ad 100644 --- a/widget.go +++ b/widget.go @@ -9,20 +9,20 @@ const ( // Generates a large widget URL. func LargeWidget(ty WidgetType, id string) string { - return BaseURL + "widgets/large/" + string(ty) + "/" + id + return BaseURL + "v1/widgets/large/" + string(ty) + "/" + id } // Generates a small widget URL for displaying votes. func VotesWidget(ty WidgetType, id string) string { - return BaseURL + "widgets/small/votes/" + string(ty) + "/" + id + return BaseURL + "v1/widgets/small/votes/" + string(ty) + "/" + id } -// Generates a small widget URL for displaying an entity's owner. +// Generates a small widget URL for displaying a project's owner. func OwnerWidget(ty WidgetType, id string) string { - return BaseURL + "widgets/small/owner/" + string(ty) + "/" + id + return BaseURL + "v1/widgets/small/owner/" + string(ty) + "/" + id } // Generates a small widget URL for displaying social stats. func SocialWidget(ty WidgetType, id string) string { - return BaseURL + "widgets/small/social/" + string(ty) + "/" + id + return BaseURL + "v1/widgets/small/social/" + string(ty) + "/" + id }