-53
README.md
-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
+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
+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
+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
-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
-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
-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
-9
client/src/components/Chat/NoMatchState.jsx
-9
client/src/components/Chat/SearchingState.jsx
-9
client/src/components/Chat/SearchingState.jsx
-44
client/src/components/UI/CustomAlert.jsx
-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
-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
-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
-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
+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
-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
-6
package-lock.json
-1
package.json
-1
package.json
···
1
-
{}
+115
-1
server/package-lock.json
+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
-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
+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
+1
-1
server/src/controllers/authController.js
server/src/controllers/messageController.js
server/src/controllers/messageController.js
This is a binary file and will not be displayed.
+3
-16
server/src/db/prisma.js
+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
-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
-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
-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
-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
-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
+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
+4
server/src/routes/messageRoutes.js
+6
server/src/routes/userRoutes.js
+6
server/src/routes/userRoutes.js
+31
server/src/test.http
+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
+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
+