package auth import ( "context" "encoding/json" "fmt" "io" "net/http" "golang.org/x/oauth2" ) // DiscordEndpoint is the Discord OAuth2 endpoint. var DiscordEndpoint = oauth2.Endpoint{ AuthURL: "https://discord.com/api/oauth2/authorize", TokenURL: "https://discord.com/api/oauth2/token", } // DiscordUser holds the data returned by /users/@me. type DiscordUser struct { ID string `json:"id"` Username string `json:"username"` Avatar string `json:"avatar"` } // AvatarURL returns the full Discord CDN URL for the user's avatar. func (u *DiscordUser) AvatarURL() string { if u.Avatar == "" { return "" } return fmt.Sprintf("https://cdn.discordapp.com/avatars/%s/%s.png?size=128", u.ID, u.Avatar) } // NewOAuthConfig builds an oauth2.Config for Discord. func NewOAuthConfig(clientID, clientSecret, callbackURL string) *oauth2.Config { return &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, RedirectURL: callbackURL, Scopes: []string{"identify"}, Endpoint: DiscordEndpoint, } } // AuthURL returns the Discord authorization URL with the given state. func AuthURL(cfg *oauth2.Config, state string) string { return cfg.AuthCodeURL(state, oauth2.AccessTypeOnline) } // FetchDiscordUser exchanges the authorization code for a token, then calls /users/@me. func FetchDiscordUser(ctx context.Context, cfg *oauth2.Config, code string) (*DiscordUser, error) { token, err := cfg.Exchange(ctx, code) if err != nil { return nil, fmt.Errorf("exchange oauth code: %w", err) } client := cfg.Client(ctx, token) resp, err := client.Get("https://discord.com/api/v10/users/@me") if err != nil { return nil, fmt.Errorf("fetch discord user: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("discord api %d: %s", resp.StatusCode, body) } var user DiscordUser if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { return nil, fmt.Errorf("decode discord user: %w", err) } return &user, nil }