Upload 3 files
Browse files- Dockerfile +7 -0
- package.json +28 -0
- 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 |
+
});
|