diff --git a/server/src/models/Chat.js b/server/src/models/Chat.js index 3cba143..363a6fe 100644 --- a/server/src/models/Chat.js +++ b/server/src/models/Chat.js @@ -1,21 +1,10 @@ import mongoose from "mongoose"; - const chatSchema = new mongoose.Schema({ server_id: String, channels: [ { channel_id: String, channel_name: String, - chat_details: [ - { - content: String, - sender_id: String, - sender_name: String, - sender_pic: String, - sender_tag: String, - timestamp: String, - }, - ], }, ], }); diff --git a/server/src/models/DirectMessageThread.js b/server/src/models/DirectMessageThread.js index cb6d367..1dad487 100644 --- a/server/src/models/DirectMessageThread.js +++ b/server/src/models/DirectMessageThread.js @@ -2,16 +2,9 @@ import mongoose from "mongoose"; const directMessageThreadSchema = new mongoose.Schema({ participants: [String], - messages: [ - { - sender_id: String, - sender_name: String, - sender_tag: String, - sender_pic: String, - content: String, - timestamp: Number, - }, - ], }); -export default mongoose.model("direct_message_threads", directMessageThreadSchema); +export default mongoose.model( + "direct_message_threads", + directMessageThreadSchema, +); diff --git a/server/src/models/Message.js b/server/src/models/Message.js new file mode 100644 index 0000000..fbf38a6 --- /dev/null +++ b/server/src/models/Message.js @@ -0,0 +1,40 @@ +import mongoose from "mongoose"; + +const messageSchema = new mongoose.Schema( + { + sender: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + channelId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Chat", + }, + threadId: { + type: mongoose.Schema.Types.ObjectId, + ref: "DirectMessageThread", + }, + content: { + type: String, + required: true, + }, + sender_name: String, + sender_pic: String, + sender_tag: String, + timestamp: { + type: Date, + default: Date.now, + }, + isEdited: { + type: Boolean, + default: false, + }, + }, + { timestamps: true }, +); + +messageSchema.index({ channelId: 1, timestamp: -1 }); +messageSchema.index({ threadId: 1, timestamp: -1 }); + +export default mongoose.model("Message", messageSchema); diff --git a/server/src/routes/chat.js b/server/src/routes/chat.js index 29ee586..bf4e613 100644 --- a/server/src/routes/chat.js +++ b/server/src/routes/chat.js @@ -5,10 +5,10 @@ import jwt from "jsonwebtoken"; import Chat from "../models/Chat.js"; import Server from "../models/Server.js"; +import Message from "../models/Message.js"; import User from "../models/User.js"; import * as cache from "../lib/cache.js"; import logger from "../lib/winston.js"; -import { getChats } from "../services/chatService.js"; import { incrementServerUnread } from "../services/unreadService.js"; import { getIO } from "../socket/runtime.js"; @@ -58,27 +58,23 @@ router.post("/store_message", expressRateLimit("chat"), async (req, res) => { timestamp, }; - const response = await Chat.find({ - server_id, - "channels.channel_id": channel_id, - }); - async function notifyServerRecipients() { const server = await Server.findOne({ _id: server_id }).lean(); const io = getIO(); - if (!server || !io) { - return; - } + if (!server || !io) return; const recipients = (server.users || []).filter( (user) => user.user_id !== id, ); for (const recipient of recipients) { await incrementServerUnread(recipient.user_id, server_id, channel_id); - const shouldNotify = await shouldSendNotification(recipient.user_id, "server_messages"); + const shouldNotify = await shouldSendNotification( + recipient.user_id, + "server_messages", + ); if (shouldNotify) { - io.to(recipient.user_id).emit("server_message_notification", { + io.to(recipient.user_id).emit("server_messaage_notification", { server_id, channel_id, channel_name, @@ -88,93 +84,86 @@ router.post("/store_message", expressRateLimit("chat"), async (req, res) => { } } - if (!response || response.length === 0) { - const pushNewChannel = { - $push: { - channels: [ - { - channel_id, - channel_name, - chat_details: [chatMessage], - }, - ], - }, - }; - try { - const data = await Chat.updateOne({ server_id }, pushNewChannel); - if (data && data.modifiedCount > 0) { - await cache.del(`chat:${server_id}:${channel_id}`); - await notifyServerRecipients(); - const io = getIO(); - if (io) { - io.to(`channel:${channel_id}`).emit( - "server_message_received", - chatMessage, - ); - } - return res.json({ status: 200, message: chatMessage }); - } - return res.status(500).json({ status: 500, message: "Update failed" }); - } catch (err) { - return res.status(500).json({ status: 500, message: "Server error" }); - } - } else { - const pushNewChat = { - $push: { - "channels.$.chat_details": [chatMessage], - }, - }; - try { - const data = await Chat.updateOne( - { server_id, "channels.channel_id": channel_id }, - pushNewChat, + try { + const newMessage = new Message({ + sender: id, + channelId: channel_id, + content: message, + timestamp: timestamp, + sender_name: username, + sender_pic: profile_pic, + sender_tag: tag, + }); + + await newMessage.save(); + + await Chat.updateOne( + { server_id, "channels.channel_id": channel_id }, + { $setOnInsert: { server_id, channels: [{ channel_id, channel_name }] } }, + { upsert: true }, + ); + + await cache.del(`chat:${server_id}:${channel_id}`); + await notifyServerRecipients(); + + const io = getIO(); + if (io) { + io.to(`channel:${channel_id}`).emit( + "server_message_received", + chatMessage, ); - if (data && data.modifiedCount > 0) { - await cache.del(`chat:${server_id}:${channel_id}`); - await notifyServerRecipients(); - const io = getIO(); - if (io) { - io.to(`channel:${channel_id}`).emit( - "server_message_received", - chatMessage, - ); - } - return res.json({ status: 200, message: chatMessage }); - } - return res.status(500).json({ status: 500, message: "Update failed" }); - } catch (err) { - return res.status(500).json({ status: 500, message: "Server error" }); } + + return res.json({ status: 200, message: chatMessage }); + } catch (err) { + console.error("Store message error:", err); + return res.status(500).json({ status: 500, message: "Server error" }); } }); router.post("/get_messages", async (req, res) => { - const { channel_id, server_id } = req.body; + const { channel_id, server_id, cursor } = req.body; if (!channel_id || !server_id) { return res .status(400) - .json({ error: "Invalid request. Missing channel_id or server_id." }); + .json({ error: "Invalid request.. Missing channel_id or server_id." }); } try { const cacheKey = `chat:${server_id}:${channel_id}`; - const cached = await cache.getJson(cacheKey); - if (cached && Array.isArray(cached.chats)) { - return res.json({ chats: cached.chats, cached: true }); + + if (!cursor) { + const cached = await cache.getJson(cacheKey); + if (cached && Array.isArray(cached.chats)) { + return res.json({ chats: cached.chats, cached: true }); + } + } + + let query = { channelId: channel_id }; + if (cursor) { + query.timestamp = { $lt: cursor }; } - const response = await getChats(server_id, channel_id); - if ( - !response || - !response[0] || - !response[0].channels || - response[0].channels.length === 0 - ) { - return res.json({ chats: [] }); + const msgs = await Message.find(query) + .sort({ timestamp: -1 }) + .limit(50) + .lean(); + + const reversedMsgs = msgs.reverse(); + const chats = reversedMsgs.map((m) => ({ + content: m.content, + sender_id: String(m.sender), + sender_name: m.sender_name, + sender_pic: m.sender_pic, + sender_tag: m.sender_tag, + timestamp: new Date(m.timestamp).getTime(), + })); + + if (!cursor) { + await cache.setJson(cacheKey, { chats }); } - const chats = response[0].channels[0].chat_details || []; - await cache.setJson(cacheKey, { chats }); + return res.json({ chats }); } catch (error) { logger.error(`Error retrieving chats: ${error.message}`); @@ -185,49 +174,33 @@ router.post("/get_messages", async (req, res) => { router.post("/edit_server_message", async (req, res) => { const { server_id, channel_id, timestamp, content } = req.body; const user = getAuthorizedUser(req, res); - if (!user) { - return; - } - const senderId = user.id; + if (!user) return; - if (!server_id || !channel_id || !timestamp || !content || !content.trim()) { + if (!channel_id || !timestamp || !content || !content.trim()) { return res.status(400).json({ status: 400, message: "Invalid input" }); } try { - const chatDoc = await Chat.findOne({ - server_id, - "channels.channel_id": channel_id, - }); - if (!chatDoc) { - return res.status(404).json({ status: 404, message: "Chat not found" }); - } - - const channel = chatDoc.channels.find( - (entry) => entry.channel_id === channel_id, - ); - const message = channel?.chat_details.find( - (entry) => - String(entry.timestamp) === String(timestamp) && - entry.sender_id === senderId, + const updatedMsg = await Message.findOneAndUpdate( + { channelId: channel_id, timestamp: timestamp, sender: user.id }, + { content: content.trim(), isEdited: true }, + { new: true }, ); - if (!message) { + if (!updatedMsg) { return res .status(404) - .json({ status: 404, message: "Message not found" }); + .json({ status: 404, message: "Message isn't found" }); } - message.content = content.trim(); - await chatDoc.save(); await cache.del(`chat:${server_id}:${channel_id}`); const io = getIO(); if (io) { io.to(`channel:${channel_id}`).emit("server_message_updated", { timestamp, - sender_id: senderId, - content: message.content, + sender_id: user.id, + content: updatedMsg.content, }); } @@ -241,55 +214,36 @@ router.post("/edit_server_message", async (req, res) => { router.post("/delete_server_message", async (req, res) => { const { server_id, channel_id, timestamp } = req.body; const user = getAuthorizedUser(req, res); - if (!user) { - return; - } - const senderId = user.id; + if (!user) return; - if (!server_id || !channel_id || !timestamp) { + if (!channel_id || !timestamp) { return res.status(400).json({ status: 400, message: "Invalid input" }); } try { - const chatDoc = await Chat.findOne({ - server_id, - "channels.channel_id": channel_id, + const deletedMsg = await Message.findOneAndDelete({ + channelId: channel_id, + timestamp: timestamp, + sender: user.id, }); - if (!chatDoc) { - return res.status(404).json({ status: 404, message: "Chat not found" }); - } - - const channel = chatDoc.channels.find( - (entry) => entry.channel_id === channel_id, - ); - const originalLength = channel?.chat_details.length || 0; - - channel.chat_details = channel.chat_details.filter( - (entry) => - !( - String(entry.timestamp) === String(timestamp) && - entry.sender_id === senderId - ), - ); - if (channel.chat_details.length === originalLength) { + if (!deletedMsg) { return res .status(404) .json({ status: 404, message: "Message not found" }); } - await chatDoc.save(); await cache.del(`chat:${server_id}:${channel_id}`); const io = getIO(); if (io) { io.to(`channel:${channel_id}`).emit("server_message_deleted", { timestamp, - sender_id: senderId, + sender_id: user.id, }); } - res.status(200).json({ status: 200, message: "Message deleted" }); + res.status(200).json({ status: 200, message: "Message is deleted" }); } catch (error) { logger.error(`Error deleting message: ${error.message}`); res.status(500).json({ status: 500, message: "Failed to delete message" }); diff --git a/server/src/routes/directMessages.js b/server/src/routes/directMessages.js index 47dd6cc..00fc5a9 100644 --- a/server/src/routes/directMessages.js +++ b/server/src/routes/directMessages.js @@ -5,6 +5,7 @@ import jwt from "jsonwebtoken"; import DirectMessageThread from "../models/DirectMessageThread.js"; import User from "../models/User.js"; +import Message from "../models/Message.js"; import * as cache from "../lib/cache.js"; import { incrementDmUnread } from "../services/unreadService.js"; import { getIO } from "../socket/runtime.js"; @@ -35,43 +36,75 @@ function getThreadParticipants(userId, friendId) { return [userId, friendId].sort(); } +function toClientTimestamp(timestamp) { + return new Date(timestamp).getTime(); +} + router.post("/get_direct_messages", async (req, res) => { const user = getAuthorizedUser(req, res); - if (!user) { - return; - } - - const { friend_id } = req.body; + if (!user) return; + const { friend_id, cursor } = req.body; if (!friend_id) { - return res.status(400).json({ message: "friend_id is required", status: 400 }); + return res + .status(400) + .json({ message: "friend_id is required", status: 400 }); } const participants = getThreadParticipants(user.id, friend_id); const cacheKey = `dm:${participants[0]}:${participants[1]}`; - const cached = await cache.getJson(cacheKey); - if (cached && Array.isArray(cached.messages)) { - return res.status(200).json({ status: 200, messages: cached.messages, cached: true }); + + if (!cursor) { + const cached = await cache.getJson(cacheKey); + if (cached && Array.isArray(cached.messages)) { + return res + .status(200) + .json({ status: 200, messages: cached.messages, cached: true }); + } } - const thread = await DirectMessageThread.findOne({ - participants, - }).lean(); + try { + const thread = await DirectMessageThread.findOne({ participants }).lean(); + + if (!thread) { + return res.status(200).json({ status: 200, messages: [] }); + } + + let query = { threadId: thread._id }; + if (cursor) { + query.timestamp = { $lt: cursor }; + } + + const msgs = await Message.find(query) + .sort({ timestamp: -1 }) + .limit(50) + .lean(); - const messages = thread?.messages || []; - await cache.setJson(cacheKey, { messages }); + const reversedMsgs = msgs.reverse(); - return res.status(200).json({ - status: 200, - messages, - }); + const messages = reversedMsgs.map((m) => ({ + sender_id: String(m.sender), + sender_name: m.sender_name, + sender_tag: m.sender_tag, + sender_pic: m.sender_pic, + content: m.content, + timestamp: toClientTimestamp(m.timestamp), + })); + + if (!cursor) { + await cache.setJson(cacheKey, { messages }); + } + + return res.status(200).json({ status: 200, messages }); + } catch (error) { + console.error("Error retrieving DMs:", error); + return res.status(500).json({ message: "Server error", status: 500 }); + } }); router.post("/send_direct_message", async (req, res) => { const user = getAuthorizedUser(req, res); - if (!user) { - return; - } + if (!user) return; const { friend_id, content } = req.body; @@ -83,80 +116,95 @@ router.post("/send_direct_message", async (req, res) => { const friend = await User.findOne({ _id: friend_id }).lean(); if (!currentUser || !friend) { - return res.status(404).json({ message: "User not found", status: 404 }); + return res.status(404).json({ message: "User isn't found", status: 404 }); } - const isFriend = (currentUser.friends || []).some((entry) => entry.id === friend_id); + const isFriend = (currentUser.friends || []).some( + (entry) => entry.id === friend_id, + ); if (!isFriend) { - return res.status(403).json({ message: "Users are not friends", status: 403 }); + return res + .status(403) + .json({ message: "Users aren't friends", status: 403 }); } const currentUserId = String(currentUser._id); const friendUserId = String(friend._id); - - const message = { - sender_id: currentUserId, - sender_name: currentUser.username, - sender_tag: currentUser.tag, - sender_pic: currentUser.profile_pic, - content: content.trim(), - timestamp: Date.now(), - }; + const timestamp = Date.now(); const participants = getThreadParticipants(currentUserId, friendUserId); - await DirectMessageThread.findOneAndUpdate( - { participants }, - { - $setOnInsert: { participants }, - $push: { messages: message }, - }, - { upsert: true, returnDocument: "after" } - ); - await cache.del(`dm:${participants[0]}:${participants[1]}`); - - const io = getIO(); - if (io) { - await incrementDmUnread(friendUserId, currentUserId); - io.to(friendUserId).emit("direct_message_received", { - friend_id: currentUserId, - sender_id: currentUserId, - sender_name: currentUser.username, - sender_tag: currentUser.tag, - sender_pic: currentUser.profile_pic, - content: message.content, - timestamp: message.timestamp, - }); - io.to(currentUserId).emit("direct_message_received", { - friend_id: friendUserId, - sender_id: currentUserId, + try { + const thread = await DirectMessageThread.findOneAndUpdate( + { participants }, + { $setOnInsert: { participants } }, + { upsert: true, new: true }, + ); + + const newMessage = new Message({ + sender: currentUserId, + threadId: thread._id, + content: content.trim(), + timestamp: timestamp, sender_name: currentUser.username, sender_tag: currentUser.tag, sender_pic: currentUser.profile_pic, - content: message.content, - timestamp: message.timestamp, }); - const shouldNotify = await shouldSendNotification(friendUserId, "direct_messages"); - if (shouldNotify) { - io.to(friendUserId).emit("direct_message_notification", { + + await newMessage.save(); + await cache.del(`dm:${participants[0]}:${participants[1]}`); + + const io = getIO(); + if (io) { + await incrementDmUnread(friendUserId, currentUserId); + const socketMessage = { friend_id: currentUserId, + sender_id: currentUserId, sender_name: currentUser.username, - }); + sender_tag: currentUser.tag, + sender_pic: currentUser.profile_pic, + content: newMessage.content, + timestamp: toClientTimestamp(newMessage.timestamp), + }; + + io.to(friendUserId).emit("direct_message_received", socketMessage); + + socketMessage.friend_id = friendUserId; + io.to(currentUserId).emit("direct_message_received", socketMessage); + + const shouldNotify = await shouldSendNotification( + friendUserId, + "direct_messages", + ); + if (shouldNotify) { + io.to(friendUserId).emit("direct_message_notification", { + friend_id: currentUserId, + sender_name: currentUser.username, + }); + } } - } - return res.status(200).json({ - status: 200, - message: "Message sent", - data: message, - }); + return res.status(200).json({ + status: 200, + message: "Message sent", + data: { + sender_id: currentUserId, + sender_name: currentUser.username, + sender_tag: currentUser.tag, + sender_pic: currentUser.profile_pic, + content: newMessage.content, + timestamp: toClientTimestamp(newMessage.timestamp), + }, + }); + } catch (error) { + console.error("Error sending DM:", error); + return res.status(500).json({ message: "Server error", status: 500 }); + } }); router.post("/edit_direct_message", async (req, res) => { const user = getAuthorizedUser(req, res); - if (!user) { - return; - } + if (!user) return; const { friend_id, timestamp, content } = req.body; if (!friend_id || !timestamp || !content || !content.trim()) { @@ -164,48 +212,53 @@ router.post("/edit_direct_message", async (req, res) => { } const participants = getThreadParticipants(user.id, friend_id); - const thread = await DirectMessageThread.findOne({ participants }); - if (!thread) { - return res.status(404).json({ status: 404, message: "Thread not found" }); - } + try { + const thread = await DirectMessageThread.findOne({ participants }).lean(); + if (!thread) { + return res.status(404).json({ status: 404, message: "Thread not found" }); + } - const message = thread.messages.find( - (entry) => String(entry.timestamp) === String(timestamp) && entry.sender_id === user.id - ); + const updatedMsg = await Message.findOneAndUpdate( + { threadId: thread._id, timestamp: timestamp, sender: user.id }, + { content: content.trim(), isEdited: true }, + { new: true }, + ); - if (!message) { - return res.status(404).json({ status: 404, message: "Message not found" }); - } + if (!updatedMsg) { + return res + .status(404) + .json({ status: 404, message: "Message not found" }); + } - message.content = content.trim(); - await thread.save(); - await cache.del(`dm:${participants[0]}:${participants[1]}`); - - const io = getIO(); - if (io) { - io.to(friend_id).emit("direct_message_updated", { - friend_id: user.id, - timestamp, - content: message.content, - sender_id: user.id, - }); - io.to(user.id).emit("direct_message_updated", { - friend_id, - timestamp, - content: message.content, - sender_id: user.id, - }); - } + await cache.del(`dm:${participants[0]}:${participants[1]}`); + + const io = getIO(); + if (io) { + io.to(friend_id).emit("direct_message_updated", { + friend_id: user.id, + timestamp, + content: updatedMsg.content, + sender_id: user.id, + }); + io.to(user.id).emit("direct_message_updated", { + friend_id, + timestamp, + content: updatedMsg.content, + sender_id: user.id, + }); + } - return res.status(200).json({ status: 200, message: "Message updated" }); + return res.status(200).json({ status: 200, message: "Message updated" }); + } catch (error) { + console.error("Error editing DM:", error); + return res.status(500).json({ message: "Server error", status: 500 }); + } }); router.post("/delete_direct_message", async (req, res) => { const user = getAuthorizedUser(req, res); - if (!user) { - return; - } + if (!user) return; const { friend_id, timestamp } = req.body; if (!friend_id || !timestamp) { @@ -213,39 +266,48 @@ router.post("/delete_direct_message", async (req, res) => { } const participants = getThreadParticipants(user.id, friend_id); - const thread = await DirectMessageThread.findOne({ participants }); - if (!thread) { - return res.status(404).json({ status: 404, message: "Thread not found" }); - } + try { + const thread = await DirectMessageThread.findOne({ participants }).lean(); + if (!thread) { + return res + .status(404) + .json({ status: 404, message: "Thread is not found" }); + } - const originalLength = thread.messages.length; - thread.messages = thread.messages.filter( - (entry) => !(String(entry.timestamp) === String(timestamp) && entry.sender_id === user.id) - ); + const deletedMsg = await Message.findOneAndDelete({ + threadId: thread._id, + timestamp: timestamp, + sender: user.id, + }); - if (thread.messages.length === originalLength) { - return res.status(404).json({ status: 404, message: "Message not found" }); - } + if (!deletedMsg) { + return res + .status(404) + .json({ status: 404, message: "Message isn't found" }); + } - await thread.save(); - await cache.del(`dm:${participants[0]}:${participants[1]}`); + await cache.del(`dm:${participants[0]}:${participants[1]}`); - const io = getIO(); - if (io) { - io.to(friend_id).emit("direct_message_deleted", { - friend_id: user.id, - timestamp, - sender_id: user.id, - }); - io.to(user.id).emit("direct_message_deleted", { - friend_id, - timestamp, - sender_id: user.id, - }); - } + const io = getIO(); + if (io) { + io.to(friend_id).emit("direct_message_deleted", { + friend_id: user.id, + timestamp, + sender_id: user.id, + }); + io.to(user.id).emit("direct_message_deleted", { + friend_id, + timestamp, + sender_id: user.id, + }); + } - return res.status(200).json({ status: 200, message: "Message deleted" }); + return res.status(200).json({ status: 200, message: "Message deleted" }); + } catch (error) { + console.error("Error deleting DM:", error); + return res.status(500).json({ message: "Server error", status: 500 }); + } }); export default router;