diff --git a/app/src/Api.js b/app/src/Api.js index d33b65b..3a5dcf1 100644 --- a/app/src/Api.js +++ b/app/src/Api.js @@ -149,9 +149,9 @@ export const UpdatePlayerApi = async (data) => { }); }; -export const GetVoxeetTokenApi = async () => { +export const GetVoxeetTokenApi = async (data) => { return fetcher({ - endpoint: 'validator', + endpoint: `validator/${data.partyName}`, method: 'GET', }); }; diff --git a/app/src/CallManager.js b/app/src/CallManager.js index bfc51d7..be96e4f 100644 --- a/app/src/CallManager.js +++ b/app/src/CallManager.js @@ -33,10 +33,12 @@ export default class CallManager { this.__accessToken = LocalData().vrT; this.__sessionInitialized = false; this.__admin = false; + this.__adminOnly = true; } - toggleCall(onCall, conference, admin) { + toggleCall(onCall, conference, admin, adminOnly) { this.listener({ callActionPending: true }); this.__admin = admin; + this.__adminOnly = adminOnly; if (onCall === true) this.leaveCall(); else this.boot(conference); } @@ -56,13 +58,17 @@ export default class CallManager { console.error('Failed to fetch conference', err); this.startCall(); }); - } else if (opened && !conference) this.startCall(); - else console.error('Unable to open Voxeet Session'); + } else if (opened && !conference) { + this.startCall(); + } else { + this.listener({ onCall: false, muted: false, callActionPending: false }); + console.error('Unable to open Voxeet Session'); + } } async startCall() { - if (this.__admin === true) { + if (!this.__adminOnly || (this.__adminOnly && this.__admin)) { VoxeetSDK.conference - .create({ alias: LocalData().partyName, pin: LocalData().partyPin }) + .create({ alias: `AUTODELETE_${LocalData().partyName}`, pin: LocalData().partyPin }) .then(async (conference) => { const updatedVoxeet = await UpdateVoxeetApi({ conference, @@ -72,10 +78,21 @@ export default class CallManager { }) .then((res) => res.json()) .then((res) => res.status <= 299) - .catch((err) => console.error('Update Voxeet API Error', err)); + .catch((err) => { + if (err.conference) { + this.joinCall(err.conference); + return true; + } else { + this.notify(err.message); + this.listener({ onCall: false, muted: false, callActionPending: false }); + console.error('Update Voxeet API Error', err); + return false; + } + }); if (updatedVoxeet === true) { this.joinCall(conference); } else { + this.listener({ onCall: false, muted: false, callActionPending: false }); console.error('Failed to update party Voxeet'); } }); @@ -87,7 +104,13 @@ export default class CallManager { async joinCall(conference) { const joined = await VoxeetSDK.conference .join(conference, { constraints: { audio: true, video: false } }) - .then(() => true); + .then(() => true) + .catch(e => { + this.listener({ onCall: false, muted: false, callActionPending: false }); + this.notify('Unable to join call. Try again in a moment.'); + console.error('Error joining call', e); + return true; + }); if (!joined) { this.startCall(); } @@ -158,7 +181,7 @@ export default class CallManager { this.__accessToken !== null || this.__refreshToken !== null ) { - await GetVoxeetTokenApi() + await GetVoxeetTokenApi({ partyName: LocalData().partyName }) .then((res) => res.json()) .then((tokenResult) => { if (tokenResult.status <= 299) { diff --git a/app/src/Chat.js b/app/src/Chat.js index 77c7e6a..616e966 100644 --- a/app/src/Chat.js +++ b/app/src/Chat.js @@ -235,6 +235,7 @@ export default class Chat extends Component { const data = { partyName: LocalData().partyName, }; + if (this.state.onCall) this.mCallManager.toggleCall(this.state.onCall, this.state.conference, this.state.admin, this.state.adminOnly); DeleteRoomApi(data) .then((res) => { this.props.notify('Ended party'); @@ -255,6 +256,7 @@ export default class Chat extends Component { partyName: LocalData().partyName, userUid: LocalData().userUid, }; + if (this.state.onCall) this.mCallManager.toggleCall(this.state.onCall, this.state.conference, this.state.admin, this.state.adminOnly); LeaveRoomApi(data) .then((res) => { this.props.notify('Left party'); @@ -312,7 +314,7 @@ export default class Chat extends Component { : giphyFetch.search(this.state.newMessage, { offset, limit: 1 }); } handlePhoneCall() { - const { onCall, conference, admin } = this.state; + const { onCall, conference, admin, adminOnly } = this.state; if (window.navigator && onCall === false) { try { const overlay = document.createElement('div'); @@ -328,7 +330,7 @@ export default class Chat extends Component { }) .then(() => { document.getElementById('mic-permission-overlay').remove(); - this.mCallManager.toggleCall(onCall, conference, admin); + this.mCallManager.toggleCall(onCall, conference, admin, adminOnly); }) .catch(() => { document.getElementById('mic-permission-overlay').remove(); @@ -339,7 +341,7 @@ export default class Chat extends Component { } catch (err) { console.error('Handle Phone Call Error', err); } - } else this.mCallManager.toggleCall(onCall, conference, admin); + } else this.mCallManager.toggleCall(onCall, conference, admin, adminOnly); } handleMute() { this.mCallManager.toggleMute(this.state.muted); diff --git a/functions/app.js b/functions/app.js index 399ea2f..c4a460f 100644 --- a/functions/app.js +++ b/functions/app.js @@ -1,13 +1,19 @@ const FunctionModules = require('./modules/index'); const FunctionModules2 = require('./modules2/index'); +const Webhooks = require('./webhooks/index'); const express = require('express'); const cors = require('cors'); +// WEBHOOKS +const webhooks = express(); +webhooks.use(cors({ origin: true })); +webhooks.all('/conferenceEnded',Webhooks.conferenceEnd); + // VERSION 1 const app = express(); app.use(cors({ origin: true })); app.use(FunctionModules.authenticator); -app.get('/validator', FunctionModules.tokenValidator); +app.get('/validator/:partyName', FunctionModules.tokenValidator); app.get('/refresher/:token', FunctionModules.tokenRefresher); app.post('/create', FunctionModules.createRoom); app.post('/join', FunctionModules.joinRoom); @@ -35,4 +41,5 @@ app2.delete('/delete/:rid', FunctionModules2.deleteRoom); module.exports = { v1: app, v2: app2, + webhooks, }; diff --git a/functions/index.js b/functions/index.js index e1aad21..605dd62 100644 --- a/functions/index.js +++ b/functions/index.js @@ -19,3 +19,5 @@ exports.widgets2 = functions.https.onRequest(REST.v2); exports.scheduledCleanUp = functions.pubsub .schedule('every 60 minutes') .onRun(FunctionModules.scheduledCleanUp); + +exports.webhooks = functions.https.onRequest(REST.webhooks); diff --git a/functions/modules/tokenValidator.js b/functions/modules/tokenValidator.js index dc28d91..de9bdc5 100644 --- a/functions/modules/tokenValidator.js +++ b/functions/modules/tokenValidator.js @@ -1,6 +1,9 @@ const VoxeetSecret = require('../VoxeetSecret.json'); +const admin = require('firebase-admin'); const fetch = require('node-fetch'); + module.exports = async (req, res) => { + const bodyData = JSON.parse(req.body); const { key, secret } = VoxeetSecret; const credentials = Buffer.from(encodeURI(key) + ':' + encodeURI(secret)).toString('base64'); const headers = { @@ -14,8 +17,48 @@ module.exports = async (req, res) => { body: JSON.stringify({ grant_type: 'client_credentials', }), - }) - .then((oauthResponse) => oauthResponse.json()) - .catch((err) => res.status(500).send(err)); - if (oauth) res.status(200).send(oauth); + }).then((oauthResponse) => oauthResponse.json()) + .catch((err) => res.status(500).send({err, message: 'Unable to generate token for call'})); + + const [exists, conferencePending, conference, adminOnly, adminUid] = await admin + .firestore() + .collection('rooms') + .doc(req.params.partyName) + .get() + .then(snap => { + const data = snap.exists ? snap.data() : {}; + return [snap.exists, data.conferencePending, data.conference, data.adminOnly, data.adminUid]; + }); + + if (exists === true) { + if (conference === undefined || conference === null) { + if (conferencePending === undefined || conferencePending === null) { + if (!adminOnly || (adminOnly && (adminUid === bodyData.uid)) ) { + await admin + .firestore() + .collection('rooms') + .doc(req.params.partyName) + .update({ + conferencePending: bodyData.uid + }); + await admin + .firestore() + .collection('rooms') + .doc(req.params.partyName) + .collection('messages') + .add({ + uid: '', + name: '', + text: 'A call session is being created (will be available soon).', + time: new Date(), + }); + } + } + } + if (oauth) res.status(200).send(oauth); + } else { + res.status(400).send({ message: 'Unable to get party for call.' }); + } + + return; }; diff --git a/functions/modules/updateVoxeet.js b/functions/modules/updateVoxeet.js index 73ae0c0..386f0d9 100644 --- a/functions/modules/updateVoxeet.js +++ b/functions/modules/updateVoxeet.js @@ -1,47 +1,68 @@ const admin = require('firebase-admin'); module.exports = async (req, res) => { - const { conference, partyName, partyPin, userName } = JSON.parse(req.body); + const { conference, partyName, partyPin, userName, uid } = JSON.parse(req.body); // GET ROOM PIN - const roomPin = await admin + const [exists, roomPin, conferencePending, partyConference] = await admin .firestore() .collection('rooms') .doc(partyName) .get() - .then((roomSnap) => (roomSnap.exists ? roomSnap.data().pin : null)); + .then((roomSnap) => { + const data = roomSnap.exists ? roomSnap.data() : {}; + return [roomSnap.exists, data.pin, data.conferencePending, data.conference]; + }); // CHECK IF ROOM EXISTS - if (roomPin) { - // CHECK FOR CORRECT PIN - if (roomPin === partyPin) { - // UPDATE CONFERENCE DETAILS - await admin - .firestore() - .collection('rooms') - .doc(partyName) - .update({ conference }) - .catch((err) => - res.status(500).send({ message: 'Unable to update the call details', error: err }) - ); - // ADD SYSTEM MESSAGE - await admin - .firestore() - .collection('rooms') - .doc(partyName) - .collection('messages') - .add({ - uid: '', - name: '', - text: `${userName} started the call.`, - time: new Date(), - }) - .catch((err) => console.error('Failed to add update conference message', err)); - res.status(200).send({ message: 'Started the conference' }); - } else { - // WRONG ROOM PIN - return res.status(401).send({ message: 'Wrong party pin, try rejoining' }); + if (exists === true) { + if (roomPin) { + // CHECK FOR CORRECT PIN + if (roomPin === partyPin) { + if (conferencePending) { + if (conferencePending === uid) { + // UPDATE CONFERENCE DETAILS + await admin + .firestore() + .collection('rooms') + .doc(partyName) + .update({ conference }) + .catch((err) => + res.status(500).send({ message: 'Unable to update the call details', error: err }) + ); + // REMOVE PENDING STATUS + await admin + .firestore() + .collection('rooms') + .doc(partyName) + .update({ conferencePending : admin.firestore.FieldValue.delete() }) + .catch((err) => console.error('Failed to remove pending status', err)); + // ADD SYSTEM MESSAGE + await admin + .firestore() + .collection('rooms') + .doc(partyName) + .collection('messages') + .add({ + uid: '', + name: '', + text: `${userName} started the call, everyone can join now.`, + time: new Date(), + }) + .catch((err) => console.error('Failed to add update conference message', err)); + res.status(200).send({ message: 'Started the conference' }); + } else { + res.status(409).send({ message: 'Looks like someone is already starting a call' }); + } + } else if (partyConference) { + res.status(409).send({ message: 'Call already exists', conference: partyConference }); + } + } else { + // WRONG ROOM PIN + res.status(401).send({ message: 'Wrong party pin, try rejoining' }); + } } } else { // ROOM DOESN'T EXIST - return res.status(404).send({ message: 'Party Not Found' }); + res.status(404).send({ message: 'Party Not Found' }); } + return; }; diff --git a/functions/webhooks/conferenceEnd.js b/functions/webhooks/conferenceEnd.js new file mode 100644 index 0000000..60febff --- /dev/null +++ b/functions/webhooks/conferenceEnd.js @@ -0,0 +1,30 @@ +const admin = require('firebase-admin'); + +module.exports = async (req, res) => { + console.info(`[START]\tWEBHOOKS/Conference End\nID:${req.body.conference.confId}\nParty:${req.body.conference.confAlias}`); + if (req.body.eventType === 'Conference.Ended') { + if (req.body.participants.length > 0) { + if (req.body.confAlias.startsWith('AUTODELETE_')) { + const partyName = req.body.confAlias.replace(/AUTODELETE_/, ""); + admin.firestore() + .collection('rooms') + .doc(partyName) + .update({ conference: admin.firestore.FieldValue.delete() }) + .then(() => { + console.info(`[DONE]\tWEBHOOKS/Conference End\nID:${req.body.conference.confId}\nParty:${req.body.conference.confAlias}`); + return; + }) + .catch(err => { + console.error(`[ERROR]\tWEBHOOKS/Conference End/Firebase Error\nID:${req.body.conference.confId}\nParty:${req.body.conference.confAlias}\nError:${JSON.stringify(err)}`) + }); + res.status(200).send({ message: 'Processed conference end'}); + } + } else { + console.info(`[DONE]\tWEBHOOKS/Conference End/End Of Duplicate Conference\nID:${req.body.conference.confId}\nParty:${req.body.conference.confAlias}`); + } + } else { + console.error(`[ERROR]\tWEBHOOKS/Conference End/Wrong Event Type\nID:${req.body.conference.confId}\nParty:${req.body.conference.confAlias}`) + res.status(400).send({ message: 'Wrong event type' }); + } + return; +}; diff --git a/functions/webhooks/index.js b/functions/webhooks/index.js new file mode 100644 index 0000000..5cf0ebb --- /dev/null +++ b/functions/webhooks/index.js @@ -0,0 +1,5 @@ +const conferenceEnd = require('./conferenceEnd'); + +module.exports = { + conferenceEnd, +};