package discord import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) const baseURL = "https://discord.com/api/v10" type TokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` Scope string `json:"scope"` } type User struct { ID string `json:"id"` Username string `json:"username"` Discriminator string `json:"discriminator"` Avatar *string `json:"avatar"` Email *string `json:"email"` Verified bool `json:"verified"` } type ApplicationRPCResponse struct { ID string `json:"id"` Name string `json:"name"` Icon string `json:"icon"` Description string `json:"description"` Summary string `json:"summary"` IsMonetized bool `json:"is_monetized"` IsVerified bool `json:"is_verified"` IsDiscoverable bool `json:"is_discoverable"` Hook bool `json:"hook"` GuildID string `json:"guild_id"` StorefrontAvailable bool `json:"storefront_available"` BotPublic bool `json:"bot_public"` BotRequireCodeGrant bool `json:"bot_require_code_grant"` TermsOfServiceURL string `json:"terms_of_service_url"` PrivacyPolicyURL string `json:"privacy_policy_url"` CustomInstallURL string `json:"custom_install_url"` VerifyKey string `json:"verify_key"` Flags int `json:"flags"` Tags []string `json:"tags"` } type Client struct { clientID string clientSecret string redirectURI string httpClient *http.Client } func NewClient(clientID, clientSecret, redirectURI string) *Client { return &Client{ clientID: clientID, clientSecret: clientSecret, redirectURI: redirectURI, httpClient: &http.Client{Timeout: 10 * time.Second}, } } // AuthURL returns the Discord OAuth2 authorization URL. // state should be a random, unguessable string stored in the session. func (c *Client) AuthURL(state string) string { v := url.Values{} v.Set("client_id", c.clientID) v.Set("redirect_uri", c.redirectURI) v.Set("response_type", "code") v.Set("scope", "identify email") v.Set("state", state) return baseURL + "/oauth2/authorize?" + v.Encode() } // ExchangeCode exchanges an authorization code for an access token. func (c *Client) ExchangeCode(ctx context.Context, code string) (*TokenResponse, error) { body := url.Values{} body.Set("grant_type", "authorization_code") body.Set("code", code) body.Set("redirect_uri", c.redirectURI) return c.tokenRequest(ctx, body) } // RefreshToken exchanges a refresh token for a new access token. func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) { body := url.Values{} body.Set("grant_type", "refresh_token") body.Set("refresh_token", refreshToken) return c.tokenRequest(ctx, body) } // GetCurrentUser fetches the authenticated user's profile. func (c *Client) GetCurrentUser(ctx context.Context, accessToken string) (*User, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/users/@me", nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) var user User if err := c.do(req, &user); err != nil { return nil, err } return &user, nil } // GetApplication fetches public metadata for a bot application. func (c *Client) GetApplication(ctx context.Context, id string) (*ApplicationRPCResponse, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/applications/%s/rpc", baseURL, id), nil) if err != nil { return nil, err } var app ApplicationRPCResponse if err := c.do(req, &app); err != nil { return nil, err } return &app, nil } func (c *Client) tokenRequest(ctx context.Context, body url.Values) (*TokenResponse, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/oauth2/token", strings.NewReader(body.Encode()), ) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(c.clientID, c.clientSecret) var t TokenResponse if err := c.do(req, &t); err != nil { return nil, err } return &t, nil } func (c *Client) do(req *http.Request, out any) error { res, err := c.httpClient.Do(req) if err != nil { return err } defer res.Body.Close() if res.StatusCode >= 400 { body, _ := io.ReadAll(res.Body) return fmt.Errorf("discord API error %d: %s", res.StatusCode, body) } return json.NewDecoder(res.Body).Decode(out) }