rohanshaw commited on
Commit
3fb736f
·
verified ·
1 Parent(s): 3718533

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +7 -0
  2. package.json +28 -0
  3. server.js +260 -0
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM node:22-slim
2
+ WORKDIR /app
3
+ COPY package*.json ./
4
+ RUN npm ci --only=production
5
+ COPY . .
6
+ EXPOSE 7860
7
+ CMD ["node", "server.js"]
package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "vibelyy",
3
+ "version": "1.0.0",
4
+ "description": "random video chats with real peoples",
5
+ "keywords": [
6
+ "omegle",
7
+ "video",
8
+ "chat",
9
+ "vibe",
10
+ "random",
11
+ "people",
12
+ "dating"
13
+ ],
14
+ "license": "ISC",
15
+ "author": "Rohan Shaw",
16
+ "type": "commonjs",
17
+ "main": "server.js",
18
+ "scripts": {
19
+ "test": "echo \"Error: no test specified\" && exit 1",
20
+ "start": "node server.js"
21
+ },
22
+ "dependencies": {
23
+ "axios": "^1.11.0",
24
+ "express": "^5.1.0",
25
+ "mongodb": "^6.18.0",
26
+ "socket.io": "^4.8.1"
27
+ }
28
+ }
server.js ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const http = require('http');
3
+ const { Server } = require('socket.io');
4
+ const { MongoClient, ObjectId } = require('mongodb');
5
+ const axios = require('axios');
6
+
7
+ const PORT = process.env.PORT || 7860;
8
+ const MONGO_URI = process.env.MONGO_URI || '';
9
+ const MONGO_DBNAME = process.env.MONGO_DBNAME || 'vibelyy';
10
+
11
+ async function main() {
12
+ // MongoDB client
13
+ const mongoClient = new MongoClient(MONGO_URI, { useUnifiedTopology: true });
14
+ await mongoClient.connect();
15
+ const db = mongoClient.db(MONGO_DBNAME);
16
+ const blockedColl = db.collection('blocked'); // { blockerId, blockedId, at }
17
+ const sessionsColl = db.collection('sessions'); // { peerA, peerB, startTime, endTime, locationA, locationB }
18
+
19
+ const app = express();
20
+ const server = http.createServer(app);
21
+ const io = new Server(server, { cors: { origin: '*' } });
22
+
23
+ // In-memory waiting list of lightweight objects:
24
+ // { id, nickname, gender, blockedIds: [], location, socket }
25
+ let waitingUsers = [];
26
+
27
+ app.get('/health', (req, res) => res.send('ok'));
28
+
29
+ // Helper: fetch approx location for an IP (returns "City, Country")
30
+ async function getLocationForIp(ip) {
31
+ try {
32
+ // ipapi.co has no auth for basic lookup
33
+ const res = await axios.get(`https://ipapi.co/${ip}/json/`, { timeout: 4000 });
34
+ const city = res.data.city || null;
35
+ const country = res.data.country_name || res.data.country || null;
36
+ if (city && country) return `${city}, ${country}`;
37
+ if (country) return country;
38
+ return 'Unknown';
39
+ } catch (e) {
40
+ return 'Unknown';
41
+ }
42
+ }
43
+
44
+ // Find a match index in waitingUsers per preference: try opposite gender first
45
+ function findMatchIndex(newUser) {
46
+ // skip users that blocked newUser or are blocked by newUser
47
+ const blockedSet = new Set((newUser.blockedIds || []).map(String));
48
+ // 1) try opposite gender
49
+ let idx = waitingUsers.findIndex(u =>
50
+ u.id !== newUser.id &&
51
+ !blockedSet.has(String(u.id)) &&
52
+ !((u.blockedIds || []).map(String).includes(String(newUser.id))) &&
53
+ u.gender && newUser.gender && u.gender !== newUser.gender
54
+ );
55
+ if (idx !== -1) return idx;
56
+ // 2) fallback: any compatible user
57
+ idx = waitingUsers.findIndex(u =>
58
+ u.id !== newUser.id &&
59
+ !blockedSet.has(String(u.id)) &&
60
+ !((u.blockedIds || []).map(String).includes(String(newUser.id)))
61
+ );
62
+ return idx;
63
+ }
64
+
65
+ io.on('connection', (socket) => {
66
+ console.log('ws connect', socket.id);
67
+
68
+ // Data on this socket: socket.meta = { nickname, gender, blockedIds, location, peerId, sessionId }
69
+ socket.meta = {};
70
+
71
+ // Text chat messages
72
+ socket.on('message', ({ to, text, id } = {}) => {
73
+ if (!to || !text) return;
74
+ // Forward to peer (include id for receipts)
75
+ io.to(to).emit('message', { from: socket.id, text, id });
76
+ // Acknowledge delivery to sender
77
+ if (id) socket.emit('delivered', id);
78
+ });
79
+
80
+ socket.on('seen', ({ to, id } = {}) => {
81
+ if (!to || !id) return;
82
+ io.to(to).emit('seen', id);
83
+ });
84
+
85
+ socket.on('join', async (payload = {}) => {
86
+ // payload: { nickname, gender, blockedIds }
87
+ const nickname = (payload.nickname || 'Anon').slice(0, 64);
88
+ const gender = payload.gender || '';
89
+ const blockedIds = Array.isArray(payload.blockedIds) ? payload.blockedIds.map(String) : [];
90
+
91
+ // Attempt to get client IP (respect X-Forwarded-For if present)
92
+ // let ip = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address || '';
93
+ // if (typeof ip === 'string' && ip.includes(',')) ip = ip.split(',')[0].trim();
94
+
95
+ const ip = payload.ip || socket.handshake.address || '';
96
+
97
+ console.log('join', socket.id, nickname, gender, blockedIds, ip);
98
+
99
+ const location = await getLocationForIp(ip);
100
+
101
+ // set meta
102
+ socket.meta = { nickname, gender, blockedIds, location, peerId: null, sessionId: null };
103
+
104
+ // Build newUser object
105
+ const newUser = { id: socket.id, nickname, gender, blockedIds, location, socket };
106
+
107
+ // Try to find match
108
+ const idx = findMatchIndex(newUser);
109
+ if (idx === -1) {
110
+ // push to waiting
111
+ waitingUsers.push(newUser);
112
+ socket.emit('waiting');
113
+ console.log('waitingUsers add', socket.id);
114
+ return;
115
+ }
116
+
117
+ // Pop matched user
118
+ const peer = waitingUsers.splice(idx, 1)[0];
119
+
120
+ // mark each other's peerId
121
+ socket.meta.peerId = peer.id;
122
+ peer.socket.meta.peerId = socket.id;
123
+
124
+ // Create session doc in DB
125
+ const sessionDoc = {
126
+ peerA: socket.id,
127
+ peerB: peer.id,
128
+ nicknameA: socket.meta.nickname,
129
+ nicknameB: peer.nickname,
130
+ startTime: new Date(),
131
+ endTime: null,
132
+ locationA: socket.meta.location,
133
+ locationB: peer.location
134
+ };
135
+ const r = await sessionsColl.insertOne(sessionDoc);
136
+ const sessionId = r.insertedId.toString();
137
+ socket.meta.sessionId = sessionId;
138
+ peer.socket.meta.sessionId = sessionId;
139
+
140
+ // Inform both peers (include profile info and location)
141
+ socket.emit('paired', { id: peer.id, nickname: peer.nickname, location: peer.location, sessionId });
142
+ peer.socket.emit('paired', { id: socket.id, nickname: socket.meta.nickname, location: socket.meta.location, sessionId });
143
+
144
+ // Set up session auto-end timer (30 minutes)
145
+ const END_MS = 30 * 60 * 1000;
146
+ setTimeout(async () => {
147
+ try {
148
+ // Double-check that session still exists and both sockets still connected and sessionId matches
149
+ if (socket.meta.sessionId === sessionId) {
150
+ // Notify both
151
+ socket.emit('session_end', { reason: 'time_limit' });
152
+ }
153
+ if (peer.socket && peer.socket.meta.sessionId === sessionId) {
154
+ peer.socket.emit('session_end', { reason: 'time_limit' });
155
+ }
156
+ // Update DB endTime
157
+ await sessionsColl.updateOne({ _id: new ObjectId(sessionId) }, { $set: { endTime: new Date() } });
158
+ } catch (e) {
159
+ console.warn('session end error', e);
160
+ }
161
+ }, END_MS);
162
+
163
+ console.log('paired', socket.id, '↔', peer.id);
164
+ }); // join
165
+
166
+ // Signaling messages: forward to `to`
167
+ socket.on('signal', ({ to, data } = {}) => {
168
+ if (!to || !data) return;
169
+ io.to(to).emit('signal', { from: socket.id, data });
170
+ });
171
+
172
+ // Typing indicator
173
+ socket.on('typing', (to) => {
174
+ if (!to) return;
175
+ io.to(to).emit('typing');
176
+ });
177
+
178
+ // Report: store minimal info in sessions collection as a report array (optional)
179
+ socket.on('report', async ({ peerId, reason } = {}) => {
180
+ try {
181
+ const doc = { reporter: socket.id, peerId: peerId || null, reason: reason || null, time: new Date() };
182
+ // Append to a `reports` array in sessions if session exists; else insert into `reports` collection
183
+ if (socket.meta && socket.meta.sessionId) {
184
+ await sessionsColl.updateOne({ _id: new ObjectId(socket.meta.sessionId) }, { $push: { reports: doc } });
185
+ } else {
186
+ await db.collection('reports').insertOne(doc);
187
+ }
188
+ console.log('report saved', doc);
189
+ } catch (e) {
190
+ console.warn('report save failed', e);
191
+ }
192
+ });
193
+
194
+ // Block a peer: persist blocked relationship
195
+ socket.on('block', async (blockedPeerId) => {
196
+ if (!blockedPeerId) return;
197
+ try {
198
+ await blockedColl.insertOne({ blockerId: socket.id, blockedId: String(blockedPeerId), at: new Date() });
199
+ // Also update in-memory meta so matchmaking uses it if the client remains connected
200
+ socket.meta.blockedIds = Array.from(new Set([...(socket.meta.blockedIds || []), String(blockedPeerId)]));
201
+ console.log(`${socket.id} blocked ${blockedPeerId}`);
202
+ } catch (e) {
203
+ console.warn('block save failed', e);
204
+ }
205
+ });
206
+
207
+ // Leave current chat and try to rejoin (or just leave)
208
+ socket.on('leave', async () => {
209
+ try {
210
+ const peerId = socket.meta.peerId;
211
+ if (peerId) {
212
+ // notify peer
213
+ io.to(peerId).emit('peer_left');
214
+ // close session in DB
215
+ if (socket.meta.sessionId) {
216
+ await sessionsColl.updateOne({ _id: new ObjectId(socket.meta.sessionId) }, { $set: { endTime: new Date() } });
217
+ // clear session ids
218
+ const sessId = socket.meta.sessionId;
219
+ // clear session meta for both sides if connected
220
+ socket.meta.sessionId = null;
221
+ socket.meta.peerId = null;
222
+ const s = io.sockets.sockets.get(peerId);
223
+ if (s && s.meta) { s.meta.sessionId = null; s.meta.peerId = null; }
224
+ }
225
+ } else {
226
+ // If user was waiting, remove from waiting list
227
+ waitingUsers = waitingUsers.filter(u => u.id !== socket.id);
228
+ }
229
+ } catch (e) {
230
+ console.warn('leave error', e);
231
+ }
232
+ });
233
+
234
+ socket.on('disconnect', async (reason) => {
235
+ console.log('disconnect', socket.id, reason);
236
+ // If user was in waitingUsers remove them
237
+ waitingUsers = waitingUsers.filter(u => u.id !== socket.id);
238
+
239
+ // If in a session, notify peer and update DB endTime
240
+ if (socket.meta && socket.meta.peerId) {
241
+ io.to(socket.meta.peerId).emit('peer_left');
242
+ }
243
+ if (socket.meta && socket.meta.sessionId) {
244
+ try {
245
+ await sessionsColl.updateOne({ _id: new ObjectId(socket.meta.sessionId) }, { $set: { endTime: new Date() } });
246
+ } catch (e) {
247
+ console.warn('session end on disconnect failed', e);
248
+ }
249
+ }
250
+ });
251
+
252
+ }); // io.on(connection)
253
+
254
+ server.listen(PORT, () => console.log(`Vibelyy backend listening on ${PORT}`));
255
+ }
256
+
257
+ main().catch(err => {
258
+ console.error('Fatal server error', err);
259
+ process.exit(1);
260
+ });