diff --git a/firebase.json b/firebase.json index 22905bb..21551a1 100644 --- a/firebase.json +++ b/firebase.json @@ -12,5 +12,19 @@ "**/.*", "**/node_modules/**" ] + }, + "emulators": { + "functions": { + "port": 5001 + }, + "firestore": { + "port": 8080 + }, + "hosting": { + "port": 5000 + }, + "ui": { + "enabled": true + } } } diff --git a/functions/app.js b/functions/app.js new file mode 100644 index 0000000..743d13e --- /dev/null +++ b/functions/app.js @@ -0,0 +1,16 @@ +const FunctionModules = require('./modules/index'); +const express = require('express'); +const cors = require('cors'); +const app = express(); + +app.use(cors({ origin: true })); +app.use(FunctionModules.authenticator); + +app.post('/create', FunctionModules.createRoom); +app.post('/join', FunctionModules.joinRoom); +app.post('/message', FunctionModules.sendMessage); +app.delete('/leave/:rid/users/:uid', FunctionModules.leaveRoom); +app.delete('/delete/:rid', FunctionModules.deleteRoom); +app.post('/updatePlayer', FunctionModules.updatePlayer); + +module.exports = app; diff --git a/functions/index.js b/functions/index.js index e3db650..c45356a 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,500 +1,22 @@ +const FunctionModules = require('./modules/index'); const functions = require('firebase-functions'); -const admin = require('firebase-admin'); const secret = require('./FirebaseSecret.json'); -const express = require('express'); -const cors = require('cors'); +const admin = require('firebase-admin'); +const REST = require('./app'); -const authenticator = (req, res, next) => { - res.set('Access-Control-Allow-Origin', ['*']); - if (req.method === 'OPTIONS') { - res.status(200).send(); - return next(); - } - const token = req.headers.authorization.split(' ')[1]; - const reqToken = token.split(':')[0]; - const reqUid = token.split(':')[1]; - admin - .auth() - .verifyIdToken(reqToken) - .then((user) => { - if (user.uid === reqUid) { - console.info('Verified Token - UID - ', reqUid); - if (req.method === 'POST') { - const bodydata = JSON.parse(req.body); - bodydata.uid = user.uid; - req.body = JSON.stringify(bodydata); - } - if (req.method === 'GET' || req.method === 'DELETE') { - req.body = JSON.stringify({ uid: user.uid }); - } - return next(); - } else { - return res.status(401).send({ message: 'Unauthorized' }); - } - }) - .catch((err) => { - console.error('Failed Auth - ', req.headers.authorization, '\n', err); - return res.status(500).send({ message: 'Unable to verify user token', ...err }); - }); - return null; -}; - -const timeDiff = (ds1, ds2) => { - const d1 = new Date(ds1); - const d2 = new Date(ds2); - var diff = (d2.getTime() - d1.getTime()) / 1000; - diff /= 60 * 60; - return Math.abs(Math.round(diff)); -}; - -const cleanUpRoom = async (roomId) => { - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('messages') - .listDocuments() - .then((messages) => messages.forEach((message) => message.delete())) - .catch((err) => { - console.error(`Failed to run auto clean up (delete room messages) @ ${roomId}`, err); - }); - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('users') - .listDocuments() - .then((users) => users.forEach((user) => user.delete())) - .catch((err) => { - console.error(`Failed to run auto clean up (delete room users) @ ${roomId}`, err); - }); - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .delete() - .catch((err) => { - console.error(`Failed to run auto clean up (delete room) @ ${roomId}`, err); - }); -}; - -const createRoom = async (req, res) => { - const bodydata = JSON.parse(req.body); - console.log('Requested Create - UID - ', bodydata.uid); - if (!bodydata.userName) res.status(400).send({ message: 'Missing user name' }); - const roomId = await admin - .firestore() - .collection('rooms') - .add({ - name: bodydata.roomName, - player: bodydata.player, - createdAt: new Date(), - updatedAt: new Date(), - }) - .then((ref) => ref.id) - .catch((err) => { - res.status(500).send({ message: 'Unable to start party.' }); - console.error(err); - }); - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('messages') - .add({ - uid: '', - name: '', - text: `${bodydata.userName} started the party.`, - time: new Date(), - }) - .catch((err) => { - console.error(`Failed to add system create party message : \n\t Room: ${roomId}`, err); - }); - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('users') - .doc(bodydata.uid) - .set({ - admin: true, - name: bodydata.userName, - }) - .catch((err) => { - console.error( - `Failed to set admin when creating party : \n\t Room: ${roomId} \n\t User: ${bodydata.uid}`, - err - ); - }); - res.status(201).send({ roomId, message: 'Started a party!' }); -}; - -const joinRoom = async (req, res) => { - const bodydata = JSON.parse(req.body); - console.log('Requested Join - UID - ', bodydata.uid, '\n\tRoom - ', bodydata.roomId); - const [exists, roomSource] = await admin - .firestore() - .collection('rooms') - .doc(bodydata.roomId) - .get() - .then((roomSnap) => [roomSnap.exists, roomSnap.exists ? roomSnap.data().player.source : '']) - .catch((err) => { - console.error( - `Failed to get room when joining room : \n\t Room: ${bodydata.roomId} \n\t User: ${bodydata.uid}`, - err - ); - }); - const [userExists, userAdmin] = await admin - .firestore() - .collection('rooms') - .doc(bodydata.roomId) - .collection('users') - .doc(bodydata.uid) - .get() - .then((userSnap) => [userSnap.exists, userSnap.exists ? userSnap.data().admin : false]) - .catch((err) => { - console.error( - `Failed to get user when joining room : \n\t Room: ${bodydata.roomId} \n\t User: ${bodydata.uid}`, - err - ); - }); - if (userExists && exists && roomSource === bodydata.source) - res.status(200).send({ roomId: bodydata.roomId, message: 'Joined a party' }); - else if ( - !userExists && - exists && - bodydata.roomId && - bodydata.userName && - roomSource === bodydata.source - ) { - await admin - .firestore() - .collection('rooms') - .doc(bodydata.roomId) - .collection('messages') - .add({ - uid: '', - name: '', - text: `${bodydata.userName} joined the party.`, - time: new Date(), - }) - .catch((err) => { - console.error( - `Failed to add system join party message : \n\t Room: ${roomId} \n\t ${bodydata.uid}`, - err - ); - }); - await admin - .firestore() - .collection('rooms') - .doc(bodydata.roomId) - .collection('users') - .doc(bodydata.uid) - .set({ - admin: false, - name: bodydata.userName, - }) - .catch((err) => { - console.error( - `Failed to set admin when joining party : \n\t Room: ${roomId} \n\t User: ${bodydata.uid}`, - err - ); - }); - await admin - .firestore() - .collection('rooms') - .doc(bodydata.roomId) - .update({ - updatedAt: new Date(), - }) - .catch((err) => { - console.error( - `Failed to set updated time when joining party : \n\t Room: ${roomId} \n\t User: ${bodydata.uid}`, - err - ); - }); - res.status(200).send({ roomId: bodydata.roomId, message: 'Joined a party' }); - } else if (!userExists && !bodydata.userName) - res.status(400).send({ message: 'Missing user name' }); - else if (exists && roomSource !== bodydata.source) { - if (userAdmin === true) { - await admin - .firestore() - .collection('rooms') - .doc(bodydata.roomId) - .update({ - player: { - showName: null, - source: roomSource, - status: 'NOT_PLAYING', - time: 0, - updatedBy: bodydata.uid, - }, - }) - .catch((err) => { - console.error( - `Failed to update player source when admin joining party \n\t Room: ${roomId} \n\t User: ${bodydata.uid}` - ); - res.status(500).send({ message: 'Unable to move party, please try again' }); - }); - res.status(200).send({ message: `Moved party to ${roomSource}` }); - } else res.status(417).send({ message: `This party is on ${roomSource}` }); - } else res.status(404).send({ message: 'Party Not Found' }); -}; - -const sendMessage = async (req, res) => { - const { name, text, time, uid, roomId, type } = JSON.parse(req.body); - - const roomExists = await admin - .firestore() - .collection('rooms') - .doc(roomId) - .get() - .then((roomSnap) => roomSnap.exists); - if (roomExists) { - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('messages') - .add({ - name, - text: text || '', - time: new Date(), - uid, - type: type || '', - }) - .catch((err) => { - console.error(`Failed to send message to party : \n\t Room: ${roomId} \n\t ${uid}`, err); - res.status(500).send({ message: "Couldn't send your message", ...messageErr }); - }); - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .update({ - updatedAt: new Date(), - }) - .catch((err) => { - console.error( - `Failed to set updated time when sending messasge to party : \n\t Room: ${roomId} \n\t User: ${uid}`, - err - ); - }); - res.status(200).send({ message: 'Sent a message' }); - } else { - res.status(404).send({ message: 'Looks like this party is over' }); - } -}; - -const leaveRoom = async (req, res) => { - console.log('Requested Leave - UID - ', req.params.uid, '\n\tRoom - ', req.params.rid); - await admin - .firestore() - .collection('rooms') - .doc(req.params.rid) - .collection('users') - .doc(req.params.uid) - .delete() - .catch((err) => - console.error( - `Failed to remove user from party : \n\t Room: ${req.params.rid} \n\t User: ${req.params.uid}`, - err - ) - ); - res.status(200).send({ message: 'Left the party' }); -}; - -const deleteRoom = async (req, res) => { - console.log('Requested Delete - Room - ', req.params.rid); - console.log(req.body); - const isAdmin = await admin - .firestore() - .collection('rooms') - .doc(req.params.rid) - .collection('users') - .doc(JSON.parse(req.body).uid) - .get() - .then((v) => { - if (v.exists) { - return v.data().admin; - } - return false; - }); - if (!isAdmin) { - console.log('[UNAUTHORIZED] Requested Delete - Room - ', req.params.rid); - res.status(401).send({ message: 'Not authorized to end this party.' }); - return; - } - await admin - .firestore() - .collection('rooms') - .doc(req.params.rid) - .collection('messages') - .listDocuments() - .then((messages) => messages.forEach((message) => message.delete())) - .catch((err) => { - console.error( - `Failed to delete room messages : \n\t Room: ${req.params.rid} \n\t User: ${req.params.uid}`, - err - ); - }); - await admin - .firestore() - .collection('rooms') - .doc(req.params.rid) - .collection('users') - .listDocuments() - .then((users) => users.forEach((user) => user.delete())) - .catch((err) => { - console.error( - `Failed to delete room users : \n\t Room: ${req.params.rid} \n\t User: ${req.params.uid}`, - err - ); - }); - await admin - .firestore() - .collection('rooms') - .doc(req.params.rid) - .delete() - .catch((err) => { - console.error( - `Failed to delete room : \n\t Room: ${req.params.rid} \n\t User: ${req.params.uid}`, - err - ); - }); - res.status(200).send({ message: 'Ended the party' }); -}; - -const updatePlayer = async (req, res) => { - const { player, roomId, uid, showChanged, userName } = JSON.parse(req.body); - const statusString = - player.status === 'PLAYING' - ? 'started playing from' - : player.status === 'PAUSED' - ? 'paused at' - : player.status === 'NOT_PLAYING' - ? 'stopped at' - : 'is at'; - const timeString = - player.time > 3600 - ? new Date(player.time * 1000).toISOString().substr(11, 8) - : new Date(player.time * 1000).toISOString().substr(14, 5); - const [roomExists, roomData] = await admin - .firestore() - .collection('rooms') - .doc(roomId) - .get() - .then((docSnap) => [docSnap.exists, docSnap.data()]); - if (!roomExists) return res.status(404).send({ message: 'Party not found.' }); - const userExists = await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('users') - .doc(uid) - .get() - .then((userSnap) => userSnap.exists); - if (userExists) { - if (showChanged === true) { - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('messages') - .add({ - uid: '', - name: '', - text: `${userName} changed \n${player.showName ? 'to ' + player.showName : ''}`, - time: new Date(), - }); - } - if (roomData.status !== player.status) { - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .update({ - player, - }) - .catch((err) => { - console.error(`Failed to update player : \n\t Room: ${roomId} \n\t ${uid}`, err); - res.status(500).send({ message: "Uh oh party could't be synced", ...err }); - }); - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('messages') - .add({ - uid: '', - name: '', - text: `${userName} ${statusString} ${timeString}`, - time: new Date(), - }) - .catch((err) => { - console.error( - `Failed to add system update player message : \n\t Room: ${roomId} \n\t ${uid}`, - err - ); - }); - return res.status(200).send({ message: 'Synced party player' }); - } else { - return res.status(208).send({ message: 'Player already up to date' }); - } - } else { - return res.status(401).send({ message: 'Not in room to sync' }); - } -}; - -const app = express(); -app.use(cors({ origin: true })); -app.use(authenticator); - -admin.initializeApp({ +const FIREBASE_OPTIONS = { credential: admin.credential.cert(secret), databaseURL: 'https://watch-party-crx.firebaseio.com', -}); +}; +if (process.env.NODE_ENV === 'DEV') { + FIREBASE_OPTIONS.databaseURL = 'http://localhost:4000/firestore'; + console.info('RUNNING IN DEV ENV'); +} -app.post('/create', createRoom); -app.post('/join', joinRoom); -app.post('/message', sendMessage); -app.delete('/leave/:rid/users/:uid', leaveRoom); -app.delete('/delete/:rid', deleteRoom); -app.post('/updatePlayer', updatePlayer); +admin.initializeApp(FIREBASE_OPTIONS); -exports.widgets = functions.https.onRequest(app); +exports.widgets = functions.https.onRequest(REST); -exports.scheduledCleanUp = functions.pubsub.schedule('every 60 minutes').onRun(async () => { - const startedTime = new Date(); - const roomList = await admin - .firestore() - .collection('rooms') - .listDocuments() - .then((rL) => rL) - .catch((err) => { - console.error( - `Failed to run auto clean up (get room list) @ ${new Date().toLocaleTimeString()}`, - err - ); - }); - roomList.forEach((roomRef) => { - roomRef - .get() - .then(async (roomSnap) => { - if ( - timeDiff(roomSnap.createTime.toDate(), startedTime) >= 24 || - timeDiff(roomSnap.updateTime.toDate(), startedTime) >= 12 - ) - await cleanUpRoom(roomSnap.id); - console.info(`Deleted room ${roomSnap.id} from auto cleanup`); - return; - }) - .catch((err) => { - console.error( - `Failed to run auto clean up (get room snap) @ ${new Date().toLocaleTimeString()}`, - err - ); - }); - }); -}); +exports.scheduledCleanUp = functions.pubsub + .schedule('every 60 minutes') + .onRun(FunctionModules.scheduledCleanUp); diff --git a/functions/modules/authenticator.js b/functions/modules/authenticator.js new file mode 100644 index 0000000..e2d0e1e --- /dev/null +++ b/functions/modules/authenticator.js @@ -0,0 +1,34 @@ +module.exports = (req, res, next) => { + res.set('Access-Control-Allow-Origin', ['*']); + if (req.method === 'OPTIONS') { + res.status(200).send(); + return next(); + } + const token = req.headers.authorization.split(' ')[1]; + const reqToken = token.split(':')[0]; + const reqUid = token.split(':')[1]; + admin + .auth() + .verifyIdToken(reqToken) + .then((user) => { + if (user.uid === reqUid) { + console.info('Verified Token - UID - ', reqUid); + if (req.method === 'POST') { + const bodydata = JSON.parse(req.body); + bodydata.uid = user.uid; + req.body = JSON.stringify(bodydata); + } + if (req.method === 'GET' || req.method === 'DELETE') { + req.body = JSON.stringify({ uid: user.uid }); + } + return next(); + } else { + return res.status(401).send({ message: 'Unauthorized' }); + } + }) + .catch((err) => { + console.error('Failed Auth - ', req.headers.authorization, '\n', err); + return res.status(500).send({ message: 'Unable to verify user token', ...err }); + }); + return null; +}; diff --git a/functions/modules/cleanUpRoom.js b/functions/modules/cleanUpRoom.js new file mode 100644 index 0000000..f33a8ff --- /dev/null +++ b/functions/modules/cleanUpRoom.js @@ -0,0 +1,32 @@ +const admin = require('firebase-admin'); + +module.exports = async (roomId) => { + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('messages') + .listDocuments() + .then((messages) => messages.forEach((message) => message.delete())) + .catch((err) => { + console.error(`Failed to run auto clean up (delete room messages) @ ${roomId}`, err); + }); + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('users') + .listDocuments() + .then((users) => users.forEach((user) => user.delete())) + .catch((err) => { + console.error(`Failed to run auto clean up (delete room users) @ ${roomId}`, err); + }); + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .delete() + .catch((err) => { + console.error(`Failed to run auto clean up (delete room) @ ${roomId}`, err); + }); +}; diff --git a/functions/modules/createRoom.js b/functions/modules/createRoom.js new file mode 100644 index 0000000..9e8cd71 --- /dev/null +++ b/functions/modules/createRoom.js @@ -0,0 +1,52 @@ +const admin = require('firebase-admin'); + +module.exports = async (req, res) => { + const bodydata = JSON.parse(req.body); + console.log('Requested Create - UID - ', bodydata.uid); + if (!bodydata.userName) res.status(400).send({ message: 'Missing user name' }); + const roomId = await admin + .firestore() + .collection('rooms') + .add({ + name: bodydata.roomName, + player: bodydata.player, + createdAt: new Date(), + updatedAt: new Date(), + }) + .then((ref) => ref.id) + .catch((err) => { + res.status(500).send({ message: 'Unable to start party.' }); + console.error(err); + }); + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('messages') + .add({ + uid: '', + name: '', + text: `${bodydata.userName} started the party.`, + time: new Date(), + }) + .catch((err) => { + console.error(`Failed to add system create party message : \n\t Room: ${roomId}`, err); + }); + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('users') + .doc(bodydata.uid) + .set({ + admin: true, + name: bodydata.userName, + }) + .catch((err) => { + console.error( + `Failed to set admin when creating party : \n\t Room: ${roomId} \n\t User: ${bodydata.uid}`, + err + ); + }); + res.status(201).send({ roomId, message: 'Started a party!' }); +}; diff --git a/functions/modules/deleteRoom.js b/functions/modules/deleteRoom.js new file mode 100644 index 0000000..e0b37fb --- /dev/null +++ b/functions/modules/deleteRoom.js @@ -0,0 +1,62 @@ +const admin = require('firebase-admin'); + +module.exports = async (req, res) => { + console.log('Requested Delete - Room - ', req.params.rid); + console.log(req.body); + const isAdmin = await admin + .firestore() + .collection('rooms') + .doc(req.params.rid) + .collection('users') + .doc(JSON.parse(req.body).uid) + .get() + .then((v) => { + if (v.exists) { + return v.data().admin; + } + return false; + }); + if (!isAdmin) { + console.log('[UNAUTHORIZED] Requested Delete - Room - ', req.params.rid); + res.status(401).send({ message: 'Not authorized to end this party.' }); + return; + } + await admin + .firestore() + .collection('rooms') + .doc(req.params.rid) + .collection('messages') + .listDocuments() + .then((messages) => messages.forEach((message) => message.delete())) + .catch((err) => { + console.error( + `Failed to delete room messages : \n\t Room: ${req.params.rid} \n\t User: ${req.params.uid}`, + err + ); + }); + await admin + .firestore() + .collection('rooms') + .doc(req.params.rid) + .collection('users') + .listDocuments() + .then((users) => users.forEach((user) => user.delete())) + .catch((err) => { + console.error( + `Failed to delete room users : \n\t Room: ${req.params.rid} \n\t User: ${req.params.uid}`, + err + ); + }); + await admin + .firestore() + .collection('rooms') + .doc(req.params.rid) + .delete() + .catch((err) => { + console.error( + `Failed to delete room : \n\t Room: ${req.params.rid} \n\t User: ${req.params.uid}`, + err + ); + }); + res.status(200).send({ message: 'Ended the party' }); +}; diff --git a/functions/modules/index.js b/functions/modules/index.js new file mode 100644 index 0000000..aabb20c --- /dev/null +++ b/functions/modules/index.js @@ -0,0 +1,23 @@ +const authenticator = require('./authenticator'); +const cleanUpRoom = require('./cleanUpRoom'); +const scheduledCleanUp = require('./scheduledCleanUp'); +const createRoom = require('./createRoom'); +const joinRoom = require('./joinRoom'); +const sendMessage = require('./sendMessage'); +const leaveRoom = require('./leaveRoom'); +const deleteRoom = require('./deleteRoom'); +const timeDiff = require('./timeDiff'); +const updatePlayer = require('./updatePlayer'); + +module.exports = { + authenticator, + createRoom, + joinRoom, + sendMessage, + updatePlayer, + leaveRoom, + deleteRoom, + scheduledCleanUp, + cleanUpRoom, + timeDiff, +}; diff --git a/functions/modules/joinRoom.js b/functions/modules/joinRoom.js new file mode 100644 index 0000000..d68f97d --- /dev/null +++ b/functions/modules/joinRoom.js @@ -0,0 +1,114 @@ +const admin = require('firebase-admin'); + +module.exports = async (req, res) => { + const bodydata = JSON.parse(req.body); + console.log('Requested Join - UID - ', bodydata.uid, '\n\tRoom - ', bodydata.roomId); + const [exists, roomSource] = await admin + .firestore() + .collection('rooms') + .doc(bodydata.roomId) + .get() + .then((roomSnap) => [roomSnap.exists, roomSnap.exists ? roomSnap.data().player.source : '']) + .catch((err) => { + console.error( + `Failed to get room when joining room : \n\t Room: ${bodydata.roomId} \n\t User: ${bodydata.uid}`, + err + ); + }); + const [userExists, userAdmin] = await admin + .firestore() + .collection('rooms') + .doc(bodydata.roomId) + .collection('users') + .doc(bodydata.uid) + .get() + .then((userSnap) => [userSnap.exists, userSnap.exists ? userSnap.data().admin : false]) + .catch((err) => { + console.error( + `Failed to get user when joining room : \n\t Room: ${bodydata.roomId} \n\t User: ${bodydata.uid}`, + err + ); + }); + if (userExists && exists && roomSource === bodydata.source) + res.status(200).send({ roomId: bodydata.roomId, message: 'Joined a party' }); + else if ( + !userExists && + exists && + bodydata.roomId && + bodydata.userName && + roomSource === bodydata.source + ) { + await admin + .firestore() + .collection('rooms') + .doc(bodydata.roomId) + .collection('messages') + .add({ + uid: '', + name: '', + text: `${bodydata.userName} joined the party.`, + time: new Date(), + }) + .catch((err) => { + console.error( + `Failed to add system join party message : \n\t Room: ${roomId} \n\t ${bodydata.uid}`, + err + ); + }); + await admin + .firestore() + .collection('rooms') + .doc(bodydata.roomId) + .collection('users') + .doc(bodydata.uid) + .set({ + admin: false, + name: bodydata.userName, + }) + .catch((err) => { + console.error( + `Failed to set admin when joining party : \n\t Room: ${roomId} \n\t User: ${bodydata.uid}`, + err + ); + }); + await admin + .firestore() + .collection('rooms') + .doc(bodydata.roomId) + .update({ + updatedAt: new Date(), + }) + .catch((err) => { + console.error( + `Failed to set updated time when joining party : \n\t Room: ${roomId} \n\t User: ${bodydata.uid}`, + err + ); + }); + res.status(200).send({ roomId: bodydata.roomId, message: 'Joined a party' }); + } else if (!userExists && !bodydata.userName) + res.status(400).send({ message: 'Missing user name' }); + else if (exists && roomSource !== bodydata.source) { + if (userAdmin === true) { + await admin + .firestore() + .collection('rooms') + .doc(bodydata.roomId) + .update({ + player: { + showName: null, + source: roomSource, + status: 'NOT_PLAYING', + time: 0, + updatedBy: bodydata.uid, + }, + }) + .catch(() => { + console.error( + `Failed to update player source when admin joining party \n\t Room: ${roomId} \n\t User: ${bodydata.uid}` + ); + res.status(500).send({ message: 'Unable to move party, please try again' }); + }); + res.status(200).send({ message: `Moved party to ${roomSource}` }); + } else res.status(417).send({ message: `This party is on ${roomSource}` }); + } else res.status(404).send({ message: 'Party Not Found' }); +}; diff --git a/functions/modules/leaveRoom.js b/functions/modules/leaveRoom.js new file mode 100644 index 0000000..fe0ba5b --- /dev/null +++ b/functions/modules/leaveRoom.js @@ -0,0 +1,19 @@ +const admin = require('firebase-admin'); + +module.exports = async (req, res) => { + console.log('Requested Leave - UID - ', req.params.uid, '\n\tRoom - ', req.params.rid); + await admin + .firestore() + .collection('rooms') + .doc(req.params.rid) + .collection('users') + .doc(req.params.uid) + .delete() + .catch((err) => + console.error( + `Failed to remove user from party : \n\t Room: ${req.params.rid} \n\t User: ${req.params.uid}`, + err + ) + ); + res.status(200).send({ message: 'Left the party' }); +}; diff --git a/functions/modules/scheduledCleanUp.js b/functions/modules/scheduledCleanUp.js new file mode 100644 index 0000000..8fcb181 --- /dev/null +++ b/functions/modules/scheduledCleanUp.js @@ -0,0 +1,36 @@ +const admin = require('firebase-admin'); +const FunctionModules = require('./index'); + +module.exports = async () => { + const startedTime = new Date(); + const roomList = await admin + .firestore() + .collection('rooms') + .listDocuments() + .then((rL) => rL) + .catch((err) => { + console.error( + `Failed to run auto clean up (get room list) @ ${new Date().toLocaleTimeString()}`, + err + ); + }); + roomList.forEach((roomRef) => { + roomRef + .get() + .then(async (roomSnap) => { + if ( + FunctionModules.timeDiff(roomSnap.createTime.toDate(), startedTime) >= 24 || + FunctionModules.timeDiff(roomSnap.updateTime.toDate(), startedTime) >= 12 + ) + await FunctionModules.cleanUpRoom(roomSnap.id); + console.info(`Deleted room ${roomSnap.id} from auto cleanup`); + return; + }) + .catch((err) => { + console.error( + `Failed to run auto clean up (get room snap) @ ${new Date().toLocaleTimeString()}`, + err + ); + }); + }); +}; diff --git a/functions/modules/sendMessage.js b/functions/modules/sendMessage.js new file mode 100644 index 0000000..577d89f --- /dev/null +++ b/functions/modules/sendMessage.js @@ -0,0 +1,46 @@ +const admin = require('firebase-admin'); + +module.exports = async (req, res) => { + const { name, text, time, uid, roomId, type } = JSON.parse(req.body); + + const roomExists = await admin + .firestore() + .collection('rooms') + .doc(roomId) + .get() + .then((roomSnap) => roomSnap.exists); + if (roomExists) { + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('messages') + .add({ + name, + text: text || '', + time: new Date(), + uid, + type: type || '', + }) + .catch((err) => { + console.error(`Failed to send message to party : \n\t Room: ${roomId} \n\t ${uid}`, err); + res.status(500).send({ message: "Couldn't send your message", ...messageErr }); + }); + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .update({ + updatedAt: new Date(), + }) + .catch((err) => { + console.error( + `Failed to set updated time when sending messasge to party : \n\t Room: ${roomId} \n\t User: ${uid}`, + err + ); + }); + res.status(200).send({ message: 'Sent a message' }); + } else { + res.status(404).send({ message: 'Looks like this party is over' }); + } +}; diff --git a/functions/modules/timeDiff.js b/functions/modules/timeDiff.js new file mode 100644 index 0000000..5acc187 --- /dev/null +++ b/functions/modules/timeDiff.js @@ -0,0 +1,7 @@ +module.exports = (ds1, ds2) => { + const d1 = new Date(ds1); + const d2 = new Date(ds2); + var diff = (d2.getTime() - d1.getTime()) / 1000; + diff /= 60 * 60; + return Math.abs(Math.round(diff)); +}; diff --git a/functions/modules/updatePlayer.js b/functions/modules/updatePlayer.js new file mode 100644 index 0000000..a38087c --- /dev/null +++ b/functions/modules/updatePlayer.js @@ -0,0 +1,82 @@ +const admin = require('firebase-admin'); + +module.exports = async (req, res) => { + const { player, roomId, uid, showChanged, userName } = JSON.parse(req.body); + const statusString = + player.status === 'PLAYING' + ? 'started playing from' + : player.status === 'PAUSED' + ? 'paused at' + : player.status === 'NOT_PLAYING' + ? 'stopped at' + : 'is at'; + const timeString = + player.time > 3600 + ? new Date(player.time * 1000).toISOString().substr(11, 8) + : new Date(player.time * 1000).toISOString().substr(14, 5); + const [roomExists, roomData] = await admin + .firestore() + .collection('rooms') + .doc(roomId) + .get() + .then((docSnap) => [docSnap.exists, docSnap.data()]); + if (!roomExists) return res.status(404).send({ message: 'Party not found.' }); + const userExists = await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('users') + .doc(uid) + .get() + .then((userSnap) => userSnap.exists); + if (userExists) { + if (showChanged === true) { + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('messages') + .add({ + uid: '', + name: '', + text: `${userName} changed \n${player.showName ? 'to ' + player.showName : ''}`, + time: new Date(), + }); + } + if (roomData.status !== player.status) { + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .update({ + player, + }) + .catch((err) => { + console.error(`Failed to update player : \n\t Room: ${roomId} \n\t ${uid}`, err); + res.status(500).send({ message: "Uh oh party could't be synced", err }); + }); + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('messages') + .add({ + uid: '', + name: '', + text: `${userName} ${statusString} ${timeString}`, + time: new Date(), + }) + .catch((err) => { + console.error( + `Failed to add system update player message : \n\t Room: ${roomId} \n\t ${uid}`, + err + ); + }); + return res.status(200).send({ message: 'Synced party player' }); + } else { + return res.status(208).send({ message: 'Player already up to date' }); + } + } else { + return res.status(401).send({ message: 'Not in room to sync' }); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4fb0355 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "watch-party", + "version": "1.2.1", + "description": "Watch Party", + "scripts": { + "start": "env NODE_ENV=DEV firebase emulators:start", + "build": "cd ./app && node ./scripts/build.js" + }, + "private": true +}