Skip to content

Commit 4ea1b08

Browse files
authored
Merge pull request #184 from CS3219-AY2425S1/collab
Collab
2 parents 6ab7b07 + 6042804 commit 4ea1b08

14 files changed

+1930
-2
lines changed

.env.sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ EMAIL_PASSWORD=
2222
## Matching service variables
2323
MATCHING_SVC_PORT=6969
2424

25+
## Collab service variables
26+
COLLAB_SVC_PORT=3002
27+
COLLAB_SVC_DB_URI=
28+
2529
## Redis variables
2630
REDIS_PORT=6379
2731

collab-service/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

collab-service/.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"trailingComma": "es5",
3+
"tabWidth": 2,
4+
"semi": true,
5+
"singleQuote": false
6+
}

collab-service/Dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Base image
2+
FROM node:20-alpine AS base
3+
WORKDIR /app
4+
COPY package*.json ./
5+
6+
# Install dependencies in base stage
7+
RUN npm install
8+
9+
# Development stage
10+
FROM base AS dev
11+
COPY . .
12+
# Run the JavaScript code directly in dev mode (useful for hot-reloading)
13+
CMD ["npm", "run", "dev"]
14+
15+
# Production stage
16+
FROM node:20-alpine AS prod
17+
WORKDIR /app
18+
COPY --from=base /app/node_modules ./node_modules
19+
COPY . .
20+
# Start the server in production mode
21+
CMD ["npm", "start"]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {
2+
newRoom,
3+
getRoomId,
4+
heartbeat,
5+
getAllRooms,
6+
} from "../model/repository.js";
7+
import crypto from "crypto";
8+
9+
// Create a room between two users
10+
export async function createRoom(req, res) {
11+
const { user1, user2 } = req.body;
12+
13+
if (!user1 || !user2) {
14+
return res.status(400).json({ error: "Both user1 and user2 are required" });
15+
}
16+
17+
// Generate a unique room ID by hashing the two user IDs
18+
const roomId = crypto
19+
.createHash("sha256")
20+
.update(user1 + user2)
21+
.digest("hex");
22+
const room = await newRoom(user1, user2, roomId);
23+
24+
if (room) {
25+
res.status(201).json(room);
26+
} else {
27+
res.status(500).json({ error: "Failed to create room" });
28+
}
29+
}
30+
31+
// Get room ID by user
32+
export async function getRoomByUser(req, res) {
33+
const { user } = req.params;
34+
35+
if (!user) {
36+
return res.status(400).json({ error: "User is required" });
37+
}
38+
39+
const room = await getRoomId(user);
40+
41+
if (room) {
42+
res.status(200).json(room);
43+
} else {
44+
res.status(404).json({ error: `Room not found for user: ${user}` });
45+
}
46+
}
47+
48+
// Update heartbeat for a room
49+
export async function updateHeartbeat(req, res) {
50+
const { roomId } = req.params;
51+
52+
if (!roomId) {
53+
return res.status(400).json({ error: "Room ID is required" });
54+
}
55+
56+
const updatedRoom = await heartbeat(roomId);
57+
58+
if (updatedRoom) {
59+
res.status(200).json(updatedRoom);
60+
} else {
61+
res.status(404).json({ error: `Room with ID ${roomId} not found` });
62+
}
63+
}
64+
65+
// Get all rooms
66+
export async function getAllRoomsController(req, res) {
67+
const rooms = await getAllRooms();
68+
69+
if (rooms) {
70+
res.status(200).json(rooms);
71+
} else {
72+
res.status(500).json({ error: "Failed to retrieve rooms" });
73+
}
74+
}

collab-service/app/index.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import express from "express";
2+
import cors from "cors";
3+
import collabRoutes from "./routes/collab-routes.js";
4+
5+
const app = express();
6+
7+
app.use(express.urlencoded({ extended: true }));
8+
app.use(express.json());
9+
app.use(cors()); // config cors so that front-end can use
10+
app.options("*", cors());
11+
12+
// To handle CORS Errors
13+
app.use((req, res, next) => {
14+
res.header("Access-Control-Allow-Origin", "*"); // "*" -> Allow all links to access
15+
16+
res.header(
17+
"Access-Control-Allow-Headers",
18+
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
19+
);
20+
21+
// Browsers usually send this before PUT or POST Requests
22+
if (req.method === "OPTIONS") {
23+
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH");
24+
return res.status(200).json({});
25+
}
26+
27+
// Continue Route Processing
28+
next();
29+
});
30+
31+
app.use("/collab", collabRoutes);
32+
33+
app.get("/", (req, res, next) => {
34+
console.log("Sending Greetings!");
35+
res.json({
36+
message: "Hello World from collab-service",
37+
});
38+
});
39+
40+
// Handle When No Route Match Is Found
41+
app.use((req, res, next) => {
42+
const error = new Error("Route Not Found");
43+
error.status = 404;
44+
next(error);
45+
});
46+
47+
app.use((error, req, res, next) => {
48+
res.status(error.status || 500);
49+
res.json({
50+
error: {
51+
message: error.message,
52+
},
53+
});
54+
});
55+
56+
export default app;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { connect, mongoose } from "mongoose";
2+
import UsersSession from "./usersSession-model.js";
3+
4+
export async function connectToMongo() {
5+
await connect(process.env.DB_URI);
6+
}
7+
8+
export async function newRoom(user1, user2, roomId) {
9+
try {
10+
const newRoom = new UsersSession({
11+
users: [user1, user2],
12+
roomId: roomId,
13+
lastUpdated: new Date(),
14+
});
15+
16+
const savedRoom = await newRoom.save();
17+
return savedRoom;
18+
} catch (error) {
19+
console.error("Error creating room:", error);
20+
return null;
21+
}
22+
}
23+
24+
export async function getRoomId(user) {
25+
try {
26+
const room = await UsersSession.findOne({ users: user });
27+
return room;
28+
} catch (error) {
29+
console.error("Error finding room for ${user}:", error);
30+
return null;
31+
}
32+
}
33+
34+
export async function heartbeat(roomId) {
35+
try {
36+
const room = await UsersSession.findOne({ roomId: roomId });
37+
room.lastUpdated = new Date();
38+
await room.save();
39+
return room;
40+
} catch (error) {
41+
console.error("Error updating room ${roomId}:", error);
42+
return null;
43+
}
44+
}
45+
46+
export async function getAllRooms() {
47+
try {
48+
const rooms = await UsersSession.find({});
49+
return rooms;
50+
} catch (error) {
51+
console.error("Error getting all rooms:", error);
52+
return null;
53+
}
54+
}
55+
56+
// Function to add a new message to chatHistory with transaction support
57+
export async function addMessageToChat(roomId, userId, text) {
58+
// Start a session for the transaction
59+
const session = await mongoose.startSession();
60+
61+
try {
62+
session.startTransaction();
63+
64+
// Find the session document by roomId within the transaction
65+
const sessionDoc = await UsersSession.findOne({ roomId }).session(session);
66+
67+
if (!sessionDoc) {
68+
throw new Error("Room not found");
69+
}
70+
71+
// Determine the next message index within the transaction
72+
const lastMessageIndex =
73+
sessionDoc.chatHistory.length > 0
74+
? sessionDoc.chatHistory[sessionDoc.chatHistory.length - 1].messageIndex
75+
: -1;
76+
77+
// Create the new message with incremented messageIndex
78+
const newMessage = {
79+
messageIndex: lastMessageIndex + 1,
80+
userId,
81+
text,
82+
timestamp: new Date(),
83+
};
84+
85+
// Add the new message to chatHistory within the transaction
86+
sessionDoc.chatHistory.push(newMessage);
87+
88+
// Save the document within the transaction
89+
await sessionDoc.save({ session });
90+
91+
// Commit the transaction
92+
await session.commitTransaction();
93+
94+
// End the session and return the new message
95+
session.endSession();
96+
return newMessage;
97+
} catch (error) {
98+
// If an error occurs, abort the transaction
99+
await session.abortTransaction();
100+
session.endSession();
101+
console.error("Error adding message to chat:", error);
102+
throw error;
103+
}
104+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import mongoose from "mongoose";
2+
3+
const Schema = mongoose.Schema;
4+
5+
const messageSchema = new Schema({
6+
messageIndex: {
7+
type: Number,
8+
required: true,
9+
},
10+
userId: {
11+
type: String,
12+
required: true,
13+
},
14+
text: {
15+
type: String,
16+
required: true,
17+
},
18+
// timestamp: {
19+
// type: Date,
20+
// default: Date.now,
21+
// required: true
22+
// }
23+
});
24+
25+
const usersSessionSchema = new Schema({
26+
users: {
27+
type: [String],
28+
required: true,
29+
},
30+
roomId: {
31+
type: String,
32+
required: true,
33+
},
34+
lastUpdated: {
35+
type: Date,
36+
required: true,
37+
default: Date.now,
38+
},
39+
chatHistory: {
40+
type: [messageSchema],
41+
default: [],
42+
},
43+
});
44+
45+
export default mongoose.model("UsersSession", usersSessionSchema);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import express from "express";
2+
import {
3+
createRoom,
4+
getRoomByUser,
5+
updateHeartbeat,
6+
getAllRoomsController,
7+
} from "../controller/collab-controller.js";
8+
9+
const router = express.Router();
10+
11+
router.post("/create-room", createRoom);
12+
13+
router.get("/user/:user", getRoomByUser);
14+
15+
router.patch("/heartbeat/:roomId", updateHeartbeat);
16+
17+
router.get("/rooms", getAllRoomsController);
18+
19+
export default router;

collab-service/app/server.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { connectToMongo } from "./model/repository.js";
2+
import http from "http";
3+
import index from "./index.js";
4+
import { Server } from "socket.io";
5+
import { addMessageToChat } from "./model/repository.js";
6+
7+
const PORT = process.env.PORT || 3002;
8+
const server = http.createServer(index);
9+
10+
const io = new Server(server, {
11+
cors: {
12+
origin: "*", // Allow all origins; replace with specific origin if needed
13+
methods: ["GET", "POST"],
14+
allowedHeaders: ["Content-Type", "Authorization"],
15+
credentials: true,
16+
},
17+
});
18+
19+
io.on("connection", (socket) => {
20+
console.log("User connected to Socket.IO");
21+
22+
// Join a room based on roomId
23+
socket.on("joinRoom", (roomId) => {
24+
socket.join(roomId);
25+
console.log(`User joined room: ${roomId}`);
26+
});
27+
28+
// Handle incoming chat messages
29+
socket.on("sendMessage", async (data) => {
30+
const { roomId, userId, text } = data;
31+
const newMessage = await addMessageToChat(roomId, userId, text);
32+
33+
// Broadcast the message to all clients in the same room
34+
io.to(roomId).emit("chatMessage", newMessage);
35+
});
36+
37+
socket.on("disconnect", () => {
38+
console.log("User disconnected from Socket.IO");
39+
});
40+
});
41+
42+
connectToMongo()
43+
.then(() => {
44+
server.listen(PORT, () => {
45+
console.log(`Server running on http://localhost:${PORT}`);
46+
});
47+
})
48+
.catch((error) => {
49+
console.error("Error connecting to MongoDB:", error);
50+
});

0 commit comments

Comments
 (0)