diff --git a/backend/package-lock.json b/backend/package-lock.json index 43a92b8..545893b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@clerk/express": "^1.7.41", + "@stream-io/node-sdk": "^0.7.12", "codemeet": "file:..", "cors": "^2.8.5", "dotenv": "^17.2.3", @@ -1630,6 +1631,44 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@stream-io/node-sdk": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@stream-io/node-sdk/-/node-sdk-0.7.12.tgz", + "integrity": "sha512-y5amw3sHXCJ/i2XIu6g0dmcIQudHR5jbJKWK1B8uxvlRr8cMw9+srKZ8hxIdFeCUDGNX7CcuIepF8O6X5aTsng==", + "license": "See license in LICENSE", + "dependencies": { + "@types/jsonwebtoken": "^9.0.3", + "@types/node": "^18.3.0", + "jsonwebtoken": "^9.0.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@stream-io/openai-realtime-api": "~0.1.3 || ~0.2.0 || ~0.3.0" + }, + "peerDependenciesMeta": { + "@stream-io/openai-realtime-api": { + "optional": true + } + } + }, + "node_modules/@stream-io/node-sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@stream-io/node-sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/@types/aws-lambda": { "version": "8.10.147", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", diff --git a/backend/package.json b/backend/package.json index 1271a7f..db6df49 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,7 @@ "license": "ISC", "dependencies": { "@clerk/express": "^1.7.41", + "@stream-io/node-sdk": "^0.7.12", "codemeet": "file:..", "cors": "^2.8.5", "dotenv": "^17.2.3", diff --git a/backend/src/controllers/sessionController.js b/backend/src/controllers/sessionController.js new file mode 100644 index 0000000..c16f867 --- /dev/null +++ b/backend/src/controllers/sessionController.js @@ -0,0 +1,155 @@ +import Session from "../models/Session.js"; +import { chatClient, streamClient } from "../lib/stream.js"; + +export async function createSession(req, res) { + try { + const { problem, difficulty } = req.body; + const userId = req.user._id; + const clerkId = req.user.clerkId; + if (!problem || !difficulty) { + return res + .status(400) + .json({ msg: "Problem and difficulty are required" }); + } + + //generate a unique call id for stream video + const callId = `session-${Date.now()}_${Math.random().toString(36).substring(7)}`; + + //create session in db + const session = await Session.create({ + problem, + difficulty, + host: userId, + callId, + }); + + //create stream video call + await streamClient.video.call("default", callId).getOrCreate({ + data: { + created_by_id: clerkId, + custom: { problem, difficulty, sessionId: session._id.toString() }, + }, + }); + + //chat messaging + const channel = chatClient.channel("messaging", callId, { + name: `${problem} Session`, + created_by_id: clerkId, + members: [clerkId], + }); + + await channel.create(); + + res.status(201).json({ session }); + } catch (error) { + console.error("Error createSession controller:", error.message); + res.status(500).json({ msg: "Internal server error" }); + } +} + +export async function getActiveSessions(_, res) { + try { + const sessions = await Session.find({ status: "active" }) + .populate("host", "name profileImage email clerkId") + .sort({ createdAt: -1 }) + .limit(20); + res.status(200).json({ sessions }); + } catch (error) { + console.error("Error getActiveSessions controller:", error.message); + res.status(500).json({ msg: "Internal server error" }); + } +} + +export async function getMyRecentSessions(req, res) { + try { + const userId = req.user._id; + //where user is eigther host or pwrticipant + const sessions = await Session.find({ + status: "completed", + $or: [{ host: userId }, { participant: userId }], + }) + .sort({ createdAt: -1 }) + .limit(20); + + res.status(200).json({ sessions }); + } catch (error) { + console.error("Error getMyRecentSessions controller:", error.message); + res.status(500).json({ msg: "Internal server error" }); + } +} + +export async function getSessionById(req, res) { + try { + const { id } = req.params; + const session = await Session.findById(id) + .populate("host", "name email profileImage clerkId") + .populate("participant", "name email profileImage clerkId"); + + if (!session) return res.status(404).json({ msg: "Session not found" }); + + res.status(200).json({ session }); + } catch (error) { + console.error("Error getSessionById controller:", error.message); + res.status(500).json({ msg: "Internal server error" }); + } +} + +export async function joinSession(req, res) { + try { + const { id } = req.params; + const userId = req.user._id; + const clerkId = req.user.clerkId; + + const session = await Session.findById(id); + if (!session) return res.status(404).json({ msg: "Session not found" }); + //check if session is already full + if (session.participant) + return res.status(400).json({ msg: "Session is already full" }); + session.participant = userId; + await session.save(); + + const channel = chatClient.channel("messaging", session.callId); + await channel.addMembers([clerkId]); + res.status(200).json({ session }); + } catch (error) { + console.error("Error joinSession controller:", error.message); + res.status(500).json({ msg: "Internal server error" }); + } +} + +export async function endSession(req, res) { + try { + const { id } = req.params; + const userId = req.user._id; + + const session = await Session.findById(id); + if (!session) return res.status(404).json({ msg: "Session not found" }); + + //check if you are the host or not + if (session.host.toString() !== userId.toString()) { + return res.status(403).json({ msg: "Only The host can end the session" }); + } + + //check if session is already completed + if (session.status === "completed") { + return res.status(400).json({ msg: "Session is already completed" }); + } + + session.status = "completed"; + await session.save(); + + //delete stream video call + const call = streamClient.video.call("default", session.callId); + await call.delete({ hard: true }); + + //delete stream chat channel + const channel = chatClient.channel("messaging", session.callId); + await channel.delete(); + + res.status(200).json({ msg: "Session ended successfully" }); + } catch (error) { + console.error("Error endSession controller:", error.message); + res.status(500).json({ msg: "Internal server error" }); + } +} +//idkd diff --git a/backend/src/lib/inngest.js b/backend/src/lib/inngest.js index fddd2a2..c5450a3 100644 --- a/backend/src/lib/inngest.js +++ b/backend/src/lib/inngest.js @@ -27,6 +27,8 @@ const syncUser = inngest.createFunction( name: newUser.name, image: newUser.profileImage, }); + + //Send a Welcome email ro the user in future }, ); diff --git a/backend/src/lib/stream.js b/backend/src/lib/stream.js index 44ea905..51e2aa6 100644 --- a/backend/src/lib/stream.js +++ b/backend/src/lib/stream.js @@ -1,4 +1,5 @@ import { StreamChat } from "stream-chat"; +import { StreamClient } from "@stream-io/node-sdk"; import { ENV } from "./env.js"; const apiKey = ENV.STREAM_API_KEY; @@ -8,7 +9,8 @@ if (!apiKey || !apiSecret) { console.log("STREAM_API_KEY or STREAM_API_SECRET is missing"); } -export const chatClient = StreamChat.getInstance(apiKey, apiSecret); +export const chatClient = StreamChat.getInstance(apiKey, apiSecret); //this is for chat features +export const streamClient = new StreamClient(apiKey, apiSecret); //used for video calls export const upsertStreamUser = async (userData) => { try { diff --git a/backend/src/models/Session.js b/backend/src/models/Session.js new file mode 100644 index 0000000..a6d5e32 --- /dev/null +++ b/backend/src/models/Session.js @@ -0,0 +1,39 @@ +import mongoose from "mongoose"; + +const sessionSchema = new mongoose.Schema( + { + problem: { + type: String, + required: true, + }, + difficulty: { + type: String, + enum: ["easy", "medium", "hard"], + required: true, + }, + host: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + participant: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + default: null, + }, + status: { + type: String, + enum: ["active", "completed"], + default: "active", + }, + callId: { + type: String, + default: "", + }, + }, + { timestamps: true }, +); + +const Session = mongoose.model("Session", sessionSchema); + +export default Session; diff --git a/backend/src/routes/sessionRoute.js b/backend/src/routes/sessionRoute.js new file mode 100644 index 0000000..6d7112c --- /dev/null +++ b/backend/src/routes/sessionRoute.js @@ -0,0 +1,21 @@ +import express from "express"; +import { protectRoute } from "../middleware/protectRoute.js"; +import { + createSession, + getActiveSessions, + getMyRecentSessions, + getSessionById, + joinSession, + endSession, +} from "../controllers/sessionController.js"; + +const router = express.Router(); + +router.post("/", protectRoute, createSession); +router.get("/active", protectRoute, getActiveSessions); +router.get("/my-recent", protectRoute, getMyRecentSessions); +router.get("/:id", protectRoute, getSessionById); +router.post("/:id/join", protectRoute, joinSession); +router.post("/:id/end", protectRoute, endSession); + +export default router; diff --git a/backend/src/server.js b/backend/src/server.js index 50b5dff..47ba05e 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -8,6 +8,7 @@ import { ENV } from "./lib/env.js"; import { connectDB } from "./lib/db.js"; import { inngest, functions } from "./lib/inngest.js"; import chatRoutes from "./routes/chatRoutes.js"; +import sessionRoutes from "./routes/sessionRoute.js"; const app = express(); @@ -23,6 +24,7 @@ app.use("/api/inngest", serve({ client: inngest, functions })); app.use(clerkMiddleware()); //this will add auth object to the request if the user is authenticated, which we can use in our routes app.use("/api/chat", chatRoutes); +app.use("/api/sessions", sessionRoutes); app.get("/health", (req, res) => { res.status(200).json({ msg: "success api is running" });