From 0d8673d594d84c185b117a72724be4c7b193eb8d Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 6 May 2022 06:30:14 -0500 Subject: [PATCH] nicer flow for disconnections, adding socket initialization and such --- packages/api/index.ts | 126 +------------ packages/api/sockets/index.ts | 170 ++++++++++++++++++ .../src/components/ProtectedRoute/index.tsx | 24 ++- packages/desktop/src/controller/index.tsx | 16 +- .../src/controller/lineDataProvider.tsx | 39 +++- packages/desktop/src/controller/nirvanaApi.ts | 5 + packages/desktop/src/pages/nirvanaApp.tsx | 2 +- packages/desktop/src/pages/router/index.tsx | 22 ++- packages/desktop/src/pages/terminal/index.tsx | 2 +- 9 files changed, 269 insertions(+), 137 deletions(-) create mode 100644 packages/api/sockets/index.ts diff --git a/packages/api/index.ts b/packages/api/index.ts index 08286c9..ede3878 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -1,7 +1,9 @@ import express, { Application, Request, Response } from "express"; import GetAllSocketClients from "@nirvana/core/sockets/getAllActiveSocketClients"; +import InitializeWs from "./sockets"; import { NextFunction } from "express"; +import NirvanaResponse from "@nirvana/core/responses/nirvanaResponse"; import ReceiveSignal from "../core/sockets/receiveSignal"; import SendSignal from "@nirvana/core/sockets/sendSignal"; import SocketChannels from "@nirvana/core/sockets/channels"; @@ -26,14 +28,16 @@ app.get("/", (req: Request, res: Response) => { res.send("hello world."); }); +app.use("/api", (req: Request, res: Response) => { + res.json(new NirvanaResponse("wohoo, server is healthy")); +}); + app.use("/api/user", getUserRoutes()); app.use("/api/search", getSearchRoutes()); app.use("/api/lines", getLineRoutes()); const PORT = 5000; -var server = app.listen(PORT, () => console.log("express running")); - -// socket IO stuff +const server = app.listen(PORT, () => console.log("express running")); const io = require("socket.io")(server, { // todo: add authentication @@ -42,118 +46,4 @@ const io = require("socket.io")(server, { }, }); -io.on("connection", function (socket: any) { - // add to map between between googleUserIds and socketId - - console.log("a user connected"); - - // ===== JOIN ==== - /** User wants to subscribe to live emissions of a conversation */ - socket.on(SocketChannels.JOIN_ROOM, (relationshipId: string) => { - // add this user to the room - - console.log( - `${socket.id} user joined room for relationship ${relationshipId}` - ); - - socket.join(relationshipId); - - console.log(`${socket.id} now in rooms ${socket.rooms}`); - }); - - // ==== UPDATES ==== - // can be a change of: - // 1. status...later...do short polling for this instead - // 2. new content: audio clip or link - // 3. someone is starting to speak - // send message to everyone in room except sender... - - /** User wants to send some update to a particular room */ - socket.on( - SocketChannels.SEND_AUDIO_CLIP, - (relationshipId: string, audioChunks: any) => { - console.log(`new audio chunks received..routing to appropriate room!`); - console.log(relationshipId); - console.log(audioChunks); - - io.in(relationshipId).emit( - SocketChannels.SEND_AUDIO_CLIP, - relationshipId, - audioChunks - ); - } - ); - - // tell everyone in a room when someone is starting to speak - socket.on(SocketChannels.SEND_STARTED_SPEAKING, (relationshipId: string) => { - console.log(`started speaking in ${relationshipId}`); - io.in(relationshipId).emit( - SocketChannels.SEND_STARTED_SPEAKING, - relationshipId - ); - }); - - // tell everyone in a room when someone is stopping to speak - socket.on(SocketChannels.SEND_STOPPED_SPEAKING, (relationshipId: string) => { - console.log(`stopped speaking in ${relationshipId}`); - io.in(relationshipId).emit( - SocketChannels.SEND_STOPPED_SPEAKING, - relationshipId - ); - }); - - // change of user status to all of users' rooms and db update - socket.on( - SocketChannels.SEND_USER_STATUS_UPDATE, - async (userGoogleId: string, newStatus: UserStatus) => { - console.log("new status for user"); - - const resultUpdate = await UserService.updateUserStatus( - userGoogleId, - newStatus - ); - - // tell all rooms that the user is part of - // that this user has updated their status - if (resultUpdate?.modifiedCount) { - socket.rooms.forEach((roomId: string) => { - io.in(roomId).emit( - SocketChannels.SEND_USER_STATUS_UPDATE, - userGoogleId, - newStatus - ); - }); - } - } - ); - - socket.on(SocketChannels.JOIN_LIVE_ROOM, async () => { - // TODO: only get the socket ids of the relevant rooms for this user - // return all Socket instances - const allConnectedSockets = Array.from(await io.of("/").sockets.keys()); - - io.to(socket.id).emit(SocketChannels.GET_ALL_ACTIVE_SOCKET_IDS, { - socketIds: allConnectedSockets, - } as GetAllSocketClients); - }); - - socket.on(SocketChannels.SEND_SIGNAL, async (payload: SendSignal) => { - console.log(payload); - - const sendingBackData: ReceiveSignal = { - simplePeerSignal: payload.simplePeerSignal, - senderUserSocketId: socket.id, - isGoingBackToInitiator: payload.isAnswerer ? true : false, - }; - - io.to(payload.userSocketIdToSignal).emit( - SocketChannels.RECEIVE_SIGNAL, - sendingBackData - ); - }); - - // ==== DISCONNECT ==== - socket.on("disconnect", () => { - console.log("user disconnected"); - }); -}); +InitializeWs(io); diff --git a/packages/api/sockets/index.ts b/packages/api/sockets/index.ts new file mode 100644 index 0000000..3485a04 --- /dev/null +++ b/packages/api/sockets/index.ts @@ -0,0 +1,170 @@ +import GetAllSocketClients from "@nirvana/core/sockets/getAllActiveSocketClients"; +import { JwtClaims } from "../middleware/auth"; +import ReceiveSignal from "@nirvana/core/sockets/receiveSignal"; +import SendSignal from "@nirvana/core/sockets/sendSignal"; +import SocketChannels from "@nirvana/core/sockets/channels"; +import { UserService } from "../services/user.service"; +import { UserStatus } from "@nirvana/core/models/user.model"; +import { loadConfig } from "../config"; + +const jwt = require("jsonwebtoken"); + +const config = loadConfig(); + +export default function InitializeWs(io: any) { + console.log("initializing web sockets"); + + return io + .use(function (socket: any, next: any) { + console.log("authenticating user..."); + + try { + const { token } = socket.handshake.query; + + // verify jwt token with our api secret + var decoded: JwtClaims = jwt.verify(token, config.JWT_TOKEN_SECRET); + + socket.userInfo = decoded; + + next(); + } catch (error) { + console.error(error); + next(new Error("WS Authentication Error")); + } + }) + .on("connection", function (socket: any) { + const userInfo: JwtClaims = socket.userInfo; + + console.log( + `a user connected | user Id: ${userInfo.userId} and name: ${userInfo.name}` + ); + + socket.on("test", () => { + console.log("asdf"); + }); + + // ?verification that user is in a particular line to be tuned into it or just generally in it? + + // regular socket rooms for information for all clients in a specific line + + // another namespace or so for clients tuned into certain lines + + // ===== JOIN ==== + /** User wants to subscribe to live emissions of a conversation */ + socket.on(SocketChannels.JOIN_ROOM, (relationshipId: string) => { + // add this user to the room + + console.log( + `${socket.id} user joined room for relationship ${relationshipId}` + ); + + socket.join(relationshipId); + + console.log(`${socket.id} now in rooms ${socket.rooms}`); + }); + + // ==== UPDATES ==== + // can be a change of: + // 1. status...later...do short polling for this instead + // 2. new content: audio clip or link + // 3. someone is starting to speak + // send message to everyone in room except sender... + + /** User wants to send some update to a particular room */ + socket.on( + SocketChannels.SEND_AUDIO_CLIP, + (relationshipId: string, audioChunks: any) => { + console.log( + `new audio chunks received..routing to appropriate room!` + ); + console.log(relationshipId); + console.log(audioChunks); + + io.in(relationshipId).emit( + SocketChannels.SEND_AUDIO_CLIP, + relationshipId, + audioChunks + ); + } + ); + + // tell everyone in a room when someone is starting to speak + socket.on( + SocketChannels.SEND_STARTED_SPEAKING, + (relationshipId: string) => { + console.log(`started speaking in ${relationshipId}`); + io.in(relationshipId).emit( + SocketChannels.SEND_STARTED_SPEAKING, + relationshipId + ); + } + ); + + // tell everyone in a room when someone is stopping to speak + socket.on( + SocketChannels.SEND_STOPPED_SPEAKING, + (relationshipId: string) => { + console.log(`stopped speaking in ${relationshipId}`); + io.in(relationshipId).emit( + SocketChannels.SEND_STOPPED_SPEAKING, + relationshipId + ); + } + ); + + // change of user status to all of users' rooms and db update + socket.on( + SocketChannels.SEND_USER_STATUS_UPDATE, + async (userGoogleId: string, newStatus: UserStatus) => { + console.log("new status for user"); + + const resultUpdate = await UserService.updateUserStatus( + userGoogleId, + newStatus + ); + + // tell all rooms that the user is part of + // that this user has updated their status + if (resultUpdate?.modifiedCount) { + socket.rooms.forEach((roomId: string) => { + io.in(roomId).emit( + SocketChannels.SEND_USER_STATUS_UPDATE, + userGoogleId, + newStatus + ); + }); + } + } + ); + + socket.on(SocketChannels.JOIN_LIVE_ROOM, async () => { + // TODO: only get the socket ids of the relevant rooms for this user + // return all Socket instances + const allConnectedSockets = Array.from(await io.of("/").sockets.keys()); + + io.to(socket.id).emit(SocketChannels.GET_ALL_ACTIVE_SOCKET_IDS, { + socketIds: allConnectedSockets, + } as GetAllSocketClients); + }); + + socket.on(SocketChannels.SEND_SIGNAL, async (payload: SendSignal) => { + console.log(payload); + + const sendingBackData: ReceiveSignal = { + simplePeerSignal: payload.simplePeerSignal, + senderUserSocketId: socket.id, + isGoingBackToInitiator: payload.isAnswerer ? true : false, + }; + + io.to(payload.userSocketIdToSignal).emit( + SocketChannels.RECEIVE_SIGNAL, + sendingBackData + ); + }); + + // ==== DISCONNECT ==== + socket.on("disconnect", () => { + console.log("user disconnected"); + }); + }); +} diff --git a/packages/desktop/src/components/ProtectedRoute/index.tsx b/packages/desktop/src/components/ProtectedRoute/index.tsx index f7019f2..d3b0633 100644 --- a/packages/desktop/src/components/ProtectedRoute/index.tsx +++ b/packages/desktop/src/components/ProtectedRoute/index.tsx @@ -1,9 +1,10 @@ +import { useAuthCheck, useServerCheck } from "../../controller/index"; + import { $jwtToken } from "../../controller/recoil"; import Login from "../../pages/Login"; import NirvanaApi from "../../controller/nirvanaApi"; import { STORE_ITEMS } from "../../electron/constants"; import SkeletonLoader from "../loading/skeleton"; -import { useAuthCheck } from "../../controller/index"; import { useEffect } from "react"; import { useRecoilState } from "recoil"; @@ -14,7 +15,17 @@ export default function ProtectedRoute({ }) { const [jwtToken, setJwtToken] = useRecoilState($jwtToken); - const { isLoading, isError, isFetching, isSuccess, refetch } = useAuthCheck(); + const { + isError: serverFailure, + isLoading: serverLoading, + isSuccess: isServerHealthy, + error, + } = useServerCheck(); + + console.log(error); + + const { isLoading, isError, isFetching, isSuccess, refetch } = + useAuthCheck(isServerHealthy); useEffect(() => { // on load of this, if we already have jwt tokens in store, @@ -49,6 +60,15 @@ export default function ProtectedRoute({ refetch(); }, [jwtToken]); + if (serverLoading) + return ( + + Sorry...this is our bad. Our servers are loading. We are trying our best + to back up and running! :)
Please contact me for urgent concerns: + arjunpatel@berkeley.edu +
+ ); + if (isLoading) { return (
diff --git a/packages/desktop/src/controller/index.tsx b/packages/desktop/src/controller/index.tsx index 6afe2aa..5892e06 100644 --- a/packages/desktop/src/controller/index.tsx +++ b/packages/desktop/src/controller/index.tsx @@ -6,7 +6,20 @@ import { queryClient } from "../pages/nirvanaApp"; import { useRecoilValue } from "recoil"; // ====== QUERIES -export function useAuthCheck() { + +/** + * ensure that the server is up + */ +export function useServerCheck() { + return useQuery("SERVER_CHECK", ApiCalls.serverCheck, { + retry: true, + refetchOnWindowFocus: false, + + refetchInterval: 10000, + }); +} + +export function useAuthCheck(enabled: boolean = true) { const jwtToken = useRecoilValue($jwtToken); return useQuery("AUTH_CHECK", ApiCalls.authCheck, { @@ -14,6 +27,7 @@ export function useAuthCheck() { refetchOnWindowFocus: false, refetchInterval: 10000, + enabled, }); } diff --git a/packages/desktop/src/controller/lineDataProvider.tsx b/packages/desktop/src/controller/lineDataProvider.tsx index 3f63ad8..4f26110 100644 --- a/packages/desktop/src/controller/lineDataProvider.tsx +++ b/packages/desktop/src/controller/lineDataProvider.tsx @@ -1,11 +1,13 @@ import React, { useContext } from "react"; -import $ws from "./sockets"; +import { $jwtToken } from "./recoil"; import MasterLineData from "@nirvana/core/models/masterLineData.model"; import SocketChannels from "@nirvana/core/sockets/channels"; import { User } from "@nirvana/core/models"; import { io } from "socket.io-client"; +import toast from "react-hot-toast"; import { useEffect } from "react"; +import { useRecoilValue } from "recoil"; import { useUserLines } from "./index"; interface ILineDataContext { @@ -14,23 +16,50 @@ interface ILineDataContext { relevantUsers: User[]; } -const lineDataContext = React.createContext({ +const LineDataContext = React.createContext({ lines: [], relevantUsers: [], }); -export function LineDataProvider() { +let $ws; + +export function LineDataProvider({ children }) { // persistent store of lines const { data: basicUserLinesData } = useUserLines(); + const jwtToken = useRecoilValue($jwtToken); + + useEffect(() => { + $ws = io("http://localhost:5000", { + query: { token: jwtToken }, + }); + + // client-side + $ws.on("connect_error", (err) => { + console.error(err.message); // prints the message associated with the error + toast.error("sorry...this is our bad...please refresh with cmd + r"); + }); + }, []); // connect through ws to enrich basic lines data useEffect(() => { - // $ws.on(SocketChannels.CONNECT, ); + console.log("got basic user lines data!!!"); + + // ?need to calculate diff and only do stuff then? + + // $ws.on(SocketChannels.CONNECT, console.log()); + + $ws.on("test", () => console.log("test")); }, [basicUserLinesData]); // get updated associations with certain lines + + return ( + + {children} + + ); } export function useLineDataProvider() { - return useContext(lineDataContext); + return useContext(LineDataContext); } diff --git a/packages/desktop/src/controller/nirvanaApi.ts b/packages/desktop/src/controller/nirvanaApi.ts index b871844..55f7407 100644 --- a/packages/desktop/src/controller/nirvanaApi.ts +++ b/packages/desktop/src/controller/nirvanaApi.ts @@ -56,6 +56,10 @@ export default class NirvanaApi { } } +async function serverCheck(): Promise { + return await NirvanaApi.fetch(`/`, "GET", false); +} + async function login(reqLoginTokens: { accessToken: string; idToken: string; @@ -98,6 +102,7 @@ async function createLine( } export const ApiCalls = { + serverCheck, login, authCheck, getUserDetails, diff --git a/packages/desktop/src/pages/nirvanaApp.tsx b/packages/desktop/src/pages/nirvanaApp.tsx index 89c7559..e2bdc10 100644 --- a/packages/desktop/src/pages/nirvanaApp.tsx +++ b/packages/desktop/src/pages/nirvanaApp.tsx @@ -1,12 +1,12 @@ import { QueryClient, QueryClientProvider } from "react-query"; -import Login from "./Login"; import NirvanaRouter from "./router"; import ProtectedRoute from "../components/ProtectedRoute"; import { ReactQueryDevtools } from "react-query/devtools"; import { RecoilRoot } from "recoil"; import { Toaster } from "react-hot-toast"; import { configure } from "react-hotkeys"; +import { io } from "socket.io-client"; import testConnection from "@nirvana/core"; testConnection(); diff --git a/packages/desktop/src/pages/router/index.tsx b/packages/desktop/src/pages/router/index.tsx index d52b6b5..5b4984a 100644 --- a/packages/desktop/src/pages/router/index.tsx +++ b/packages/desktop/src/pages/router/index.tsx @@ -16,6 +16,7 @@ import React, { useState } from "react"; import { useCallback, useEffect, useMemo } from "react"; import { useRecoilState, useRecoilValue } from "recoil"; +import { LineDataProvider } from "../../controller/lineDataProvider"; import LineDetailsTerminal from "../lineDetailsTerminal"; import NirvanaHeader from "../../components/header/index"; import NirvanaTerminal from "../terminal"; @@ -311,18 +312,21 @@ export default function NirvanaRouter() { }, [setDesktopMode]); return ( -
- setDesktopMode("terminal")} /> + +
+ setDesktopMode("terminal")} /> -
- {(desktopMode === "terminal" || desktopMode === "terminalDetails") && ( - - )} +
+ {(desktopMode === "terminal" || + desktopMode === "terminalDetails") && ( + + )} - {desktopMode === "terminalDetails" && } + {desktopMode === "terminalDetails" && } - {desktopMode === "overlayOnly" && } + {desktopMode === "overlayOnly" && } +
-
+
); } diff --git a/packages/desktop/src/pages/terminal/index.tsx b/packages/desktop/src/pages/terminal/index.tsx index a801ff6..db69b8c 100644 --- a/packages/desktop/src/pages/terminal/index.tsx +++ b/packages/desktop/src/pages/terminal/index.tsx @@ -77,7 +77,7 @@ export default function NirvanaTerminal({ ) : ( <> - {!userLinesRes?.data?.masterLines.length && ( + {!userLinesRes?.data?.masterLines?.length && ( You have no lines!
Create one to connect to your team instantly.