163 lines
4.6 KiB
Go
163 lines
4.6 KiB
Go
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)
|
|
}
|