topdiscordlist

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: MIT Imports: 15 Imported by: 0

README

Top Discord List SDK for Go

Hand out rewards when someone votes for your Discord server or bot on Top Discord List.

go get github.com/TopDiscordLists/go-sdk

Go 1.23 or newer. The API client and webhook verification use nothing but the standard library; the vote stream pulls in github.com/coder/websocket, since Go has no WebSocket client of its own.

Listen for votes

Your bot dials out, so this needs no public URL.

listener := topdiscordlist.NewListener(os.Getenv("TDL_TOKEN"))

err := listener.Run(context.Background(), func(vote topdiscordlist.VoteEvent) {
    log.Printf("%s voted, now at %d", vote.Username, vote.VoteCount)
    giveReward(vote.UserID)
})

Run blocks and reconnects on its own with backoff. Cancel the context to stop.

Receive webhooks

http.Handle("/vote", topdiscordlist.WebhookHandler(secret, func(p topdiscordlist.Payload) {
    if p.Vote.IsTest {
        return
    }
    giveReward(p.User.DiscordID)
}))
http.ListenAndServe(":3000", nil)

The handler checks the signature, replies 200, and only then runs your function, so a slow reward never turns into a retry.

Verifying by hand, if you already have a router you like:

ok := topdiscordlist.VerifySignature(secret, r.Header.Get("X-TDL-Signature"),
    string(rawBody), topdiscordlist.DefaultTolerance)

Read the body before anything unmarshals it. Re-serialized JSON will not match.

Call the API

client := topdiscordlist.New(os.Getenv("TDL_TOKEN"))
ctx := context.Background()

listing, err := client.Listing(ctx)
check, err := client.HasVoted(ctx, "123456789012345678")
voters, err := client.Votes(ctx, 50, 1)

err = client.PostStats(ctx, "my-bot", topdiscordlist.Stats{ServerCount: 1200})

Errors from the API come back as *topdiscordlist.APIError with the status on it:

var apiErr *topdiscordlist.APIError
if errors.As(err, &apiErr) && apiErr.Status == 403 {
    log.Fatal("token was revoked")
}

Test events

A test delivery is identical to a real one except Event is "test" and Vote.IsTest is true. Branch on it, or you will hand yourself free rewards every time you press the button.

Contributing

go run ./conformance checks this package against the shared signature vectors, the same eight cases every other language SDK has to pass. CI runs it on every push. If the signature logic breaks, people quietly stop receiving votes, which is why that gate exists.

MIT licensed.

Documentation

Overview

Package topdiscordlist talks to the Top Discord List developer API and verifies the vote webhooks it sends you.

Index

Constants

View Source
const (
	DefaultBaseURL   = "https://topdiscordlist.com/api"
	SignatureHeader  = "X-TDL-Signature"
	EventHeader      = "X-TDL-Event"
	DeliveryHeader   = "X-TDL-Delivery"
	DefaultTolerance = 5 * time.Minute
)

Variables

This section is empty.

Functions

func Sign

func Sign(secret string, timestamp int64, body string) string

Sign returns the hex HMAC-SHA256 of "{timestamp}.{body}".

func VerifySignature

func VerifySignature(secret, header, body string, tolerance time.Duration) bool

VerifySignature checks an X-TDL-Signature header against the raw request body. Pass a tolerance of 0 to skip the freshness check.

func VerifySignatureAt

func VerifySignatureAt(secret, header, body string, tolerance time.Duration, now time.Time) bool

VerifySignatureAt is VerifySignature with the current time supplied by the caller, which makes replaying a stored delivery in tests straightforward.

func WebhookHandler

func WebhookHandler(secret string, fn func(Payload)) http.HandlerFunc

WebhookHandler verifies each delivery and hands the payload to fn. It replies before fn runs, so a slow reward never causes a retry.

Types

type APIError

type APIError struct {
	Status  int
	Message string
}

APIError is returned when the API responds with a non 2xx status.

func (*APIError) Error

func (e *APIError) Error() string

type Client

type Client struct {
	Token   string
	BaseURL string
	HTTP    *http.Client
}

Client calls the developer API with your listing token.

func New

func New(token string) *Client

New returns a client for the given listing token.

func (*Client) HasVoted

func (c *Client) HasVoted(ctx context.Context, discordID string) (*VoteCheck, error)

HasVoted reports whether that Discord user's vote is still active.

func (*Client) HasVotedByUserID

func (c *Client) HasVotedByUserID(ctx context.Context, userID string) (*VoteCheck, error)

HasVotedByUserID is the same check, keyed by Top Discord List user id.

func (*Client) Listing

func (c *Client) Listing(ctx context.Context) (*Listing, error)

Listing returns your listing's name, slug, and vote count.

func (*Client) PostStats

func (c *Client) PostStats(ctx context.Context, slug string, stats Stats) error

PostStats reports your bot's own server, user, and shard counts.

func (*Client) Votes

func (c *Client) Votes(ctx context.Context, limit, page int) ([]Voter, error)

Votes lists your most recent voters, newest first.

type Listener

type Listener struct {
	Token      string
	BaseURL    string
	MaxBackoff time.Duration
}

Listener streams votes over a WebSocket. Your bot connects out to us, so it works fine without a public URL.

func NewListener

func NewListener(token string) *Listener

NewListener returns a listener for the given listing token.

func (*Listener) Run

func (l *Listener) Run(ctx context.Context, fn func(VoteEvent)) error

Run connects and calls fn for every vote until ctx is cancelled. It reconnects on its own with exponential backoff, so a dropped connection recovers.

type Listing

type Listing struct {
	Type      string `json:"type"`
	ID        string `json:"id"`
	Slug      string `json:"slug"`
	Name      string `json:"name"`
	VoteCount int    `json:"voteCount"`
}

Listing describes your server or bot listing.

type Payload

type Payload struct {
	Event      string `json:"event"`
	DeliveryID string `json:"deliveryId"`
	SentAt     string `json:"sentAt"`
	Vote       struct {
		ID      string `json:"id"`
		VotedAt string `json:"votedAt"`
		IsTest  bool   `json:"isTest"`
	} `json:"vote"`
	User struct {
		ID        string `json:"id"`
		Username  string `json:"username"`
		DiscordID string `json:"discordId"`
	} `json:"user"`
	Listing struct {
		Type      string `json:"type"`
		ID        string `json:"id"`
		Slug      string `json:"slug"`
		Name      string `json:"name"`
		VoteCount int    `json:"voteCount"`
	} `json:"listing"`
	Streak struct {
		Listing int `json:"listing"`
		Global  int `json:"global"`
	} `json:"streak"`
}

Payload is the body of a vote webhook.

type Stats

type Stats struct {
	ServerCount int  `json:"serverCount"`
	UserCount   *int `json:"userCount,omitempty"`
	ShardCount  *int `json:"shardCount,omitempty"`
}

Stats are the counts a bot reports about itself.

type VoteCheck

type VoteCheck struct {
	Voted     bool   `json:"voted"`
	VotedAt   string `json:"votedAt"`
	ExpiresAt string `json:"expiresAt"`
}

VoteCheck reports whether a user's vote is still inside the cooldown window.

type VoteEvent

type VoteEvent struct {
	Type       string `json:"type"`
	VoteID     string `json:"voteId"`
	UserID     string `json:"userId"`
	Username   string `json:"username"`
	TargetType string `json:"targetType"`
	TargetID   string `json:"targetId"`
	Slug       string `json:"slug"`
	Name       string `json:"name"`
	VoteCount  int    `json:"voteCount"`
	VotedAt    string `json:"votedAt"`
}

VoteEvent is a single vote pushed over the vote stream.

type Voter

type Voter struct {
	ID      string `json:"id"`
	VotedAt string `json:"votedAt"`
	User    struct {
		ID        string `json:"id"`
		Username  string `json:"username"`
		DiscordID string `json:"discordId"`
	} `json:"user"`
}

Voter is one entry from the recent voters list.

Directories

Path Synopsis
Command conformance checks this package against the shared signature test vectors.
Command conformance checks this package against the shared signature test vectors.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL