From 46dc3dc83ffe0b7d5ce92942a1b4a982477ba81f Mon Sep 17 00:00:00 2001 From: adb-sh Date: Mon, 17 Oct 2022 00:43:05 +0200 Subject: [PATCH] implement sessions; retry to create spotify client --- backend/api/auth.mjs | 10 ++++- backend/api/index.mjs | 2 + backend/api/session.mjs | 95 +++++++++++++++++++++++++++++++++++++++++ backend/api/user.mjs | 64 ++------------------------- backend/db/schemas.mjs | 22 ++++++++++ backend/lib/helpers.mjs | 47 +++++++++++--------- frontend | 2 +- 7 files changed, 158 insertions(+), 84 deletions(-) create mode 100644 backend/api/session.mjs diff --git a/backend/api/auth.mjs b/backend/api/auth.mjs index 123981b..ab054a0 100644 --- a/backend/api/auth.mjs +++ b/backend/api/auth.mjs @@ -1,6 +1,6 @@ import { store } from "../store.mjs"; import { randomString } from "../lib/randomString.mjs"; -import { UserStore } from "../db/schemas.mjs"; +import { SessionStore, UserStore } from "../db/schemas.mjs"; import { createLocalUser, findUserBySpotifyId } from "../lib/helpers.mjs"; export const applyAuthRoutes = (router) => { @@ -29,7 +29,6 @@ export const applyAuthRoutes = (router) => { { 'spotify.userId': newUser.client.user.id }, { accessToken, - role: 'none', spotify: { refreshToken: newUser.client.refreshMeta.refreshToken, userId: newUser.client.user.id, @@ -39,6 +38,13 @@ export const applyAuthRoutes = (router) => { { upsert: true }, ); + if (!await SessionStore.findOne().bySpotifyId(newUser.client.user.id)) + await new SessionStore({ + host: userStore, + clients: [], + queue: [], + }).save(); + res.status(200); res.send({ message: 'authorized', accessToken }); } catch (e) { diff --git a/backend/api/index.mjs b/backend/api/index.mjs index afe3f01..a30e9a1 100644 --- a/backend/api/index.mjs +++ b/backend/api/index.mjs @@ -1,4 +1,5 @@ import { applyUserRoutes, applyUserRoutesPublic } from "./user.mjs"; +import { applySessionRoutes } from "./session.mjs"; export const applyApiRoutes = (router) => { @@ -8,6 +9,7 @@ export const applyApiRoutes = (router) => { }); applyUserRoutes(router); + applySessionRoutes(router); }; diff --git a/backend/api/session.mjs b/backend/api/session.mjs new file mode 100644 index 0000000..98e6ede --- /dev/null +++ b/backend/api/session.mjs @@ -0,0 +1,95 @@ +import { SessionStore } from "../db/schemas.mjs"; + +export const applySessionRoutes = (router) => { + + router.post('/session', async (req, res) => { + const user = res.locals.user; + if (await SessionStore.findOne().bySpotifyId(user.id)) { + res.status(400); + res.send({ message: 'you are already in a session' }); + return; + } + const sessionStore = new SessionStore({ + host: user, + clients: [], + }); + await sessionStore.save(); + + res.status(201); + res.send({ message: 'created' }); + }); + + router.get('/session', async (req, res) => { + const user = res.locals.user; + const sessionStore = await SessionStore.findOne().byHostSpotifyId(user.id); + + if (!sessionStore) { + res.status(404); + res.send('you are not hosting a session'); + return; + } + + res.status(200); + res.send({ session: sessionStore }); + }); + + router.delete('/session', async (req, res) => { + const user = res.locals.user; + const sessionStore = await SessionStore.findOne().byHostSpotifyId(user.id); + + if (!sessionStore) { + res.status(404); + res.send('you are not hosting a session'); + return; + } + + await sessionStore.delete(); + + res.status(204); + res.send({ message: 'session deleted' }); + }); + + router.post('/session/join', async (req, res) => { + if (!req.body?.hostId) { + res.status(400); + res.send({ message: 'hostId is undefined' }); + return; + } + const { hostId } = req.body.hostId; + const user = await res.locals.user; + if (await SessionStore.findOne().bySpotifyId(user.id)) { + res.status(400); + res.send({ message: 'you are already in a session' }); + return; + } + const sessionStore = SessionStore.findOne().byHostSpotifyId(hostId); + if (!sessionStore) { + res.status(400); + res.send({ message: 'session does not exist' }); + return; + } + + sessionStore.clients.push(user); + await sessionStore.save(); + + res.status(200); + res.send({ message: 'you joined' }); + }); + + router.post('/session/leave', async (req, res) => { + const user = await res.locals.user; + const sessionStore = SessionStore.findOne().byClientSpotifyId(user.id); + if (!sessionStore) { + res.status(400); + res.send({ message: 'you are not a client of any session' }); + return; + } + + sessionStore.clients = sessionStore.clients.filter(client => client.id !== user.id); + await sessionStore.save(); + + res.status(200); + res.send({ message: 'you left' }); + }); + +}; diff --git a/backend/api/user.mjs b/backend/api/user.mjs index 24fdeec..c1f7149 100644 --- a/backend/api/user.mjs +++ b/backend/api/user.mjs @@ -1,79 +1,21 @@ import { store } from "../store.mjs"; -import { UserStore } from "../db/schemas.mjs"; +import { SessionStore, UserStore } from "../db/schemas.mjs"; export const applyUserRoutes = (router) => { applyUserRoutesPublic(router); - router.post('/user/joinSession', async (req, res) => { - if (!req.body?.userId) { - res.status(400); - res.send({ message: 'userId is undefined' }); - return; - } - const { userId } = req.body.userId; - if (res.locals.user.role === 'host') { - res.status(400); - res.send({ message: 'user is host' }); - return; - } - const host = store.users.find(({ client }) => client.user.id === userId) - host.listeners.push(res.locals.user); - res.locals.user.role = 'listener'; - res.locals.user.listeningTo = host; - res.status(200); - res.send({ message: 'joined' }); - }); - - router.delete('/user/leaveSession', async (req, res) => { - if (res.locals.user.role === 'host') { - res.status(400); - res.send({ message: 'user is host' }); - return; - } - const host = store.users.find(({ client }) => client.user.id === userId) - host.listeners.push(res.locals.user); - res.locals.user.role = 'none'; - res.locals.user.listeningTo.listeners = res.locals.user.listeningTo.listeners.filter( - ({ client }) => client.user.id !== res.locals.user.client.user.id - ); - res.locals.user.listeningTo = null; - res.status(200); - res.send({ message: 'left' }); - }); - - router.get('/user/currentlyPlaying', async (req, res) => { + router.get('/me/currentlyPlaying', async (req, res) => { const currentlyPlaying = await (await res.locals.user.spotify.local)?.player?.getCurrentlyPlaying('track'); res.status(200); res.send({ currentlyPlaying }); }); - router.get('/user/role', (req, res) => { + router.get('/me/role', (req, res) => { res.status(200); res.send({ role: res.locals.user?.role }); }); - /*router.post('/user/role', async (req, res) => { - if ( - req.body.role !== 'host' || - req.body.role !== 'listener' || - req.body.role !== 'none' - ) { - res.status(400); - res.send({ message: 'role value is invalid' }); - } - const { role } = req.body; - try { - res.locals.user.listeners = []; - res.locals.user.role = role; - res.status(200); - res.send({ role }); - } catch (e) { - res.status(500); - res.send({ message: 'server error' }); - } - });*/ - }; export const applyUserRoutesPublic = (router) => { diff --git a/backend/db/schemas.mjs b/backend/db/schemas.mjs index a808a8e..4929106 100644 --- a/backend/db/schemas.mjs +++ b/backend/db/schemas.mjs @@ -30,4 +30,26 @@ const userSchema = new Schema({ }, }); +const sessionSchema = new Schema({ + host: userSchema, + clients: Array, + queue: Array, +}, { + query: { + byHostSpotifyId(id) { + return this.where({ 'host.spotify.userId': id }); + }, + byClientSpotifyId(id) { + return this.where({ 'clients.userId': id }); + }, + bySpotifyId(id) { + return this.where({ '$or': [ + { 'host.spotify.userId': id }, + { 'clients.spotify.userId': id }, + ]}); + }, + }, +}); + export const UserStore = model('User', userSchema); +export const SessionStore = model('Session', sessionSchema); diff --git a/backend/lib/helpers.mjs b/backend/lib/helpers.mjs index 6322cd5..4dd7004 100644 --- a/backend/lib/helpers.mjs +++ b/backend/lib/helpers.mjs @@ -2,26 +2,33 @@ import { Client, Player } from "spotify-api.js"; import { store } from "../store.mjs"; import { UserStore } from "../db/schemas.mjs"; -export const createLocalUser = async ({ refreshToken = undefined, code = undefined }) => { - const client = await Client.create({ - refreshToken: true, - retryOnRateLimit: true, - token: { - clientID: store.clientID, - clientSecret: store.clientSecret, - redirectURL: store.redirectURL, - refreshToken, - code, - }, - async onRefresh() { - await UserStore.findOneAndUpdate( - { 'spotify.userId': client.user.id }, - { 'spotify.refreshToken': client.refreshMeta.refreshToken }, - ); - }, - }); - const player = new Player(client); - return { client, player }; +export const createLocalUser = async ({ refreshToken = undefined, code = undefined }, retry = 4) => { + try { + const client = await Client.create({ + refreshToken: true, + retryOnRateLimit: true, + token: { + clientID: store.clientID, + clientSecret: store.clientSecret, + redirectURL: store.redirectURL, + refreshToken, + code, + }, + async onRefresh() { + await UserStore.findOneAndUpdate( + { 'spotify.userId': client.user.id }, + { 'spotify.refreshToken': client.refreshMeta.refreshToken }, + ); + }, + }); + const player = new Player(client); + return { client, player }; + } catch (e) { + if (retry-- < 1) throw e; + if (e.response.data.status === 503) await new Promise(_ => setTimeout(_, 500)); + else throw e; + return await createLocalUser({ refreshToken, code }, retry); + } }; export const findUserBySpotifyId = async ( diff --git a/frontend b/frontend index f3b9591..cf96264 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit f3b95916688df6b4a9834bb5d4adfb9ceec6093d +Subproject commit cf9626402434ff2e2449457c40182bab92a8ad55