diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..37f8642 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @cpsiaki @LinaBell @liebeskind \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 48a6653..06569ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -973,9 +973,9 @@ ] }, "node_modules/@rtsdk/topia": { - "version": "0.12.6", - "resolved": "https://registry.npmjs.org/@rtsdk/topia/-/topia-0.12.6.tgz", - "integrity": "sha512-YNMoz0Y/xZ1vZwFs+C+aXg8Qd7Z/QR32i5tIdYzbCUSxC173WgcyMmTW6cAy1RDFNxZQgPmH2H4ehieIpUxlJA==" + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/@rtsdk/topia/-/topia-0.15.4.tgz", + "integrity": "sha512-J1g/+yp61OvBx0YrloDiYiDeuO2/C64dFluaSx78+h+GSg7NSWINdQtrRl6qgcUTqNXMHbVZW4HwKAo58Fmx5A==" }, "node_modules/@swc/core": { "version": "1.4.8", @@ -5899,7 +5899,7 @@ "version": "0.1.0", "dependencies": { "@googleapis/youtube": "^14.0.0", - "@rtsdk/topia": "^0.12.6", + "@rtsdk/topia": "^0.15.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", diff --git a/server/controllers/media/AddMedia.ts b/server/controllers/media/AddMedia.ts index 33e103e..78e136e 100644 --- a/server/controllers/media/AddMedia.ts +++ b/server/controllers/media/AddMedia.ts @@ -7,6 +7,8 @@ import { Request, Response } from "express"; export default async function AddMedia(req: Request, res: Response) { const credentials = getCredentials(req.query); + const { interactiveNonce, profileId, urlSlug, visitorId } = credentials; + const { videos, type }: { videos: Video[] | string[]; type: "catalog" | "queue" } = req.body; const [isAdmin, jukeboxAsset] = await Promise.all([checkIsAdmin(credentials), getDroppedAsset(credentials)]); @@ -26,15 +28,15 @@ export default async function AddMedia(req: Request, res: Response) { analytics.push({ analyticName: "addsToCatalog", incrementBy: videos.length, - uniqueKey: credentials.urlSlug, - urlSlug: credentials.urlSlug, + uniqueKey: urlSlug, + urlSlug, }); } else if (type === "queue") { analytics.push({ analyticName: "addsToQueue", incrementBy: videos.length, - profileId: credentials.profileId, - uniqueKey: credentials.profileId, + profileId, + uniqueKey: profileId, }); } let firstVideo = null; @@ -42,7 +44,7 @@ export default async function AddMedia(req: Request, res: Response) { firstVideo = jukeboxAsset.dataObject.catalog.find((video: Video) => video.id.videoId === videos[0]); if (firstVideo) { const mediaLink = `https://www.youtube.com/watch?v=${firstVideo.id.videoId}`; - analytics.push({ analyticName: "plays", urlSlug: credentials.urlSlug, uniqueKey: credentials.urlSlug }); + analytics.push({ analyticName: "plays", urlSlug, uniqueKey: urlSlug }); promises.push( jukeboxAsset.updateMediaType({ mediaLink, @@ -55,7 +57,7 @@ export default async function AddMedia(req: Request, res: Response) { syncUserMedia: true, // Make it so everyone has the video synced instead of it playing from the beginning when they approach. }), ); - const world = World.create(credentials.urlSlug, { credentials }); + const world = World.create(urlSlug, { credentials }); world .triggerParticle({ name: "musicNote_float", @@ -91,9 +93,9 @@ export default async function AddMedia(req: Request, res: Response) { redisObj.publish(`${process.env.INTERACTIVE_KEY}_JUKEBOX`, { assetId: jukeboxAsset.id, videos: firstVideo ? [firstVideo.id.videoId, ...videos] : videos, - interactiveNonce: credentials.interactiveNonce, - urlSlug: credentials.urlSlug, - visitorId: credentials.visitorId, + interactiveNonce, + urlSlug, + visitorId, kind: type === "catalog" ? "addedToCatalog" : "addedToQueue", event: "mediaAction", }); diff --git a/server/controllers/media/GetJukeboxDataObject.ts b/server/controllers/media/GetJukeboxDataObject.ts index a933358..b376f97 100644 --- a/server/controllers/media/GetJukeboxDataObject.ts +++ b/server/controllers/media/GetJukeboxDataObject.ts @@ -6,6 +6,8 @@ import { checkIsAdmin } from "../../middleware/isAdmin.js"; export default async function GetJukeboxDataObject(req: Request, res: Response) { try { const credentials = getCredentials(req.query); + const { profileId, urlSlug } = credentials; + const jukeboxAsset = await getDroppedAsset(credentials); if (jukeboxAsset.error) { return res.status(404).json({ message: "Asset not found" }); @@ -18,9 +20,9 @@ export default async function GetJukeboxDataObject(req: Request, res: Response) analytics: [ { analyticName: "views", - profileId: credentials.profileId, - uniqueKey: credentials.profileId, - urlSlug: credentials.urlSlug, + profileId, + uniqueKey: profileId, + urlSlug, }, ], }, diff --git a/server/controllers/media/NextSong.ts b/server/controllers/media/NextSong.ts index 75c106c..e2a104d 100644 --- a/server/controllers/media/NextSong.ts +++ b/server/controllers/media/NextSong.ts @@ -23,6 +23,8 @@ export default async function NextSong(req: Request, res: Response) { } else { credentials = getCredentials(req.query); } + const { urlSlug } = credentials; + const jukeboxAsset = await getDroppedAsset(credentials); if (jukeboxAsset.error) { return res.status(404).json({ message: "Asset not found" }); @@ -34,6 +36,7 @@ export default async function NextSong(req: Request, res: Response) { let nowPlaying = "-1" as "-1" | Video; const promises = []; const analytics = []; + try { // Lock the asset dataObject before attempting to find the next available song // This would reduce YouTube API quota usage @@ -46,9 +49,11 @@ export default async function NextSong(req: Request, res: Response) { }, }, ); + if (queue.length > 0) { const [nextSong, index] = await findNextAvailableSong(queue, jukeboxAsset.dataObject.catalog); nowPlaying = nextSong; + if (nowPlaying) { remainingQueue = queue.slice(index + 1); const videoId = nowPlaying.id.videoId; @@ -56,7 +61,7 @@ export default async function NextSong(req: Request, res: Response) { const mediaLink = `https://www.youtube.com/watch?v=${videoId}`; - const world = World.create(credentials.urlSlug, { credentials }); + const world = World.create(urlSlug, { credentials }); world .triggerParticle({ name: "musicNote_float", @@ -68,6 +73,7 @@ export default async function NextSong(req: Request, res: Response) { }) .then() .catch(() => console.error("Cannot trigger particle")); + promises.push( jukeboxAsset.updateMediaType({ mediaLink, @@ -80,7 +86,7 @@ export default async function NextSong(req: Request, res: Response) { syncUserMedia: true, // Make it so everyone has the video synced instead of it playing from the beginning when they approach. }), ); - analytics.push({ analyticName: "plays", urlSlug: credentials.urlSlug, uniqueKey: credentials.urlSlug }); + analytics.push({ analyticName: "plays", urlSlug, uniqueKey: urlSlug }); } else { promises.push(jukeboxAsset.updateMediaType({ mediaType: "none" })); } diff --git a/server/controllers/media/RemoveMedia.ts b/server/controllers/media/RemoveMedia.ts index febe5af..36930c9 100644 --- a/server/controllers/media/RemoveMedia.ts +++ b/server/controllers/media/RemoveMedia.ts @@ -1,20 +1,22 @@ import redisObj from "../../redis-sse/index.js"; -import { Credentials, Video } from "../../types/index.js"; -import { getDroppedAsset } from "../../utils/index.js"; +import { Video } from "../../types/index.js"; +import { getCredentials, getDroppedAsset } from "../../utils/index.js"; import { Request, Response } from "express"; export default async function RemoveMedia(req: Request, res: Response) { - const { assetId, interactivePublicKey, interactiveNonce, urlSlug, visitorId } = req.query as Credentials; + const credentials = getCredentials(req.query); + const { interactiveNonce, urlSlug, visitorId } = credentials; const { videoIds, type }: { videoIds: string[]; type: "catalog" | "queue" } = req.body; - const credentials = { assetId, interactivePublicKey, interactiveNonce, urlSlug, visitorId }; const jukeboxAsset = await getDroppedAsset(credentials); if (jukeboxAsset.error) { return res.status(404).json({ message: "Asset not found" }); } + const timeFactor = new Date(Math.round(new Date().getTime() / 10000) * 10000); const lockId = `${jukeboxAsset.id}_${timeFactor}`; + try { const jukeboxUpdate: { catalog?: Video[]; @@ -29,6 +31,7 @@ export default async function RemoveMedia(req: Request, res: Response) { } else if (type === "queue") { jukeboxUpdate.queue = jukeboxAsset.dataObject.queue.filter((videoId: string) => !videoIds.includes(videoId)); } + await jukeboxAsset.updateDataObject( { ...jukeboxAsset.dataObject, @@ -41,6 +44,7 @@ export default async function RemoveMedia(req: Request, res: Response) { }, }, ); + redisObj.publish(`${process.env.INTERACTIVE_KEY}_JUKEBOX`, { assetId: jukeboxAsset.id, videos: videoIds, diff --git a/server/controllers/status/handleCheckInteractiveCredentials.ts b/server/controllers/status/handleCheckInteractiveCredentials.ts index 2e1baaf..faab2d2 100644 --- a/server/controllers/status/handleCheckInteractiveCredentials.ts +++ b/server/controllers/status/handleCheckInteractiveCredentials.ts @@ -13,6 +13,7 @@ export const handleCheckInteractiveCredentials = async ( // if key matches proceed with check using jwt created by topiaInit const user = User.create({ credentials }); await user.checkInteractiveCredentials(); + return res.json({ success: true }); } catch (error) { return errorHandler({ diff --git a/server/controllers/status/isAdminCheck.ts b/server/controllers/status/isAdminCheck.ts index 1fa42b9..a8e3e91 100644 --- a/server/controllers/status/isAdminCheck.ts +++ b/server/controllers/status/isAdminCheck.ts @@ -1,11 +1,10 @@ -import { Credentials } from "../../types/index.js"; -import { errorHandler, getVisitor } from "../../utils/index.js"; +import { errorHandler, getCredentials, getVisitor } from "../../utils/index.js"; import { Request, Response } from "express"; export default async function isAdminCheck(req: Request, res: Response) { try { - const { interactivePublicKey, interactiveNonce, urlSlug, visitorId } = req.query as Credentials; - const visitor = await getVisitor({ interactivePublicKey, interactiveNonce, urlSlug, visitorId }); + const credentials = getCredentials(req.query); + const visitor = await getVisitor(credentials); if (!visitor) { return res.status(404).json({ message: "Visitor not found" }); } else if (visitor.isAdmin) { @@ -18,7 +17,8 @@ export default async function isAdminCheck(req: Request, res: Response) { error, functionName: "isAdminCheck", message: "Error in Admin Check", - req, res + req, + res, }); } } diff --git a/server/middleware/isAdmin.ts b/server/middleware/isAdmin.ts index e2d8784..f4f61bf 100644 --- a/server/middleware/isAdmin.ts +++ b/server/middleware/isAdmin.ts @@ -1,19 +1,17 @@ -import { Credentials } from "../types/index.js"; -import { getVisitor } from "../utils/index.js"; +import { getCredentials, getVisitor } from "../utils/index.js"; import { Request, Response, NextFunction } from "express"; const checkIsAdmin = async (credentials) => { - const { interactivePublicKey, interactiveNonce, urlSlug, visitorId } = credentials; - const visitor = await getVisitor({ interactivePublicKey, interactiveNonce, urlSlug, visitorId }); + const visitor = await getVisitor(credentials); if (!visitor.isAdmin) { return false; } - return true -} + return true; +}; async function isAdmin(req: Request, res: Response, next: NextFunction) { - const { interactivePublicKey, interactiveNonce, urlSlug, visitorId } = req.query as Credentials; - const isAdmin = await checkIsAdmin({ interactivePublicKey, interactiveNonce, urlSlug, visitorId }); + const credentials = getCredentials(req.query); + const isAdmin = await checkIsAdmin(credentials); if (!isAdmin) { return res.status(401).json({ message: "Unauthorized" }); } diff --git a/server/package.json b/server/package.json index fb50706..d8efbcf 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@googleapis/youtube": "^14.0.0", - "@rtsdk/topia": "^0.12.6", + "@rtsdk/topia": "^0.15.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", diff --git a/server/utils/droppedAssets/getDroppedAsset.ts b/server/utils/droppedAssets/getDroppedAsset.ts index 4a68212..f7e7c74 100644 --- a/server/utils/droppedAssets/getDroppedAsset.ts +++ b/server/utils/droppedAssets/getDroppedAsset.ts @@ -7,13 +7,7 @@ export const getDroppedAsset = async (credentials: Credentials) => { const { assetId, interactivePublicKey, interactiveNonce, urlSlug, visitorId } = credentials; if (!assetId || !interactivePublicKey || !interactiveNonce || !urlSlug || !visitorId) throw "Invalid credentials"; - const droppedAsset = await DroppedAsset.get(assetId, urlSlug, { - credentials: { - interactiveNonce, - interactivePublicKey, - visitorId, - }, - }); + const droppedAsset = await DroppedAsset.get(assetId, urlSlug, { credentials }); if (!droppedAsset) throw "Dropped asset not found"; diff --git a/server/utils/visitors/getVisitor.ts b/server/utils/visitors/getVisitor.ts index 4b6fb77..33fe2ae 100644 --- a/server/utils/visitors/getVisitor.ts +++ b/server/utils/visitors/getVisitor.ts @@ -1,18 +1,12 @@ -import { Visitor } from "../topiaInit.js" -import { errorHandler } from "../errorHandler.js" +import { Visitor } from "../topiaInit.js"; +import { errorHandler } from "../errorHandler.js"; import { Credentials } from "../../types/index.js"; export const getVisitor = async (credentials: Credentials) => { try { - const { interactivePublicKey, interactiveNonce, urlSlug, visitorId } = credentials; + const { urlSlug, visitorId } = credentials; - const visitor = await Visitor.get(parseInt(visitorId), urlSlug, { - credentials: { - interactiveNonce, - interactivePublicKey, - visitorId: parseInt(visitorId), - }, - }); + const visitor = await Visitor.get(visitorId, urlSlug, { credentials }); // @ts-ignore if (!visitor || !visitor.username) throw "Not in world";