diff --git a/api/services/spotify.json b/api/services/spotify.json index 99aba86..abbc15f 100644 --- a/api/services/spotify.json +++ b/api/services/spotify.json @@ -80,45 +80,15 @@ }, "params": [ { - "name": "artist", + "name": "trackUri", "type": "string", "description": { - "en": "Artist of the song to play", - "fr": "Interprète de la musique à jouer" - } - }, - { - "name": "track", - "type": "string", - "description": { - "en": "Title of the song to play", - "fr": "Titre de la musique à jouer" + "en": "Url of a track", + "fr": "Url d'une musique'" } } ], - "returns": [ - { - "name": "URL", - "description": { - "en": "URL of the song", - "fr": "URL de la musique" - } - }, - { - "name": "ARTIST", - "description": { - "en": "Artist of the song", - "fr": "Interprête de la musique" - } - }, - { - "name": "TRACK", - "description": { - "en": "Title of the song", - "fr": "Titre de la musique" - } - } - ] + "returns": [] }, { "name": "Spotify_Pause", @@ -145,45 +115,15 @@ }, "params": [ { - "name": "artist", + "name": "trackUri", "type": "string", "description": { - "en": "Artist of the song to add", - "fr": "Interprète de la musique à ajouter" - } - }, - { - "name": "track", - "type": "string", - "description": { - "en": "Title of the song to add", - "fr": "Titre de la musique à ajouter" + "en": "Url of a track", + "fr": "Url d'une musique'" } } ], - "returns": [ - { - "name": "URL", - "description": { - "en": "URL of the added song", - "fr": "URL de la musique ajoutée" - } - }, - { - "name": "ARTIST", - "description": { - "en": "Artist of the added song", - "fr": "Interprète de la musique ajoutée" - } - }, - { - "name": "TRACK", - "description": { - "en": "Title of the added song", - "fr": "Titre de la musique ajoutée" - } - } - ] + "returns": [] }, { "name": "Spotify_AddToPlaylist", @@ -197,53 +137,23 @@ }, "params": [ { - "name": "artist", + "name": "trackUri", "type": "string", "description": { - "en": "Artist of the song to add", - "fr": "Interprète de la musique à ajouter" + "en": "Url of a track", + "fr": "Url d'une musique'" } }, { - "name": "track", + "name": "playlistUri", "type": "string", "description": { - "en": "Title of the song to add", - "fr": "Titre de la musique à ajouter" - } - }, - { - "name": "playlist", - "type": "string", - "description": { - "en": "Name of the playlist", - "fr": "Nom de la playlist dans laquelle ajouter la musique" + "en": "Id of the playlist", + "fr": "Id de la playlist dans laquelle ajouter la musique" } } ], - "returns": [ - { - "name": "URL", - "description": { - "en": "URL of the added song", - "fr": "URL de la musique ajoutée" - } - }, - { - "name": "ARTIST", - "description": { - "en": "Artist of the added song", - "fr": "Interprète de la musique ajoutée" - } - }, - { - "name": "TRACK", - "description": { - "en": "Title of the added song", - "fr": "Titre de la musique ajoutée" - } - } - ] + "returns": [] } ] -} \ No newline at end of file +} diff --git a/api/src/Api/OIDC.hs b/api/src/Api/OIDC.hs index fd96da9..6b11a72 100644 --- a/api/src/Api/OIDC.hs +++ b/api/src/Api/OIDC.hs @@ -62,7 +62,8 @@ urlHandler Spotify (Just r) = do clientId <- liftIO $ envAsString "SPOTIFY_CLIENT_ID" "" backRedirect <- liftIO $ envAsString "BACK_URL" "" throwError $ err302 { errHeaders = - [("Location", B8.pack $ "https://accounts.spotify.com/authorize?response_type=code&scope=user-library-read&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] } + [("Location", B8.pack $ "https://accounts.spotify.com/authorize?response_type=code&scope=user-library-read user-library-modify streaming playlist-modify-private playlist-read-collaborative playlist-read-private playlist-modify-public user-modify-playback-state user-read-private&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] } + urlHandler Github (Just r) = do clientId <- liftIO $ envAsString "GITHUB_CLIENT_ID" "" backRedirect <- liftIO $ envAsString "BACK_URL" "" diff --git a/api/src/Api/Worker.hs b/api/src/Api/Worker.hs index 27fa3ed..486522f 100644 --- a/api/src/Api/Worker.hs +++ b/api/src/Api/Worker.hs @@ -30,6 +30,7 @@ import Repository.User (updateTokens, getTokensByUserId, delTokens) import GHC.Generics (Generic) import Data.Int (Int64) import Data.Text (Text) +import Data.Time (UTCTime) data WorkerUserData = WorkerUserData @@ -47,7 +48,7 @@ newtype ErrorBody = ErrorBody { error :: Text } data RefreshBody = RefreshBody { accessToken :: Text , refreshToken :: Text - , expiresIn :: Int64 + , expiresAt :: UTCTime } $(deriveJSON defaultOptions ''WorkerUserData) diff --git a/api/src/Core/OIDC.hs b/api/src/Core/OIDC.hs index 15affb2..2a74b5d 100644 --- a/api/src/Core/OIDC.hs +++ b/api/src/Core/OIDC.hs @@ -6,13 +6,14 @@ import qualified Data.ByteString.Char8 as B8 import qualified Data.HashMap.Strict as HM import App (AppM) -import Core.User (ExternalToken (ExternalToken), Service (Github, Discord, Spotify, Google, Twitter, Anilist)) +import Core.User (ExternalToken (ExternalToken, expiresAt), Service (Github, Discord, Spotify, Google, Twitter, Anilist)) import Data.Aeson.Types (Object, Value (String)) import Data.Text (Text, pack, unpack) import Network.HTTP.Simple (JSONException, addRequestHeader, getResponseBody, httpJSONEither, parseRequest, setRequestMethod, setRequestQueryString, setRequestBodyURLEncoded) import System.Environment.MrEnv (envAsBool, envAsInt, envAsInteger, envAsString) -import Utils (lookupObjString) +import Utils (lookupObjString, lookupObjInt) import Data.ByteString.Base64 +import Data.Time (getCurrentTime, addUTCTime) data OAuth2Conf = OAuth2Conf { oauthClientId :: String , oauthClientSecret :: String @@ -55,11 +56,12 @@ getGithubTokens code = do ] request' response <- httpJSONEither request + currTime <- getCurrentTime return $ case (getResponseBody response :: Either JSONException Object) of Left _ -> Nothing Right obj -> do access <- lookupObjString obj "access_token" - Just $ ExternalToken (pack access) "" 0 Github + Just $ ExternalToken (pack access) "" currTime Github -- DISCORD getDiscordConfig :: IO OAuth2Conf @@ -86,12 +88,15 @@ getDiscordTokens code = do ] request' response <- httpJSONEither request + currTime <- getCurrentTime return $ case (getResponseBody response :: Either JSONException Object) of Left _ -> Nothing Right obj -> do access <- lookupObjString obj "access_token" refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Discord + expiresIn <- lookupObjInt obj "expires_in" + let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime + Just $ ExternalToken (pack access) (pack refresh) expiresAt Discord -- GOOGLE getGoogleConfig :: IO OAuth2Conf @@ -118,12 +123,15 @@ getGoogleTokens code = do ] request' response <- httpJSONEither request + currTime <- getCurrentTime return $ case (getResponseBody response :: Either JSONException Object) of Left _ -> Nothing Right obj -> do access <- lookupObjString obj "access_token" refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Google + expiresIn <- lookupObjInt obj "expires_in" + let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime + Just $ ExternalToken (pack access) (pack refresh) expiresAt Google -- SPOTIFY getSpotifyConfig :: IO OAuth2Conf @@ -151,12 +159,15 @@ getSpotifyTokens code = do ] request' response <- httpJSONEither request + currTime <- getCurrentTime return $ case (getResponseBody response :: Either JSONException Object) of Left _ -> Nothing Right obj -> do access <- lookupObjString obj "access_token" refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Spotify + expiresIn <- lookupObjInt obj "expires_in" + let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime + Just $ ExternalToken (pack access) (pack refresh) expiresAt Spotify -- TWITTER getTwitterConfig :: IO OAuth2Conf @@ -184,12 +195,15 @@ getTwitterTokens code = do ] request' response <- httpJSONEither request + currTime <- getCurrentTime return $ case (getResponseBody response :: Either JSONException Object) of Left _ -> Nothing Right obj -> do access <- lookupObjString obj "access_token" refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Twitter + expiresIn <- lookupObjInt obj "expires_in" + let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime + Just $ ExternalToken (pack access) (pack refresh) expiresAt Twitter -- ANILIST getAnilistConfig :: IO OAuth2Conf @@ -216,12 +230,15 @@ getAnilistTokens code = do ] request' response <- httpJSONEither request + currTime <- getCurrentTime return $ case (getResponseBody response :: Either JSONException Object) of Left _ -> Nothing Right obj -> do access <- lookupObjString obj "access_token" refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Anilist + expiresIn <- lookupObjInt obj "expires_in" + let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime + Just $ ExternalToken (pack access) (pack refresh) expiresAt Anilist diff --git a/api/src/Core/User.hs b/api/src/Core/User.hs index 3e2f59a..3a7f7b2 100644 --- a/api/src/Core/User.hs +++ b/api/src/Core/User.hs @@ -20,6 +20,7 @@ import Servant (AuthProtect, FromHttpApiData) import Servant.API (FromHttpApiData (parseUrlPiece)) import Servant.Server.Experimental.Auth (AuthServerData) import Servant.Auth.JWT (ToJWT, FromJWT) +import Data.Time (UTCTime) newtype UserId = UserId {toInt64 :: Int64} deriving newtype (DBEq, DBType, Eq, Show, Num, FromJSON, ToJSON, FromHttpApiData) @@ -41,7 +42,7 @@ instance FromHttpApiData Service where data ExternalToken = ExternalToken { accessToken :: Text , refreshToken :: Text - , expiresIn :: Int64 + , expiresAt :: UTCTime , service :: Service } deriving (Eq, Show, Generic) diff --git a/api/src/Utils.hs b/api/src/Utils.hs index 16a04be..7a35ef2 100644 --- a/api/src/Utils.hs +++ b/api/src/Utils.hs @@ -33,7 +33,7 @@ lookupObjString obj key = case Data.HashMap.Strict.lookup key obj of lookupObjInt :: Object -> Text -> Maybe Int64 lookupObjInt obj key = case Data.HashMap.Strict.lookup key obj of - Just (Number x) -> toBoundedInteger $ x + Just (Number x) -> toBoundedInteger x _ -> Nothing uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d diff --git a/worker/src/models/pipeline.ts b/worker/src/models/pipeline.ts index b24ffe5..fbf200e 100644 --- a/worker/src/models/pipeline.ts +++ b/worker/src/models/pipeline.ts @@ -96,7 +96,7 @@ export class Pipeline { export class Token { accessToken: string; refreshToken: string; - expiresIn: string; + expiresAt: string; }; export class Reaction { @@ -132,7 +132,7 @@ export const pipelineFromApi = (data: any): Pipeline => { { accessToken: x.accessToken, refreshToken: x.refreshToken, - expiresIn: x.expiresIn + expiresAt: x.expiresAt } as Token ])), }; diff --git a/worker/src/runner.ts b/worker/src/runner.ts index af0943f..d439372 100644 --- a/worker/src/runner.ts +++ b/worker/src/runner.ts @@ -27,7 +27,7 @@ export class Runner { for (let [key, value] of Object.entries(params)) { let newValue = value; if (typeof value == "string") { - newValue = value.replace(/{(\w*)(?:@(\d))?}/, (_, name, index) => { + newValue = value.replace(/{(\w*)(?:@(\d))?}/g, (_, name, index) => { if (index) return this._history[parseInt(index)][name] return this._history[this._history.length - 1][name] diff --git a/worker/src/services/spotify.ts b/worker/src/services/spotify.ts index 8be3082..bbe618b 100644 --- a/worker/src/services/spotify.ts +++ b/worker/src/services/spotify.ts @@ -7,12 +7,14 @@ import { Pipeline, PipelineEnv, PipelineType, ReactionType, ServiceType } from " @service(ServiceType.Spotify) export class Spotify extends BaseService { + private _pipeline: Pipeline; private _spotify; constructor(pipeline: Pipeline) { super(); if (!("Spotify" in pipeline.userData)) throw new Error("User is not authenticated via Spotify"); + this._pipeline = pipeline; this._spotify = new SpotifyWebApi({ accessToken: pipeline.userData["Spotify"].accessToken, refreshToken: pipeline.userData["Spotify"].refreshToken, @@ -22,7 +24,20 @@ export class Spotify extends BaseService { } private async _refreshIfNeeded(): Promise { - + if (Date.parse(this._pipeline.userData["Spotify"].expiresAt) >= Date.now() + 100_000) + return; + const ret = await this._spotify.refreshAccessToken(); + fetch(`${process.env["WORKER_API_URL"]}/spotify/${this._pipeline.userId}?WORKER_API_KEY=${process.env["WORKER_API_KEY"]}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + accessToken: ret.body.access_token, + refreshToken: ret.body.refresh_token, + expiresAt: new Date(Date.now() + ret.body.expires_in), + }), + }); } @action(PipelineType.OnSpotifyAddToPlaylist, ["playlistId"]) @@ -53,64 +68,32 @@ export class Spotify extends BaseService { }); } - private async _searchTrack(artistName: string, trackName: string) { - await this._refreshIfNeeded(); - let searchResult = await this._spotify.searchTracks(`artist=${artistName}&track=${trackName}`); - if (searchResult.body.tracks.total == 0) - throw new Error(`Spotify API: '${trackName}' by ${artistName}: no such track`); - return searchResult.body.tracks.items[0]; - } - - private async _searchPlaylist(playlistName: string) { - await this._refreshIfNeeded(); - let searchResult = await this._spotify.searchPlaylists(`name=${playlistName}&type=playlist`); - if (searchResult.body.playlists.total == 0) - throw new Error(`Spotify API: '${playlistName}': no such playlist`); - return searchResult.body.playlists.items[0]; - } - - @reaction(ReactionType.PlayTrack, ['artist', 'track']) + @reaction(ReactionType.PlayTrack, ['trackUri']) async playTrack(params: any): Promise { await this._refreshIfNeeded(); - let track = await this._searchTrack(params['artist'], params['track']); - await this._spotify.play({uris: [track.uri]}); - return { - URL: track.uri, - ARTIST: track.artists?.[0].name, - TRACK: track.name, - }; + await this._spotify.play({uris: [params.trackUri]}); + return {}; } @reaction(ReactionType.Pause, []) - async pause(params: any): Promise { + async pause(_: any): Promise { await this._refreshIfNeeded(); await this._spotify.pause(); return {}; } - @reaction(ReactionType.AddTrackToLibrary, ['artist', 'track']) + @reaction(ReactionType.AddTrackToLibrary, ['trackUri']) async addTrackToLibrary(params: any): Promise { await this._refreshIfNeeded(); - let track = await this._searchTrack(params['artist'], params['track']); - await this._spotify.addToMySavedTracks([track.id]); - return { - URL: track.uri, - ARTIST: track.artists?.[0].name, - TRACK: track.name, - }; + await this._spotify.addToMySavedTracks([params.trackUri]); + return {}; } - @reaction(ReactionType.AddToPlaylist, ['artist', 'track', 'playlist']) + @reaction(ReactionType.AddToPlaylist, ['trackUri', 'playlistUri']) async addToPlaylist(params: any): Promise { await this._refreshIfNeeded(); - let playlist = await this._searchPlaylist( params['playlist']); - let track = await this._searchTrack(params['artist'], params['track']); - await this._spotify.addTracksToPlaylist(playlist.id, [track.uri]); - return { - URL: track.uri, - ARTIST: track.artists?.[0].name, - TRACK: track.name, - }; + await this._spotify.addTracksToPlaylist(params.playlistUri, ["spotify:track:" + params.trackUri]); + return {}; } } diff --git a/worker/src/services/youtube.ts b/worker/src/services/youtube.ts index ade2b6c..cd3f2b6 100644 --- a/worker/src/services/youtube.ts +++ b/worker/src/services/youtube.ts @@ -22,9 +22,19 @@ export class Youtube extends BaseService { refresh_token: pipeline.userData["Google"].refreshToken, access_token: pipeline.userData["Google"].accessToken, }); - // client.on("tokens", x => { - // - // }); + client.on("tokens", x => { + fetch(`${process.env["WORKER_API_URL"]}/google/${pipeline.userId}?WORKER_API_KEY=${process.env["WORKER_API_KEY"]}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + accessToken: x.access_token, + refreshToken: x.refresh_token, + expiresAt: x.expiry_date, + }), + }); + }); this._youtube = new youtube_v3.Youtube({ auth: client, });