package discord import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "github.com/pkg/errors" ) // Token represents a response from the Discord OAuth API after a successful authorization request type Token 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"` } const oauthurl string = "https://discord.com/oauth2/authorize" const apiurl string = "https://discord.com/api/v10" // GetOAuthLink generates a new Discord OAuth2 link for user authentication func (api *APIClient) GetOAuthLink(state string) (string, error) { if state == "" { return "", errors.New("state cannot be empty") } values := url.Values{} values.Add("response_type", "code") values.Add("client_id", api.cfg.ClientID) values.Add("scope", api.cfg.OAuthScopes) values.Add("state", state) values.Add("redirect_uri", fmt.Sprintf("%s/%s", api.trustedHost, api.cfg.RedirectPath)) values.Add("prompt", "none") return fmt.Sprintf("%s?%s", oauthurl, values.Encode()), nil } // AuthorizeWithCode uses a users authorization token generated by OAuth2 to get a token for // making requests to the API on behalf of the user func (api *APIClient) AuthorizeWithCode(code string) (*Token, error) { if code == "" { return nil, errors.New("code cannot be empty") } data := url.Values{} data.Set("grant_type", "authorization_code") data.Set("code", code) data.Set("redirect_uri", fmt.Sprintf("%s/%s", api.trustedHost, api.cfg.RedirectPath)) req, err := http.NewRequest( "POST", apiurl+"/oauth2/token", strings.NewReader(data.Encode()), ) if err != nil { return nil, errors.Wrap(err, "failed to create request") } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(api.cfg.ClientID, api.cfg.ClientSecret) resp, err := api.Do(req) if err != nil { return nil, errors.Wrap(err, "failed to execute request") } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(err, "failed to read response body") } if resp.StatusCode != http.StatusOK { return nil, errors.Errorf("discord API returned status %d: %s", resp.StatusCode, string(body)) } var tokenResp Token if err := json.Unmarshal(body, &tokenResp); err != nil { return nil, errors.Wrap(err, "failed to parse token response") } return &tokenResp, nil } // RefreshToken uses the refresh token to generate a new token pair func (api *APIClient) RefreshToken(token *Token) (*Token, error) { if token == nil { return nil, errors.New("token cannot be nil") } data := url.Values{} data.Set("grant_type", "refresh_token") data.Set("refresh_token", token.RefreshToken) req, err := http.NewRequest( "POST", apiurl+"/oauth2/token", strings.NewReader(data.Encode()), ) if err != nil { return nil, errors.Wrap(err, "failed to create request") } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(api.cfg.ClientID, api.cfg.ClientSecret) resp, err := api.Do(req) if err != nil { return nil, errors.Wrap(err, "failed to execute request") } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(err, "failed to read response body") } if resp.StatusCode != http.StatusOK { return nil, errors.Errorf("discord API returned status %d: %s", resp.StatusCode, string(body)) } var tokenResp Token if err := json.Unmarshal(body, &tokenResp); err != nil { return nil, errors.Wrap(err, "failed to parse token response") } return &tokenResp, nil } // RevokeToken sends a request to the Discord API to revoke the token pair func (api *APIClient) RevokeToken(token *Token) error { if token == nil { return errors.New("token cannot be nil") } data := url.Values{} data.Set("token", token.AccessToken) data.Set("token_type_hint", "access_token") req, err := http.NewRequest( "POST", apiurl+"/oauth2/token/revoke", strings.NewReader(data.Encode()), ) if err != nil { return errors.Wrap(err, "failed to create request") } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(api.cfg.ClientID, api.cfg.ClientSecret) resp, err := api.Do(req) if err != nil { return errors.Wrap(err, "failed to execute request") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return errors.Errorf("discord API returned status %d", resp.StatusCode) } return nil }