A Place Where Random People Meet And Chat.

Compare changes

Choose any two refs to compare.

-53
README.md
··· 1 - # Not A Friend 2 - 3 - A application which provides a platform for random strangers to meet and chat. 4 - 5 - --- 6 - ## What This Project Demonstrates 7 - 8 - - Real-time communication using WebSockets 9 - - User session and state management 10 - - Friend request and relationship handling 11 - - Client-side data persistence with IndexedDB 12 - - Managing live connections and disconnections 13 - 14 - --- 15 - 16 - ## Tech Stack 17 - 18 - - **Frontend:** React 19 - - **Backend:** Node.js, Express 20 - - **Database:** PostgreSQL, Redis 21 - - **Client Side Storage**: IndexedDB 22 - - **Other Tools:** WebSockets, Prisma 23 - 24 - --- 25 - 26 - ## Getting Started 27 - 28 - Steps to run the project locally: 29 - 30 - 1. Installing Requirements 31 - 1. Node 32 - 2. PostgreSQL 33 - 3. Redis 34 - 35 - 2. Running Client 36 - 37 - ```bash 38 - git clone https://tangled.org/jai44.tngl.sh/NotAFriend 39 - cd NotAFriend 40 - cd client 41 - npm install 42 - npm run dev 43 - ``` 44 - 45 - 3. Running Server 46 - 47 - ``` bash 48 - cd ../server 49 - npm install 50 - cd prisma 51 - npx prisma generate 52 - npm run dev 53 - ```
+1 -40
client/package-lock.json
··· 9 9 "version": "0.0.0", 10 10 "dependencies": { 11 11 "axios": "^1.12.2", 12 - "idb": "^8.0.3", 13 12 "react": "^19.1.1", 14 13 "react-dom": "^19.1.1", 15 - "react-router-dom": "^7.9.2", 16 - "s": "^1.0.0", 17 - "ws": "^8.18.3" 14 + "react-router-dom": "^7.9.2" 18 15 }, 19 16 "devDependencies": { 20 17 "@eslint/js": "^9.36.0", ··· 2076 2073 "node": ">= 0.4" 2077 2074 } 2078 2075 }, 2079 - "node_modules/idb": { 2080 - "version": "8.0.3", 2081 - "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", 2082 - "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", 2083 - "license": "ISC" 2084 - }, 2085 2076 "node_modules/ignore": { 2086 2077 "version": "5.3.2", 2087 2078 "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", ··· 2575 2566 "fsevents": "~2.3.2" 2576 2567 } 2577 2568 }, 2578 - "node_modules/s": { 2579 - "version": "1.0.0", 2580 - "resolved": "https://registry.npmjs.org/s/-/s-1.0.0.tgz", 2581 - "integrity": "sha512-Tz63UXhdEBvvIV6Q0a+AV2Dx1TPq+vVWNYBxyCT9TG0uqn9kySwFTjfq3C1YuGBRwYtt9Tof11L6GCKi88foqw==", 2582 - "license": "Apache-2.0", 2583 - "engines": { 2584 - "node": ">=0.8" 2585 - } 2586 - }, 2587 2569 "node_modules/scheduler": { 2588 2570 "version": "0.26.0", 2589 2571 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", ··· 2794 2776 "license": "MIT", 2795 2777 "engines": { 2796 2778 "node": ">=0.10.0" 2797 - } 2798 - }, 2799 - "node_modules/ws": { 2800 - "version": "8.18.3", 2801 - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", 2802 - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", 2803 - "license": "MIT", 2804 - "engines": { 2805 - "node": ">=10.0.0" 2806 - }, 2807 - "peerDependencies": { 2808 - "bufferutil": "^4.0.1", 2809 - "utf-8-validate": ">=5.0.2" 2810 - }, 2811 - "peerDependenciesMeta": { 2812 - "bufferutil": { 2813 - "optional": true 2814 - }, 2815 - "utf-8-validate": { 2816 - "optional": true 2817 - } 2818 2779 } 2819 2780 }, 2820 2781 "node_modules/yocto-queue": {
+1 -4
client/package.json
··· 11 11 }, 12 12 "dependencies": { 13 13 "axios": "^1.12.2", 14 - "idb": "^8.0.3", 15 14 "react": "^19.1.1", 16 15 "react-dom": "^19.1.1", 17 - "react-router-dom": "^7.9.2", 18 - "s": "^1.0.0", 19 - "ws": "^8.18.3" 16 + "react-router-dom": "^7.9.2" 20 17 }, 21 18 "devDependencies": { 22 19 "@eslint/js": "^9.36.0",
+1 -2
client/src/App.jsx
··· 5 5 import Login from "./pages/Login.jsx"; 6 6 import Chat from "./pages/Chat.jsx"; 7 7 import Signup from "./pages/Signup.jsx"; 8 - import Friends from "./pages/Friends.jsx" 8 + 9 9 function App() { 10 10 return ( 11 11 <> ··· 15 15 <Route path="/login" element={<Login />} /> 16 16 <Route path="/signup" element={<Signup />} /> 17 17 <Route path="/chat" element={<Chat />} /> 18 - <Route path="/friends" element={<Friends />} /> 19 18 </Routes> 20 19 </> 21 20 );
-201
client/src/Hooks/useWebSocket.jsx
··· 1 - 2 - import { useCallback, useState, useEffect, useRef } from "react"; 3 - import { createFriend } from "../features/indexedDB/indexedDb"; 4 - import { useWebSocketContext } from "../helper/webSocketContext"; 5 - 6 - export function useWebSocket(userId) { 7 - 8 - // Storing Messages 9 - const [sentMessages, setSentMessages] = useState([]); 10 - const [receiveMessages, setReceiveMessages] = useState([]); 11 - 12 - // Connections 13 - const [isConnected, setIsConnected] = useState(false); 14 - const [isSearching, setIsSearching] = useState(false); 15 - const [matched, setMatched] = useState(false); 16 - const [terminated, setTerminated] = useState(false); 17 - const [isAccepted, setIsAccepted] = useState(null); 18 - const { isFriendShipExists, setIsFriendShipExists } = useWebSocketContext(); 19 - 20 - // receiver 21 - const [receiver, setReceiver] = useState( 22 - localStorage.getItem("receiver") 23 - ); 24 - 25 - //Friend Request 26 - const [isReceived, setIsReceived] = useState(false); 27 - const [showFriendRequest, setShowFriendRequest] = useState(false); 28 - 29 - //Socket 30 - const socketRef = useRef(null); 31 - 32 - const connect = useCallback(() => { 33 - //Prevent duplicate connections 34 - if (isConnected || !userId || socketRef.current) return; 35 - 36 - setIsSearching(true); 37 - console.log(`Attempting to connect to WebSocket with userId: ${userId}`); 38 - socketRef.current = new WebSocket(`ws://localhost:3000/?userId=${userId}`); 39 - 40 - socketRef.current.onopen = () => { 41 - console.log("Web Socket Is Connected"); 42 - setIsConnected(true); 43 - }; 44 - 45 - socketRef.current.onmessage = (payload) => { 46 - try { 47 - const data = JSON.parse(payload.data); 48 - 49 - switch (data.type) { 50 - case "matched": 51 - setIsSearching(false); 52 - setMatched(true); 53 - setReceiver(data.with); 54 - localStorage.setItem("receiver", data.with); 55 - break; 56 - 57 - case "disconnected": 58 - setIsSearching(true); 59 - setMatched(false); 60 - setReceiver(null); 61 - localStorage.removeItem("receiver"); 62 - break; 63 - 64 - case "no_match": 65 - setIsSearching(false); 66 - setMatched(false); 67 - setReceiver(null); 68 - localStorage.removeItem("receiver"); 69 - break; 70 - 71 - case "friendship_created": 72 - createFriend(data.id, data.messages.sentMessages, data.messages.receivedMessages) 73 - setIsAccepted(true) 74 - break; 75 - 76 - case "already friends": 77 - setIsFriendShipExists(true) 78 - break; 79 - 80 - default: 81 - if (data.action === 'message') { 82 - setReceiveMessages(prev => [...prev, data]); 83 - } else if (data.type === 'request') { 84 - setShowFriendRequest(true); 85 - setIsReceived(true); 86 - } break; 87 - } 88 - } catch (err) { 89 - console.log("Error Parsing Message:", err); 90 - } 91 - }; 92 - 93 - // Closing Connection 94 - socketRef.current.onclose = () => { 95 - setTerminated(true); 96 - setIsConnected(false); 97 - setIsSearching(false); 98 - }; 99 - 100 - socketRef.current.onerror = (err) => { 101 - console.error("WebSocket Error:", err); 102 - setIsSearching(false); 103 - }; 104 - 105 - }, [userId, isConnected]); 106 - 107 - // Disconnect 108 - const disconnect = useCallback(() => { 109 - if (socketRef.current) { 110 - socketRef.current.close(); 111 - socketRef.current = null; // Clear ref 112 - } 113 - setIsConnected(false); 114 - setIsSearching(false); 115 - }, []); 116 - 117 - // Reset 118 - const reset = useCallback(() => { 119 - if (socketRef.current) { 120 - socketRef.current.close(); 121 - socketRef.current = null; // Clear ref 122 - } 123 - setIsConnected(false); 124 - setIsSearching(false); 125 - setMatched(false); 126 - setReceiver(null); 127 - localStorage.removeItem("receiver"); 128 - setIsAccepted(null); 129 - }, []); 130 - 131 - //Sending Messages 132 - const sendMessage = useCallback((message) => { 133 - if (!socketRef.current || !matched) return; 134 - 135 - const payload = { 136 - id: userId, 137 - action: "message", 138 - type: "sent", 139 - message, 140 - time: new Date().toISOString(), 141 - }; 142 - setSentMessages(prev => [...prev, payload]); 143 - socketRef.current.send(JSON.stringify(payload)); 144 - }, [userId, matched]); 145 - 146 - // Sending Friend Requets 147 - const sendFriendRequest = useCallback(() => { 148 - if (!socketRef.current || !matched) return; 149 - 150 - const payload = { 151 - id: userId, 152 - type: "request", 153 - }; 154 - socketRef.current.send(JSON.stringify(payload)); 155 - }, [userId, matched]); 156 - 157 - // Responding Friend Requests 158 - const respondToRequest = useCallback((accepted) => { 159 - if (!socketRef.current) return; 160 - 161 - const payload = { 162 - id: userId, 163 - type: "response", 164 - action: accepted ? "accept" : "reject", 165 - }; 166 - 167 - socketRef.current.send(JSON.stringify(payload)); 168 - setShowFriendRequest(false); 169 - }, [userId]); 170 - 171 - // Closing Connection on unMount 172 - useEffect(() => { 173 - return () => { 174 - if (socketRef.current) { 175 - socketRef.current.close(); 176 - socketRef.current = null; // Clear ref to prevent memory leaks 177 - } 178 - }; 179 - }, []); 180 - 181 - return { 182 - sentMessages, 183 - receiveMessages, 184 - isConnected, 185 - isSearching, 186 - matched, 187 - receiver, 188 - terminated, 189 - showFriendRequest, 190 - connect, 191 - disconnect, 192 - reset, 193 - sendMessage, 194 - sendFriendRequest, 195 - isAccepted, 196 - isReceived, 197 - respondToRequest, 198 - isFriendShipExists, 199 - }; 200 - } 201 -
-95
client/src/components/Chat/ChatView.jsx
··· 1 - 2 - import { useState, useMemo } from "react"; 3 - import { useWebSocket } from "../../Hooks/useWebSocket"; 4 - import { useWebSocketContext } from "../../helper/webSocketContext"; 5 - 6 - export default function ChatView({ 7 - receiver, 8 - sentMessages, 9 - receiveMessages, 10 - onSendMessage, 11 - onSendRequest, 12 - isReceived, 13 - isAccepted, 14 - }) { 15 - const [currMessage, setCurrMessage] = useState(""); 16 - const [isSent, setIsSent] = useState(false); 17 - const userId = localStorage.getItem('userId') 18 - 19 - const { isFriendShipExists } = useWebSocketContext(); 20 - 21 - const sortedSentMessages = useMemo( 22 - () => [...sentMessages].sort((a, b) => new Date(a.time) - new Date(b.time)), 23 - [sentMessages] 24 - ); 25 - 26 - const sortedReceiveMessages = useMemo( 27 - () => [...receiveMessages].sort((a, b) => new Date(a.time) - new Date(b.time)), 28 - [receiveMessages] 29 - ); 30 - 31 - const mergeSorted = (a, b) => { 32 - const res = []; 33 - let i = 0, j = 0; 34 - 35 - while (i < a.length || j < b.length) { 36 - if (i === a.length) res.push(b[j++]); 37 - else if (j === b.length) res.push(a[i++]); 38 - else res.push( 39 - new Date(a[i].time) < new Date(b[j].time) ? a[i++] : b[j++] 40 - ); 41 - } 42 - return res; 43 - }; 44 - 45 - const sortedMessages = mergeSorted(sortedSentMessages, sortedReceiveMessages); 46 - 47 - const handleKeyPress = (e) => { 48 - if (e.key === "Enter" && currMessage.trim() !== "") { 49 - onSendMessage(currMessage); 50 - setCurrMessage(""); 51 - } 52 - }; 53 - 54 - const handleClick = () => { 55 - setIsSent(true); 56 - onSendRequest(); 57 - }; 58 - 59 - return ( 60 - <div> 61 - <h2>Chat with {receiver}</h2> 62 - 63 - <input 64 - type="text" 65 - value={currMessage} 66 - onChange={(e) => setCurrMessage(e.target.value)} 67 - onKeyDown={handleKeyPress} 68 - placeholder="Type a message..." 69 - /> 70 - 71 - <ul> 72 - {sortedMessages.map((msg, i) => ( 73 - <li key={i}> 74 - {msg.id}: {msg.message} 75 - </li> 76 - ))} 77 - </ul> 78 - 79 - {!isSent && !isReceived && !isFriendShipExists && ( 80 - <button onClick={handleClick}>Send Friend Request</button> 81 - )} 82 - 83 - {isSent && ( 84 - isAccepted == null ? ( 85 - <p>Request Pending</p> 86 - ) : isAccepted ? ( 87 - <p>Request Accepted</p> 88 - ) : ( 89 - <p>Request Rejected</p> 90 - ) 91 - )} 92 - </div> 93 - ); 94 - } 95 -
-13
client/src/components/Chat/InitialState.jsx
··· 1 - 2 - export default function InitialState({ onConnect}) { 3 - const handleClick = () => { 4 - const userId = localStorage.getItem("userId"); 5 - if (!userId) { 6 - window.alert("Please login first"); 7 - return; 8 - } 9 - onConnect(); 10 - }; 11 - 12 - return <button onClick={handleClick}>I want a friend</button>; 13 - }
-9
client/src/components/Chat/NoMatchState.jsx
··· 1 - 2 - export default function NoMatchState({ onTryAgain }) { 3 - return ( 4 - <div> 5 - <p>No match found. Try again later.</p> 6 - <button onClick={onTryAgain}>Try Again</button> 7 - </div> 8 - ); 9 - }
-9
client/src/components/Chat/SearchingState.jsx
··· 1 - 2 - export default function SearchingState({ onCancel }) { 3 - return ( 4 - <div> 5 - <p>Searching for a friend...</p> 6 - <button onClick={onCancel}>Cancel</button> 7 - </div> 8 - ); 9 - }
+1 -2
client/src/components/Navbar.jsx
··· 7 7 <Link to="/">Home</Link> | 8 8 <Link to="/login">Login</Link> | 9 9 <Link to="/signup">Signup</Link> | 10 - <Link to="/chat">Chat</Link> | 11 - <Link to="/friends">Friends</Link> 10 + <Link to="/chat">Chat</Link> 12 11 </nav> 13 12 ) 14 13 }
-44
client/src/components/UI/CustomAlert.jsx
··· 1 - 2 - export default function CustomAlert({ message, onAccept, onReject }) { 3 - const styles = { 4 - overlay: { 5 - position: "fixed", 6 - top: 0, 7 - left: 0, 8 - width: "100vw", 9 - height: "100vh", 10 - backgroundColor: "rgba(0, 0, 0, 0.5)", 11 - display: "flex", 12 - alignItems: "center", 13 - justifyContent: "center", 14 - zIndex: 9999, 15 - }, 16 - alertBox: { 17 - backgroundColor: "white", 18 - padding: "20px", 19 - borderRadius: "8px", 20 - boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)", 21 - textAlign: "center", 22 - }, 23 - button: { 24 - margin: "0 5px", 25 - padding: "8px 16px", 26 - cursor: "pointer", 27 - }, 28 - }; 29 - 30 - return ( 31 - <div style={styles.overlay}> 32 - <div style={styles.alertBox}> 33 - <p>{message}</p> 34 - <button style={styles.button} onClick={onAccept}> 35 - Accept 36 - </button> 37 - <button style={styles.button} onClick={onReject}> 38 - Reject 39 - </button> 40 - </div> 41 - </div> 42 - ); 43 - } 44 -
-71
client/src/features/indexedDB/indexedDb.jsx
··· 1 - import { openDB } from 'idb'; 2 - 3 - const user = localStorage.getItem("user"); 4 - const dbName = String(user); 5 - 6 - // Open or create a database 7 - export const db = await openDB(dbName, 1, { 8 - upgrade(db) { 9 - if (!db.objectStoreNames.contains("Friends")) { 10 - const store = db.createObjectStore("Friends", { keyPath: "id" }); 11 - console.log("Object store 'Friends' created."); 12 - } 13 - } 14 - }); 15 - 16 - // Store Messages Function 17 - export const storeMessages = async (data, friendId, type) => { 18 - const tx = db.transaction("Friends", "readwrite"); 19 - const store = tx.store; 20 - 21 - if (type === "receive") { 22 - friend.receivedMessages.push(data); 23 - } else if (type === "send") { 24 - friend.sentMessages.push(data); 25 - } else { 26 - console.warn("Invalid message type:", type); 27 - } 28 - 29 - await store.put(friend); 30 - await tx.done; 31 - 32 - console.log("Message stored successfully for friend:", friendId); 33 - }; 34 - 35 - // Create A Friend 36 - export const createFriend = async (friendId, receivedMessages = [], sentMessages = []) => { 37 - 38 - const tx = db.transaction("Friends", "readwrite"); 39 - const store = tx.store; 40 - 41 - // Create the friend object 42 - const friend = { 43 - id: friendId, 44 - receivedMessages, 45 - sentMessages, 46 - createdAt: new Date(), 47 - }; 48 - 49 - // Add or update the friend record 50 - await store.put(friend); 51 - 52 - await tx.done; 53 - 54 - console.log(`Friend ${friendId} saved locally withd sent and received messages.`); 55 - }; 56 - 57 - // Get Messages Function 58 - export const getMessages = async (friendId) => { 59 - const friend = await db.get("Friends", friendId); 60 - const sent = friend?.sentMessages || []; 61 - const received = friend?.receivedMessages || []; 62 - return [sent, received]; 63 - }; 64 - 65 - export const getFriends = async () => { 66 - 67 - if (!db) throw new Error("Database not initalized"); 68 - const friendIds = await db.getAllKeys("Friends"); 69 - 70 - return friendIds; 71 - }
-18
client/src/helper/webSocketContext.jsx
··· 1 - 2 - import { createContext, useContext, useState } from "react"; 3 - 4 - const WebSocketContext = createContext(); 5 - 6 - export function WebSocketProvider({ children }) { 7 - const [isFriendShipExists, setIsFriendShipExists] = useState(false); 8 - 9 - return ( 10 - <WebSocketContext.Provider value={{ isFriendShipExists, setIsFriendShipExists }}> 11 - {children} 12 - </WebSocketContext.Provider> 13 - ); 14 - } 15 - 16 - export function useWebSocketContext() { 17 - return useContext(WebSocketContext); 18 - }
+2 -5
client/src/main.jsx
··· 2 2 import ReactDOM from "react-dom/client" 3 3 import App from "./App.jsx" 4 4 import { BrowserRouter as Router } from "react-router-dom" 5 - import { WebSocketProvider } from "./helper/webSocketContext"; 6 - 7 5 8 6 ReactDOM.createRoot(document.getElementById("root")).render( 9 7 <Router> 10 - <WebSocketProvider> 11 - <App /> 12 - </WebSocketProvider> 8 + <App /> 13 9 </Router> 14 10 ) 11 +
+126 -59
client/src/pages/Chat.jsx
··· 1 - 2 - import { useWebSocket } from "../Hooks/useWebSocket.jsx"; 3 - import CustomAlert from "../components/UI/CustomAlert.jsx"; 4 - import InitialState from "../components/Chat/InitialState.jsx"; 5 - import SearchingState from "../components/Chat/SearchingState.jsx"; 6 - import NoMatchState from "../components/Chat/NoMatchState.jsx"; 7 - import ChatView from "../components/Chat/ChatView.jsx"; 1 + import { useEffect, useState, useRef } from "react"; 8 2 9 3 export default function Chat() { 10 - const userId = localStorage.getItem("userId"); 11 - const { 12 - sentMessages, 13 - receiveMessages, 14 - isConnected, 15 - isSearching, 16 - matched, 17 - isAccepted, 18 - isReceived, 19 - receiver, 20 - terminated, 21 - showFriendRequest, 22 - connect, 23 - disconnect, 24 - reset, 25 - sendMessage, 26 - sendFriendRequest, 27 - respondToRequest, 28 - } = useWebSocket(userId); 4 + 5 + const [messages, setMessages] = useState([]); 6 + const [terminated, setTerminated] = useState(false); 7 + const [currMessage, setCurrMessage] = useState(""); 8 + const [isConnected, setIsConnected] = useState(false); 9 + const [isSearching, setIsSearching] = useState(false); 10 + const [matched, setMatched] = useState(false); 11 + const [receiver, setReceiver] = useState(localStorage.getItem("receiver")); 12 + const socketRef = useRef(null); 29 13 30 - if (terminated) { 31 - return <h1>Chat Terminated</h1>; 32 - } 14 + const handleChange = (e) => setCurrMessage(e.target.value); 33 15 34 - const renderContent = () => { 35 - if (!isConnected) { 36 - return <InitialState onConnect={connect} />; 16 + const handleKeyPress = (e) => { 17 + if (e.key === "Enter" && currMessage.trim() !== "" && matched) { 18 + const payload = { 19 + id: localStorage.getItem("userId"), 20 + message: currMessage 21 + }; 22 + socketRef.current.send(JSON.stringify(payload)); 23 + setCurrMessage(""); 37 24 } 25 + }; 38 26 39 - if (isSearching) { 40 - return <SearchingState onCancel={disconnect} />; 27 + const handleClick = () => { 28 + if (isConnected) return; 29 + 30 + const userId = localStorage.getItem("userId"); 31 + if (!userId) { 32 + alert("Please login first"); 33 + return; 41 34 } 42 35 43 - if (matched && receiver) { 44 - return ( 45 - <ChatView 46 - isReceived={isReceived} 47 - receiver={receiver} 48 - sentMessages={sentMessages} 49 - receiveMessages={receiveMessages} 50 - isAccepted={isAccepted} 51 - onSendMessage={sendMessage} 52 - onSendRequest={sendFriendRequest} 53 - /> 54 - ); 55 - } 36 + setIsSearching(true); 37 + socketRef.current = new WebSocket(`ws://localhost:3000/?userId=${userId}`); 38 + 39 + socketRef.current.onopen = () => { 40 + console.log("Connected to WebSocket"); 41 + setIsConnected(true); 42 + }; 43 + 44 + socketRef.current.onmessage = (e) => { 45 + try { 46 + const data = JSON.parse(e.data); 47 + 48 + if (data.type === "matched") { 49 + setIsSearching(false); 50 + setMatched(true); 51 + setReceiver(data.with); 52 + localStorage.setItem("receiver", data.with); 53 + console.log("Matched with:", data.with); 54 + } else if (data.type === "disconnected") { 55 + setMatched(false); 56 + setReceiver(null); 57 + localStorage.removeItem("receiver"); 58 + setIsSearching(true); 59 + console.log("Partner disconnected, searching for new match..."); 60 + } else if (data.type === "no_match") { 61 + setIsSearching(false); 62 + setMatched(false); 63 + setReceiver(null); 64 + localStorage.removeItem("receiver"); 65 + console.log("No match found"); 66 + } else { 67 + setMessages((prev) => [...prev, data]); 68 + } 69 + } catch (err) { 70 + console.error("Error parsing message:", err); 71 + setMessages((prev) => [...prev, e.data]); 72 + } 73 + }; 56 74 57 - if (isConnected && !isSearching) { 58 - return <NoMatchState onTryAgain={reset} />; 59 - } 75 + socketRef.current.onclose = () => { 76 + setTerminated(true); 77 + setIsConnected(false); 78 + setIsSearching(false); 79 + }; 60 80 61 - return null; 81 + socketRef.current.onerror = (err) => { 82 + console.error("WebSocket Error:", err); 83 + setIsSearching(false); 84 + }; 62 85 }; 63 86 87 + useEffect(() => { 88 + return () => { 89 + if (socketRef.current) { 90 + socketRef.current.close(); 91 + } 92 + }; 93 + }, []); 94 + 95 + if (terminated) return <h1>Chat Terminated</h1>; 96 + 64 97 return ( 65 98 <div> 66 - {renderContent()} 67 - {showFriendRequest && ( 68 - <CustomAlert 69 - message={`Friend Request From ${receiver}`} 70 - onAccept={() => respondToRequest(true)} 71 - onReject={() => respondToRequest(false)} 72 - /> 73 - )} 99 + {!isConnected ? ( 100 + <button onClick={handleClick}>I want a friend</button> 101 + ) : isSearching ? ( 102 + <div> 103 + <p>Searching for a friend...</p> 104 + <button onClick={() => { 105 + socketRef.current?.close(); 106 + setIsConnected(false); 107 + setIsSearching(false); 108 + }}>Cancel</button> 109 + </div> 110 + ) : matched && receiver ? ( 111 + <div> 112 + <h2>Chat with {receiver}</h2> 113 + <input 114 + type="text" 115 + value={currMessage} 116 + onChange={handleChange} 117 + onKeyDown={handleKeyPress} 118 + placeholder="Type a message..." 119 + /> 120 + <ul> 121 + {messages.map((msg, i) => ( 122 + <li key={i}> 123 + {typeof msg === 'string' ? msg : `${msg.id}: ${msg.message}`} 124 + </li> 125 + ))} 126 + </ul> 127 + </div> 128 + ) : isConnected && !isSearching ? ( 129 + <div> 130 + <p>No match found. Try again later.</p> 131 + <button onClick={() => { 132 + socketRef.current?.close(); 133 + setIsConnected(false); 134 + setIsSearching(false); 135 + setMatched(false); 136 + setReceiver(null); 137 + localStorage.removeItem("receiver"); 138 + }}>Try Again</button> 139 + </div> 140 + ) : null} 74 141 </div> 75 142 ); 76 143 }
-44
client/src/pages/Friends.jsx
··· 1 - import { useEffect, useState } from "react"; 2 - import { useNavigate } from "react-router-dom"; 3 - import { getFriends } from "../features/indexedDB/indexedDb.jsx"; 4 - 5 - function Friends() { 6 - const [friends, setFriends] = useState([]); 7 - 8 - useEffect(() => { 9 - const loadFriends = async () => { 10 - try { 11 - const friendsList = await getFriends(); 12 - setFriends(friendsList); 13 - } catch (err) { 14 - console.error("Error loading friends:", err); 15 - } 16 - }; 17 - 18 - loadFriends(); 19 - }, []); 20 - 21 - const navigate = useNavigate(); 22 - 23 - const handleClick = (friend) => { 24 - navigate(`/chat?friendId=${friend.id}`); 25 - }; 26 - 27 - return ( 28 - <ul> 29 - {friends.length > 0 ? ( 30 - friends.map(friend => ( 31 - <li key={friend}> 32 - <a href="#" onClick={() => handleClick(friend)}> 33 - {friend} 34 - </a> 35 - </li> 36 - )) 37 - ) : ( 38 - <li>No friends found.</li> 39 - )} 40 - </ul> 41 - ); 42 - } 43 - 44 - export default Friends;
-6
package-lock.json
··· 1 - { 2 - "name": "Chat", 3 - "lockfileVersion": 3, 4 - "requires": true, 5 - "packages": {} 6 - }
-1
package.json
··· 1 - {}
+115 -1
server/package-lock.json
··· 13 13 "bcryptjs": "^3.0.2", 14 14 "cors": "^2.8.5", 15 15 "express": "^5.1.0", 16 - "jsonwebtoken": "^9.0.2" 16 + "http": "^0.0.1-security", 17 + "jsonwebtoken": "^9.0.2", 18 + "redis": "^5.8.3", 19 + "ws": "^8.18.3" 17 20 }, 18 21 "devDependencies": { 19 22 "prisma": "^6.16.2" ··· 104 107 "@prisma/debug": "6.16.2" 105 108 } 106 109 }, 110 + "node_modules/@redis/bloom": { 111 + "version": "5.8.3", 112 + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.3.tgz", 113 + "integrity": "sha512-1eldTzHvdW3Oi0TReb8m1yiFt8ZwyF6rv1NpZyG5R4TpCwuAdKQetBKoCw7D96tNFgsVVd6eL+NaGZZCqhRg4g==", 114 + "license": "MIT", 115 + "engines": { 116 + "node": ">= 18" 117 + }, 118 + "peerDependencies": { 119 + "@redis/client": "^5.8.3" 120 + } 121 + }, 122 + "node_modules/@redis/client": { 123 + "version": "5.8.3", 124 + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.3.tgz", 125 + "integrity": "sha512-MZVUE+l7LmMIYlIjubPosruJ9ltSLGFmJqsXApTqPLyHLjsJUSAbAJb/A3N34fEqean4ddiDkdWzNu4ZKPvRUg==", 126 + "license": "MIT", 127 + "dependencies": { 128 + "cluster-key-slot": "1.1.2" 129 + }, 130 + "engines": { 131 + "node": ">= 18" 132 + } 133 + }, 134 + "node_modules/@redis/json": { 135 + "version": "5.8.3", 136 + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.3.tgz", 137 + "integrity": "sha512-DRR09fy/u8gynHGJ4gzXYeM7D8nlS6EMv5o+h20ndTJiAc7RGR01fdk2FNjnn1Nz5PjgGGownF+s72bYG4nZKQ==", 138 + "license": "MIT", 139 + "engines": { 140 + "node": ">= 18" 141 + }, 142 + "peerDependencies": { 143 + "@redis/client": "^5.8.3" 144 + } 145 + }, 146 + "node_modules/@redis/search": { 147 + "version": "5.8.3", 148 + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.3.tgz", 149 + "integrity": "sha512-EMIvEeGRR2I0BJEz4PV88DyCuPmMT1rDtznlsHY3cKSDcc9vj0Q411jUnX0iU2vVowUgWn/cpySKjpXdZ8m+5g==", 150 + "license": "MIT", 151 + "engines": { 152 + "node": ">= 18" 153 + }, 154 + "peerDependencies": { 155 + "@redis/client": "^5.8.3" 156 + } 157 + }, 158 + "node_modules/@redis/time-series": { 159 + "version": "5.8.3", 160 + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.3.tgz", 161 + "integrity": "sha512-5Jwy3ilsUYQjzpE7WZ1lEeG1RkqQ5kHtwV1p8yxXHSEmyUbC/T/AVgyjMcm52Olj/Ov/mhDKjx6ndYUi14bXsw==", 162 + "license": "MIT", 163 + "engines": { 164 + "node": ">= 18" 165 + }, 166 + "peerDependencies": { 167 + "@redis/client": "^5.8.3" 168 + } 169 + }, 107 170 "node_modules/@standard-schema/spec": { 108 171 "version": "1.0.0", 109 172 "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", ··· 252 315 "consola": "^3.2.3" 253 316 } 254 317 }, 318 + "node_modules/cluster-key-slot": { 319 + "version": "1.1.2", 320 + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", 321 + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", 322 + "license": "Apache-2.0", 323 + "engines": { 324 + "node": ">=0.10.0" 325 + } 326 + }, 255 327 "node_modules/confbox": { 256 328 "version": "0.2.2", 257 329 "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", ··· 694 766 "engines": { 695 767 "node": ">= 0.4" 696 768 } 769 + }, 770 + "node_modules/http": { 771 + "version": "0.0.1-security", 772 + "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", 773 + "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" 697 774 }, 698 775 "node_modules/http-errors": { 699 776 "version": "2.0.0", ··· 1171 1248 "url": "https://paulmillr.com/funding/" 1172 1249 } 1173 1250 }, 1251 + "node_modules/redis": { 1252 + "version": "5.8.3", 1253 + "resolved": "https://registry.npmjs.org/redis/-/redis-5.8.3.tgz", 1254 + "integrity": "sha512-MfSrfV6+tEfTw8c4W0yFp6XWX8Il4laGU7Bx4kvW4uiYM1AuZ3KGqEGt1LdQHeD1nEyLpIWetZ/SpY3kkbgrYw==", 1255 + "license": "MIT", 1256 + "dependencies": { 1257 + "@redis/bloom": "5.8.3", 1258 + "@redis/client": "5.8.3", 1259 + "@redis/json": "5.8.3", 1260 + "@redis/search": "5.8.3", 1261 + "@redis/time-series": "5.8.3" 1262 + }, 1263 + "engines": { 1264 + "node": ">= 18" 1265 + } 1266 + }, 1174 1267 "node_modules/router": { 1175 1268 "version": "2.2.0", 1176 1269 "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", ··· 1402 1495 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1403 1496 "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1404 1497 "license": "ISC" 1498 + }, 1499 + "node_modules/ws": { 1500 + "version": "8.18.3", 1501 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", 1502 + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", 1503 + "license": "MIT", 1504 + "engines": { 1505 + "node": ">=10.0.0" 1506 + }, 1507 + "peerDependencies": { 1508 + "bufferutil": "^4.0.1", 1509 + "utf-8-validate": ">=5.0.2" 1510 + }, 1511 + "peerDependenciesMeta": { 1512 + "bufferutil": { 1513 + "optional": true 1514 + }, 1515 + "utf-8-validate": { 1516 + "optional": true 1517 + } 1518 + } 1405 1519 } 1406 1520 } 1407 1521 }
+5 -2
server/package.json
··· 5 5 "main": "index.js", 6 6 "type": "module", 7 7 "scripts": { 8 - "dev" : "node src/server.js", 8 + "dev": "node src/server.js", 9 9 "test": "echo \"Error: no test specified\" && exit 1" 10 10 }, 11 11 "keywords": [], ··· 16 16 "bcryptjs": "^3.0.2", 17 17 "cors": "^2.8.5", 18 18 "express": "^5.1.0", 19 - "jsonwebtoken": "^9.0.2" 19 + "http": "^0.0.1-security", 20 + "jsonwebtoken": "^9.0.2", 21 + "redis": "^5.8.3", 22 + "ws": "^8.18.3" 20 23 }, 21 24 "devDependencies": { 22 25 "prisma": "^6.16.2"
+1 -1
server/src/app.js
··· 8 8 9 9 const app = express() 10 10 11 + 11 12 //CORS For Getting Requests From Frontend 12 13 app.use(cors()) 13 14 ··· 20 21 // Connect user routes โ†’ any request starting with /api/users 21 22 //app.use("/api/users", userRoutes) 22 23 app.use("/api/auth", authRoutes) 23 - //app.use("api/messages", chatRoutes) 24 24 25 25 26 26 // Fallback route
+1 -1
server/src/controllers/authController.js
··· 60 60 } 61 61 62 62 // Verifying Password 63 - const ok = await bcrypt.compare(password, user.password); 63 + const ok = await bcrypt.compare(password, user.password); 64 64 if (!ok) { 65 65 return res.status(401).json({ success: false, message: "invalid credentials" }); 66 66 }
server/src/controllers/messageController.js

This is a binary file and will not be displayed.

+3
server/src/controllers/userController.js
··· 1 + const findUser = () => { 2 + 3 + }
+3 -16
server/src/db/prisma.js
··· 1 1 2 - import { PrismaClient } from "./../generated/prisma/client.js"; 3 - const prisma = new PrismaClient(); 2 + import { PrismaClient } from "./../generated/prisma/client.js" 3 + const prisma = new PrismaClient() 4 4 5 - export const existingFriendship = async (userId, friendId) => { 6 - console.log("Exists") 7 - return await prisma.friendship.findFirst({ 8 - where: { 9 - OR: [ 10 - { userId: Number(userId), friendId: Number(friendId) }, 11 - { userId: Number(friendId), friendId: Number(userId) }, 12 - ], 13 - }, 14 - }); 15 - }; 16 - 17 - export default prisma; 18 - 5 + export default prisma
-29
server/src/helper/createFriendship.js
··· 1 - import { 2 - getMessages 3 - } from "./../redisClient.js"; 4 - import prisma from "../db/prisma.js" 5 - 6 - 7 - async function createFriendship(userId, friendId) { 8 - await prisma.$transaction(async (tx) => { 9 - await tx.friendship.createMany({ 10 - data: [ 11 - { userId, friendId, createdAt: new Date() }, 12 - { userId: friendId, friendId: userId, createdAt: new Date() }, 13 - ], 14 - }); 15 - 16 - console.log(`Friendship created between ${userId} and ${friendId}`); 17 - }); 18 - 19 - // Fetch stored messages in both directions 20 - const [sentMessages, receivedMessages] = await Promise.all([ 21 - getMessages(userId, friendId, "sent"), 22 - getMessages(userId, friendId, "received"), 23 - ]); 24 - 25 - return { sentMessages, receivedMessages }; 26 - } 27 - 28 - 29 - export default createFriendship
-27
server/src/helper/createFriendship.jsx
··· 1 - import { 2 - getMessages 3 - } from "./redisClient.js"; 4 - 5 - async function createFriendship(userId, friendId) { 6 - await prisma.$transaction(async (tx) => { 7 - await tx.friendship.createMany({ 8 - data: [ 9 - { userId, friendId, createdAt: new Date() }, 10 - { userId: friendId, friendId: userId, createdAt: new Date() }, 11 - ], 12 - }); 13 - 14 - console.log(`Friendship created between ${userId} and ${friendId}`); 15 - }); 16 - 17 - // Fetch stored messages in both directions 18 - const [sentMessages, receivedMessages] = await Promise.all([ 19 - getMessages(userId, friendId, "sent"), 20 - getMessages(userId, friendId, "received"), 21 - ]); 22 - 23 - return { sentMessages, receivedMessages }; 24 - } 25 - 26 - 27 - export default createFriendship
-35
server/src/prisma/migrations/20251019100015_/migration.sql
··· 1 - -- CreateTable 2 - CREATE TABLE "public"."Messages" ( 3 - "id" SERIAL NOT NULL, 4 - "senderId" INTEGER NOT NULL, 5 - "receiverId" INTEGER NOT NULL, 6 - "message" TEXT NOT NULL, 7 - "time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 - 9 - CONSTRAINT "Messages_pkey" PRIMARY KEY ("id") 10 - ); 11 - 12 - -- CreateTable 13 - CREATE TABLE "public"."Friendship" ( 14 - "id" SERIAL NOT NULL, 15 - "userId" INTEGER NOT NULL, 16 - "friendId" INTEGER NOT NULL, 17 - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 - 19 - CONSTRAINT "Friendship_pkey" PRIMARY KEY ("id") 20 - ); 21 - 22 - -- CreateIndex 23 - CREATE UNIQUE INDEX "Friendship_userId_friendId_key" ON "public"."Friendship"("userId", "friendId"); 24 - 25 - -- AddForeignKey 26 - ALTER TABLE "public"."Messages" ADD CONSTRAINT "Messages_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 27 - 28 - -- AddForeignKey 29 - ALTER TABLE "public"."Messages" ADD CONSTRAINT "Messages_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 30 - 31 - -- AddForeignKey 32 - ALTER TABLE "public"."Friendship" ADD CONSTRAINT "Friendship_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 33 - 34 - -- AddForeignKey 35 - ALTER TABLE "public"."Friendship" ADD CONSTRAINT "Friendship_friendId_fkey" FOREIGN KEY ("friendId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-14
server/src/prisma/migrations/20251020140702_init/migration.sql
··· 1 - /* 2 - Warnings: 3 - 4 - - You are about to drop the `Messages` table. If the table is not empty, all the data it contains will be lost. 5 - 6 - */ 7 - -- DropForeignKey 8 - ALTER TABLE "public"."Messages" DROP CONSTRAINT "Messages_receiverId_fkey"; 9 - 10 - -- DropForeignKey 11 - ALTER TABLE "public"."Messages" DROP CONSTRAINT "Messages_senderId_fkey"; 12 - 13 - -- DropTable 14 - DROP TABLE "public"."Messages";
-16
server/src/prisma/schema.prisma
··· 19 19 username String @unique 20 20 name String 21 21 password String 22 - 23 - friendships Friendship[] @relation("UserFriendships") // friendships this user initiated 24 - friends Friendship[] @relation("FriendUser") // friendships where this user is the friend 25 - } 26 - 27 - model Friendship { 28 - id Int @id @default(autoincrement()) 29 - userId Int 30 - friendId Int 31 - createdAt DateTime @default(now()) 32 - 33 - // Each friendship connects two users: 34 - user User @relation("UserFriendships", fields: [userId], references: [id]) 35 - friend User @relation("FriendUser", fields: [friendId], references: [id]) 36 - 37 - @@unique([userId, friendId]) 38 22 }
+1 -7
server/src/redisClient.js
··· 93 93 await redis.expire(field, 3600); // 1 hour expiry 94 94 }; 95 95 96 - 97 - export const getMessages = async (user, friend, type) => { 98 - const field = `${user}:${friend}:${type}`; 99 - const messages = await redis.lRange(field, 0, -1); // Get all messages 100 - return messages.map(msg => JSON.parse(msg)); // Return list of JSON objects 101 - }; 102 - 103 96 export const getTime = () => new Date().toISOString(); 104 97 98 +
+4
server/src/routes/messageRoutes.js
··· 1 + 2 + export const sendMessage = async (id, message) => { 3 + 4 + }
+6
server/src/routes/userRoutes.js
··· 1 + import express from "express" 1 2 3 + const router = express.Router() 4 + 5 + router.post('/finduser', findUser) 6 + 7 + export default router
+31
server/src/test.http
··· 1 + ### Variables 2 + 3 + @base = http://localhost:3000 4 + @name = Test User 5 + @password = StrongP@ssw0rd! 6 + @username = user_{{timestamp}} 7 + 8 + 9 + ### Signup 10 + 11 + POST {{base}}/api/auth/signup HTTP/1.1 12 + Content-Type: application/json 13 + Accept: application/json 14 + 15 + { 16 + "name": "{{name}}", 17 + "password": "{{password}}", 18 + "username": "{{username}}" 19 + } 20 + 21 + 22 + ### Login 23 + 24 + POST {{base}}/api/auth/login HTTP/1.1 25 + Content-Type: application/json 26 + Accept: application/json 27 + 28 + { 29 + "password": "{{password}}", 30 + "username": "{{username}}" 31 + }
+54 -97
server/src/webSocket.js
··· 1 1 2 - import WebSocket, { WebSocketServer } from "ws"; 3 - import { 4 - addUser, 5 - removeUser, 6 - getRandom, 7 - address, 8 - getTime, 9 - addMessage, 10 - } from "./redisClient.js"; 11 - import createFriendship from "./helper/createFriendship.js"; 12 - import { existingFriendship } from "./db/prisma.js"; 13 - 14 - const makeMessagePairs = (sent, received) => [ 15 - { sentMessages: sent, receivedMessages: received }, 16 - { sentMessages: received, receivedMessages: sent }, 17 - ]; 18 - 19 - const sendIfOpen = (socket, payload) => { 20 - if (socket && socket.readyState === WebSocket.OPEN) { 21 - socket.send(JSON.stringify(payload)); 22 - } 23 - }; 24 - 25 - const onAccept = async (userId, friendId) => { 26 - const { sentMessages, receivedMessages } = await createFriendship(userId, friendId); 27 - return makeMessagePairs(sentMessages, receivedMessages); 28 - }; 2 + import { WebSocketServer } from "ws"; 3 + import { redis, addUser, removeUser, getRandom, address, addMessage, getTime } from "./redisClient.js"; 29 4 30 5 const webSocketServer = (server) => { 31 6 const ws = new WebSocketServer({ server }); 32 7 33 8 ws.on("connection", async (socket, request) => { 34 - const params = new URLSearchParams(request.url.replace("/?", "")); 35 - const userId = params.get("userId"); 9 + try { 10 + const url = new URL(request.url, `http://${request.headers.host}`); 11 + const userId = url.searchParams.get("userId"); 36 12 37 - if (!userId) { 38 - socket.close(); 39 - return; 40 - } 13 + if (!userId) { 14 + socket.close(); 15 + return; 16 + } 41 17 42 - console.log(`Socket connected: ${userId}`); 18 + console.log(`Socket connected: ${userId}`); 19 + await addUser(userId, socket); 43 20 44 - const userSocket = socket; 45 - await addUser(userId, userSocket); 21 + const friendId = await getRandom(userId); 22 + if (friendId) { 23 + const friendSocket = address.get(friendId); 24 + if (friendSocket) { 25 + console.log(`Matched ${userId} with ${friendId}`); 26 + socket.send(JSON.stringify({ type: "matched", with: friendId })); 27 + friendSocket.send(JSON.stringify({ type: "matched", with: userId })); 28 + } else { 29 + console.log(`Friend ${friendId} socket not found, removing pairing`); 30 + await redis.hDel("friends", userId); 31 + await redis.hDel("friends", friendId); 32 + await redis.sAdd("activeUsers", userId); 33 + await redis.sAdd("activeUsers", friendId); 34 + } 35 + } else { 36 + console.log(`No match found for ${userId} within timeout`); 37 + socket.send(JSON.stringify({ type: "no_match" })); 38 + } 46 39 47 - const friendId = await getRandom(userId); 48 - const receiverSocket = friendId ? await address.get(friendId) : null; 40 + socket.on("message", async (message) => { 41 + let payload; 42 + try { 43 + payload = JSON.parse(message.toString()); 44 + } catch (err) { 45 + console.error("Invalid JSON:", err); 46 + return; 47 + } 49 48 50 - const isFriends = await existingFriendship(userId, friendId) 49 + payload.time = getTime(); 51 50 52 - if (receiverSocket && isFriends) { 53 - console.log("Already Friends") 54 - userSocket.send(JSON.stringify({ type: "already friends" })); 55 - receiverSocket.send(JSON.stringify({ type: "already friends" })); 56 - } 57 - if (receiverSocket) { 58 - userSocket.send(JSON.stringify({ type: "matched", with: String(friendId) })); 59 - receiverSocket.send(JSON.stringify({ type: "matched", with: String(userId) })); 60 - 61 - console.log("Friendship Established") 62 - 63 - } else { 64 - userSocket.send(JSON.stringify({ type: "no_match" })); 65 - } 66 - 67 - socket.on("message", async (payload) => { 68 - const data = JSON.parse(payload.toString()); 69 - data.time = getTime(); 70 - 71 - if (data.type === "response" && !isFriends) { 72 - 73 - // Request Accepted 74 - if (data.action === 'accept') { 75 - const messages = await onAccept(Number(userId), Number(friendId)); 51 + const friendId = await redis.hGet("friends", userId); 52 + const receiverSocket = address.get(friendId); 76 53 77 - sendIfOpen(userSocket, { 78 - id: friendId, 79 - type: "response", 80 - action: "friendship created", 81 - messages: messages[0], 82 - }); 83 - 84 - sendIfOpen(receiverSocket, { 85 - id: userId, 86 - type: "response", 87 - action: "friendship created", 88 - messages: messages[1], 89 - }); 90 - 91 - return 54 + if (receiverSocket) { 55 + receiverSocket.send(JSON.stringify(payload)); 92 56 } 93 57 94 - //Request Declined 95 - sendIfOpen(userSocket, { 96 - id: friendId, 97 - type: "response", 98 - action: "friend request declined", 99 - }) 100 - } else { 58 + await addMessage(payload, userId, "send"); 59 + await addMessage(payload, friendId, "receive"); 60 + }); 101 61 102 - } 103 - 104 - await addMessage(data, userId, "sent"); 105 - await addMessage(data, friendId, "receive"); 106 - 107 - const receiverSocket = address.get(friendId); 108 - if (receiverSocket) receiverSocket.send(JSON.stringify(data)); 109 - }); 62 + socket.on("close", async () => { 63 + console.log(`Connection closed: ${userId}`); 64 + await removeUser(userId); 65 + }); 110 66 111 - socket.on("close", async () => { 112 - console.log(`Connection closed: ${userId}`); 113 - await removeUser(userId); 114 - }); 67 + } catch (err) { 68 + console.error("Connection error:", err); 69 + socket.close(); 70 + } 115 71 }); 116 72 }; 117 73 118 74 export default webSocketServer; 75 +