mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-20 03:41:16 +00:00
[spotify] Fix: Refresh-Token gets revoked when using two functions
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
5dd6a5323c
commit
30305600e7
5 changed files with 124 additions and 37 deletions
|
@ -467,12 +467,12 @@ Example:
|
|||
|
||||
```
|
||||
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
|
||||
< Your int this hour: 72%
|
||||
< Your int this hour: 82%
|
||||
```
|
||||
|
||||
### `spotifyCurrentPlaying`
|
||||
|
||||
Retrieves the current playing track for the given channel
|
||||
Retrieves the current playing track for the given channel (returns an empty string when nothing is playing)
|
||||
|
||||
Syntax: `spotifyCurrentPlaying <channel>`
|
||||
|
||||
|
@ -487,7 +487,7 @@ Example:
|
|||
|
||||
### `spotifyLink`
|
||||
|
||||
Retrieves the link for the playing track for the given channel
|
||||
Retrieves the link for the playing track for the given channel (returns an empty string when nothing is playing)
|
||||
|
||||
Syntax: `spotifyLink <channel>`
|
||||
|
||||
|
|
101
internal/actors/spotify/auth.go
Normal file
101
internal/actors/spotify/auth.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package spotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/locker"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const expiryGrace = 10 * time.Second
|
||||
|
||||
func getAuthorizedClient(channel, redirectURL string) (client *http.Client, err error) {
|
||||
// In templating functions are called multiple times at once which
|
||||
// with Spotify replacing the refresh-token on each renew would kill
|
||||
// the stored token when multiple spotify functions are called at
|
||||
// once. Therefore we do have this method locking itself until it
|
||||
// has successfully made one request to the users profile and therefore
|
||||
// renewed the token. The next request then will use the token the
|
||||
// previous request renewed.
|
||||
locker.LockByKey(strings.Join([]string{"spotify", "api-access", channel}, ":"))
|
||||
defer locker.UnlockByKey(strings.Join([]string{"spotify", "api-access", channel}, ":"))
|
||||
|
||||
conf, err := oauthConfig(channel, redirectURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting oauth config: %w", err)
|
||||
}
|
||||
|
||||
var token *oauth2.Token
|
||||
if err = db.ReadEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), &token); err != nil {
|
||||
return nil, fmt.Errorf("loading oauth token: %w", err)
|
||||
}
|
||||
|
||||
ts := conf.TokenSource(context.Background(), token)
|
||||
|
||||
if token.Expiry.After(time.Now().Add(expiryGrace)) {
|
||||
// Token is still valid long enough, we spare the resources to do
|
||||
// the profile fetch and directly return the client with the token
|
||||
// as the scenario described here does not apply.
|
||||
return oauth2.NewClient(context.Background(), ts), nil
|
||||
}
|
||||
|
||||
logrus.WithField("channel", channel).Debug("refreshing spotify token")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), spotifyRequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// We do a request to /me once to refresh the token if needed
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.spotify.com/v1/me", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating currently-playing request: %w", err)
|
||||
}
|
||||
|
||||
oauthClient := oauth2.NewClient(context.Background(), ts)
|
||||
|
||||
resp, err := oauthClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logrus.WithError(err).Error("closing Spotify response body (leaked fd)")
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("requesting user profile: %w", err)
|
||||
}
|
||||
|
||||
updToken, err := ts.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting updated token: %w", err)
|
||||
}
|
||||
|
||||
if err := db.StoreEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), updToken); err != nil {
|
||||
logrus.WithError(err).Error("storing back Spotify auth token")
|
||||
}
|
||||
|
||||
return oauthClient, nil
|
||||
}
|
||||
|
||||
func oauthConfig(channel, redirectURL string) (conf *oauth2.Config, err error) {
|
||||
clientID, err := getModuleConfig(actorName, channel).String("clientId")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting clientId for channel: %w", err)
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://accounts.spotify.com/authorize",
|
||||
TokenURL: "https://accounts.spotify.com/api/token",
|
||||
},
|
||||
RedirectURL: redirectURL,
|
||||
Scopes: []string{"user-read-currently-playing"},
|
||||
}, nil
|
||||
}
|
|
@ -3,34 +3,25 @@ package spotify
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var errNotPlaying = errors.New("nothing playing")
|
||||
|
||||
func getCurrentTrackForChannel(channel string) (track currentPlayingTrackResponse, err error) {
|
||||
channel = strings.TrimLeft(channel, "#")
|
||||
|
||||
conf, err := oauthConfig(channel, "")
|
||||
client, err := getAuthorizedClient(channel, "")
|
||||
if err != nil {
|
||||
return track, fmt.Errorf("getting oauth config: %w", err)
|
||||
return track, fmt.Errorf("retrieving authorized Spotify client: %w", err)
|
||||
}
|
||||
|
||||
var token *oauth2.Token
|
||||
if err = db.ReadEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), &token); err != nil {
|
||||
return track, fmt.Errorf("loading oauth token: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := db.StoreEncryptedCoreMeta(strings.Join([]string{"spotify-auth", channel}, ":"), token); err != nil {
|
||||
logrus.WithError(err).Error("storing back Spotify auth token")
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), spotifyRequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
@ -39,7 +30,7 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
|
|||
return track, fmt.Errorf("creating currently-playing request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := conf.Client(context.Background(), token).Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return track, fmt.Errorf("executing request: %w", err)
|
||||
}
|
||||
|
@ -58,6 +49,10 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
|
|||
case http.StatusOK:
|
||||
// This is perfect, continue below
|
||||
|
||||
case http.StatusNoContent:
|
||||
// User is not playing anything
|
||||
return track, errNotPlaying
|
||||
|
||||
case http.StatusUnauthorized:
|
||||
// The token is FUBAR
|
||||
return track, fmt.Errorf("token expired (HTTP 401 - unauthorized)")
|
||||
|
@ -85,6 +80,10 @@ func getCurrentTrackForChannel(channel string) (track currentPlayingTrackRespons
|
|||
func getCurrentArtistTitleForChannel(channel string) (artistTitle string, err error) {
|
||||
track, err := getCurrentTrackForChannel(channel)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNotPlaying) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("getting track info: %w", err)
|
||||
}
|
||||
|
||||
|
@ -102,6 +101,10 @@ func getCurrentArtistTitleForChannel(channel string) (artistTitle string, err er
|
|||
func getCurrentLinkForChannel(channel string) (link string, err error) {
|
||||
track, err := getCurrentTrackForChannel(channel)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNotPlaying) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("getting track info: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -70,20 +70,3 @@ func handleStartAuth(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
fmt.Fprintln(w, "Spotify is now authorized for this channel, you can close this page")
|
||||
}
|
||||
|
||||
func oauthConfig(channel, redirectURL string) (conf *oauth2.Config, err error) {
|
||||
clientID, err := getModuleConfig(actorName, channel).String("clientId")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting clientId for channel: %w", err)
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://accounts.spotify.com/authorize",
|
||||
TokenURL: "https://accounts.spotify.com/api/token",
|
||||
},
|
||||
RedirectURL: redirectURL,
|
||||
Scopes: []string{"user-read-currently-playing"},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
|||
plugins.GenericTemplateFunctionGetter(getCurrentArtistTitleForChannel),
|
||||
plugins.TemplateFuncDocumentation{
|
||||
Name: "spotifyCurrentPlaying",
|
||||
Description: "Retrieves the current playing track for the given channel",
|
||||
Description: "Retrieves the current playing track for the given channel (returns an empty string when nothing is playing)",
|
||||
Syntax: "spotifyCurrentPlaying <channel>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
MatchMessage: "^!spotify",
|
||||
|
@ -51,7 +51,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
|||
plugins.GenericTemplateFunctionGetter(getCurrentLinkForChannel),
|
||||
plugins.TemplateFuncDocumentation{
|
||||
Name: "spotifyLink",
|
||||
Description: "Retrieves the link for the playing track for the given channel",
|
||||
Description: "Retrieves the link for the playing track for the given channel (returns an empty string when nothing is playing)",
|
||||
Syntax: "spotifyLink <channel>",
|
||||
Example: &plugins.TemplateFuncDocumentationExample{
|
||||
MatchMessage: "^!spotifylink",
|
||||
|
|
Loading…
Reference in a new issue