mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
181 lines
6.8 KiB
Go
181 lines
6.8 KiB
Go
package spotify
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
const (
|
|
// AuthURL is the URL to Spotify Accounts Service's OAuth2 endpoint.
|
|
AuthURL = "https://accounts.spotify.com/authorize"
|
|
// TokenURL is the URL to the Spotify Accounts Service's OAuth2
|
|
// token endpoint.
|
|
TokenURL = "https://accounts.spotify.com/api/token"
|
|
)
|
|
|
|
// Scopes let you specify exactly which types of data your application wants to access.
|
|
// The set of scopes you pass in your authentication request determines what access the
|
|
// permissions the user is asked to grant.
|
|
const (
|
|
// ScopeImageUpload seeks permission to upload images to Spotify on your behalf.
|
|
ScopeImageUpload = "ugc-image-upload"
|
|
// ScopePlaylistReadPrivate seeks permission to read
|
|
// a user's private playlists.
|
|
ScopePlaylistReadPrivate = "playlist-read-private"
|
|
// ScopePlaylistModifyPublic seeks write access
|
|
// to a user's public playlists.
|
|
ScopePlaylistModifyPublic = "playlist-modify-public"
|
|
// ScopePlaylistModifyPrivate seeks write access to
|
|
// a user's private playlists.
|
|
ScopePlaylistModifyPrivate = "playlist-modify-private"
|
|
// ScopePlaylistReadCollaborative seeks permission to
|
|
// access a user's collaborative playlists.
|
|
ScopePlaylistReadCollaborative = "playlist-read-collaborative"
|
|
// ScopeUserFollowModify seeks write/delete access to
|
|
// the list of artists and other users that a user follows.
|
|
ScopeUserFollowModify = "user-follow-modify"
|
|
// ScopeUserFollowRead seeks read access to the list of
|
|
// artists and other users that a user follows.
|
|
ScopeUserFollowRead = "user-follow-read"
|
|
// ScopeUserLibraryModify seeks write/delete access to a
|
|
// user's "Your Music" library.
|
|
ScopeUserLibraryModify = "user-library-modify"
|
|
// ScopeUserLibraryRead seeks read access to a user's "Your Music" library.
|
|
ScopeUserLibraryRead = "user-library-read"
|
|
// ScopeUserReadPrivate seeks read access to a user's
|
|
// subsription details (type of user account).
|
|
ScopeUserReadPrivate = "user-read-private"
|
|
// ScopeUserReadEmail seeks read access to a user's email address.
|
|
ScopeUserReadEmail = "user-read-email"
|
|
// ScopeUserReadBirthdate seeks read access to a user's birthdate.
|
|
ScopeUserReadBirthdate = "user-read-birthdate"
|
|
// ScopeUserReadCurrentlyPlaying seeks read access to a user's currently playing track
|
|
ScopeUserReadCurrentlyPlaying = "user-read-currently-playing"
|
|
// ScopeUserReadPlaybackState seeks read access to the user's current playback state
|
|
ScopeUserReadPlaybackState = "user-read-playback-state"
|
|
// ScopeUserModifyPlaybackState seeks write access to the user's current playback state
|
|
ScopeUserModifyPlaybackState = "user-modify-playback-state"
|
|
// ScopeUserReadRecentlyPlayed allows access to a user's recently-played songs
|
|
ScopeUserReadRecentlyPlayed = "user-read-recently-played"
|
|
// ScopeUserTopRead seeks read access to a user's top tracks and artists
|
|
ScopeUserTopRead = "user-top-read"
|
|
)
|
|
|
|
// Authenticator provides convenience functions for implementing the OAuth2 flow.
|
|
// You should always use `NewAuthenticator` to make them.
|
|
//
|
|
// Example:
|
|
//
|
|
// a := spotify.NewAuthenticator(redirectURL, spotify.ScopeUserLibaryRead, spotify.ScopeUserFollowRead)
|
|
// // direct user to Spotify to log in
|
|
// http.Redirect(w, r, a.AuthURL("state-string"), http.StatusFound)
|
|
//
|
|
// // then, in redirect handler:
|
|
// token, err := a.Token(state, r)
|
|
// client := a.NewClient(token)
|
|
//
|
|
type Authenticator struct {
|
|
config *oauth2.Config
|
|
context context.Context
|
|
}
|
|
|
|
// NewAuthenticator creates an authenticator which is used to implement the
|
|
// OAuth2 authorization flow. The redirectURL must exactly match one of the
|
|
// URLs specified in your Spotify developer account.
|
|
//
|
|
// By default, NewAuthenticator pulls your client ID and secret key from the
|
|
// SPOTIFY_ID and SPOTIFY_SECRET environment variables. If you'd like to provide
|
|
// them from some other source, you can call `SetAuthInfo(id, key)` on the
|
|
// returned authenticator.
|
|
func NewAuthenticator(redirectURL string, scopes ...string) Authenticator {
|
|
cfg := &oauth2.Config{
|
|
ClientID: os.Getenv("SPOTIFY_ID"),
|
|
ClientSecret: os.Getenv("SPOTIFY_SECRET"),
|
|
RedirectURL: redirectURL,
|
|
Scopes: scopes,
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: AuthURL,
|
|
TokenURL: TokenURL,
|
|
},
|
|
}
|
|
|
|
// disable HTTP/2 for DefaultClient, see: https://github.com/zmb3/spotify/issues/20
|
|
tr := &http.Transport{
|
|
TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
|
|
}
|
|
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: tr})
|
|
return Authenticator{
|
|
config: cfg,
|
|
context: ctx,
|
|
}
|
|
}
|
|
|
|
// SetAuthInfo overwrites the client ID and secret key used by the authenticator.
|
|
// You can use this if you don't want to store this information in environment variables.
|
|
func (a *Authenticator) SetAuthInfo(clientID, secretKey string) {
|
|
a.config.ClientID = clientID
|
|
a.config.ClientSecret = secretKey
|
|
}
|
|
|
|
// AuthURL returns a URL to the the Spotify Accounts Service's OAuth2 endpoint.
|
|
//
|
|
// State is a token to protect the user from CSRF attacks. You should pass the
|
|
// same state to `Token`, where it will be validated. For more info, refer to
|
|
// http://tools.ietf.org/html/rfc6749#section-10.12.
|
|
func (a Authenticator) AuthURL(state string) string {
|
|
return a.config.AuthCodeURL(state)
|
|
}
|
|
|
|
// Token pulls an authorization code from an HTTP request and attempts to exchange
|
|
// it for an access token. The standard use case is to call Token from the handler
|
|
// that handles requests to your application's redirect URL.
|
|
func (a Authenticator) Token(state string, r *http.Request) (*oauth2.Token, error) {
|
|
values := r.URL.Query()
|
|
if e := values.Get("error"); e != "" {
|
|
return nil, errors.New("spotify: auth failed - " + e)
|
|
}
|
|
code := values.Get("code")
|
|
if code == "" {
|
|
return nil, errors.New("spotify: didn't get access code")
|
|
}
|
|
actualState := values.Get("state")
|
|
if actualState != state {
|
|
return nil, errors.New("spotify: redirect state parameter doesn't match")
|
|
}
|
|
return a.config.Exchange(a.context, code)
|
|
}
|
|
|
|
// Exchange is like Token, except it allows you to manually specify the access
|
|
// code instead of pulling it out of an HTTP request.
|
|
func (a Authenticator) Exchange(code string) (*oauth2.Token, error) {
|
|
return a.config.Exchange(a.context, code)
|
|
}
|
|
|
|
// NewClient creates a Client that will use the specified access token for its API requests.
|
|
func (a Authenticator) NewClient(token *oauth2.Token) Client {
|
|
client := a.config.Client(a.context, token)
|
|
return Client{
|
|
http: client,
|
|
baseURL: baseAddress,
|
|
}
|
|
}
|
|
|
|
// Token gets the client's current token.
|
|
func (c *Client) Token() (*oauth2.Token, error) {
|
|
transport, ok := c.http.Transport.(*oauth2.Transport)
|
|
if !ok {
|
|
return nil, errors.New("spotify: oauth2 transport type not correct")
|
|
}
|
|
t, err := transport.Source.Token()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return t, nil
|
|
}
|