iosrvk0 commited on
Commit
4fe5060
·
verified ·
1 Parent(s): a880977

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +41 -0
  2. package.json +30 -0
  3. server.js +516 -0
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # HangOut Socket.io Service — Single File Edition
3
+ # Hugging Face Docker Space (mobile-deploy friendly)
4
+ # ============================================================
5
+ # Bas 3 files chahiye: server.js, package.json, Dockerfile
6
+ # ============================================================
7
+
8
+ FROM node:20-slim
9
+
10
+ # Install curl for healthchecks
11
+ RUN apt-get update && apt-get install -y --no-install-recommends \
12
+ curl \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ WORKDIR /app
16
+
17
+ # Copy package files first (better layer caching)
18
+ COPY package.json package-lock.json* ./
19
+
20
+ # Install production dependencies only
21
+ RUN npm install --only=production --no-audit --no-fund
22
+
23
+ # Copy the SINGLE server file
24
+ COPY server.js ./
25
+
26
+ # Hugging Face Space uses port 7860 by default
27
+ ENV PORT=7860
28
+ ENV HOST=0.0.0.0
29
+ ENV NODE_ENV=production
30
+
31
+ EXPOSE 7860
32
+
33
+ # Healthcheck
34
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
35
+ CMD curl -f http://localhost:7860/health || exit 1
36
+
37
+ # Non-root user for security
38
+ USER node
39
+
40
+ # Start the service
41
+ CMD ["node", "server.js"]
package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "hangout-socket-single",
3
+ "version": "1.0.0",
4
+ "description": "HangOut Socket.io Service — Single File Edition. Real-time chat, calls, presence, typing indicators. Designed for Hugging Face Docker Space (port 7860). Presented by RVK EDITION.",
5
+ "main": "server.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "start": "node server.js",
9
+ "dev": "node --watch server.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18.0.0"
13
+ },
14
+ "dependencies": {
15
+ "socket.io": "4.8.3",
16
+ "express": "4.21.2",
17
+ "cors": "2.8.5",
18
+ "dotenv": "16.4.7"
19
+ },
20
+ "keywords": [
21
+ "socket.io",
22
+ "realtime",
23
+ "express",
24
+ "huggingface",
25
+ "single-file",
26
+ "hangout"
27
+ ],
28
+ "author": "RVK EDITION",
29
+ "license": "MIT"
30
+ }
server.js ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // HangOut Socket.io Service — SINGLE FILE EDITION
3
+ // ============================================================
4
+ // Real-time chat, calls, presence, typing indicators,
5
+ // status updates, post/reel events, friend requests
6
+ // Designed for Hugging Face Docker Space (port 7860)
7
+ // Presented by RVK EDITION · v1.0.0
8
+ // ============================================================
9
+ // यह फाइल पूरा Socket.io service है — बस यह फाइल +
10
+ // package.json + Dockerfile HF Space पर push करो।
11
+ // ============================================================
12
+
13
+ require('dotenv').config()
14
+
15
+ const express = require('express')
16
+ const cors = require('cors')
17
+ const http = require('http')
18
+ const { Server } = require('socket.io')
19
+
20
+ const PORT = process.env.PORT || 7860
21
+ const HOST = process.env.HOST || '0.0.0.0'
22
+
23
+ // ============================================================
24
+ // EXPRESS APP (health checks + Socket.io attach)
25
+ // ============================================================
26
+
27
+ const app = express()
28
+
29
+ // CORS — APK WebView से WebSocket connections आएंगी
30
+ const allowedOrigins = (process.env.CORS_ORIGINS || '*')
31
+ .split(',')
32
+ .map((s) => s.trim())
33
+ .filter(Boolean)
34
+
35
+ app.use(
36
+ cors({
37
+ origin: allowedOrigins.includes('*') ? true : allowedOrigins,
38
+ methods: ['GET', 'POST', 'HEAD', 'OPTIONS'],
39
+ allowedHeaders: ['Content-Type', 'Authorization'],
40
+ credentials: true,
41
+ })
42
+ )
43
+
44
+ app.use(express.json())
45
+
46
+ // ─── Health endpoints (HF Space healthcheck के लिए) ───
47
+
48
+ app.get('/health', (req, res) => {
49
+ res.json({
50
+ status: 'ok',
51
+ service: 'hangout-socket-service',
52
+ version: '1.0.0',
53
+ timestamp: new Date().toISOString(),
54
+ uptime: process.uptime(),
55
+ connectedClients: io ? io.engine.clientsCount : 0,
56
+ })
57
+ })
58
+
59
+ app.get('/info', (req, res) => {
60
+ res.json({
61
+ service: 'hangout-socket-service',
62
+ version: '1.0.0',
63
+ description: 'Real-time chat, calls, presence, typing indicators',
64
+ socketPath: '/socket.io/',
65
+ transports: ['websocket', 'polling'],
66
+ events: {
67
+ clientToServer: [
68
+ 'auth:login',
69
+ 'chat:message',
70
+ 'chat:typing',
71
+ 'chat:read',
72
+ 'presence:check',
73
+ 'status:new',
74
+ 'status:view',
75
+ 'post:new',
76
+ 'post:like',
77
+ 'post:comment',
78
+ 'reel:like',
79
+ 'reel:comment',
80
+ 'friend:request',
81
+ 'friend:accept',
82
+ 'call:invite',
83
+ 'call:accept',
84
+ 'call:reject',
85
+ 'call:end',
86
+ 'call:ice-candidate',
87
+ 'call:missed',
88
+ ],
89
+ serverToClient: [
90
+ 'chat:message',
91
+ 'chat:ack',
92
+ 'chat:delivered',
93
+ 'chat:typing',
94
+ 'chat:read',
95
+ 'presence:update',
96
+ 'presence:status',
97
+ 'status:new',
98
+ 'status:view',
99
+ 'post:new',
100
+ 'post:like',
101
+ 'post:like:broadcast',
102
+ 'post:comment',
103
+ 'post:comment:broadcast',
104
+ 'reel:like',
105
+ 'reel:comment',
106
+ 'friend:request',
107
+ 'friend:accept',
108
+ 'call:incoming',
109
+ 'call:accepted',
110
+ 'call:rejected',
111
+ 'call:ended',
112
+ 'call:ice-candidate',
113
+ 'call:missed',
114
+ ],
115
+ },
116
+ })
117
+ })
118
+
119
+ app.get('/', (req, res) => {
120
+ res.json({
121
+ name: 'HangOut Socket.io Service',
122
+ version: '1.0.0',
123
+ description: 'Real-time backend for HangOut — chat, calls, presence, typing',
124
+ author: 'RVK EDITION',
125
+ endpoints: {
126
+ health: 'GET /health',
127
+ info: 'GET /info',
128
+ socket: 'WebSocket on /socket.io/',
129
+ },
130
+ connectedClients: io ? io.engine.clientsCount : 0,
131
+ })
132
+ })
133
+
134
+ // ============================================================
135
+ // HTTP SERVER + SOCKET.IO
136
+ // ============================================================
137
+
138
+ const httpServer = http.createServer(app)
139
+
140
+ const io = new Server(httpServer, {
141
+ path: '/socket.io/', // default Socket.io path
142
+ cors: {
143
+ origin: allowedOrigins.includes('*') ? true : allowedOrigins,
144
+ methods: ['GET', 'POST'],
145
+ credentials: true,
146
+ },
147
+ pingTimeout: 60000, // 60s — slow mobile connections के लिए
148
+ pingInterval: 25000, // 25s — keepalive ping
149
+ connectTimeout: 10000,
150
+ // Hugging Face के behind-reverse-proxy setup के लिए
151
+ allowEIO3: true,
152
+ })
153
+
154
+ // ============================================================
155
+ // IN-MEMORY STATE (no database — सब RAM में)
156
+ // ============================================================
157
+
158
+ // userId → { socketId, profileName, avatar }
159
+ const onlineUsers = new Map()
160
+
161
+ // userId → Set of socketIds (multi-device support)
162
+ const userSockets = new Map()
163
+
164
+ // ============================================================
165
+ // HELPER FUNCTIONS
166
+ // ============================================================
167
+
168
+ function emitToUser(userId, event, payload) {
169
+ const sockets = userSockets.get(userId)
170
+ if (!sockets) return
171
+ for (const sid of sockets) {
172
+ io.to(sid).emit(event, payload)
173
+ }
174
+ }
175
+
176
+ function broadcastPresence(userId, isOnline) {
177
+ io.emit('presence:update', {
178
+ userId,
179
+ isOnline,
180
+ lastSeen: new Date().toISOString(),
181
+ })
182
+ }
183
+
184
+ function removeSocketFromUser(userId, socketId) {
185
+ const sockets = userSockets.get(userId)
186
+ if (!sockets) return
187
+ sockets.delete(socketId)
188
+ if (sockets.size === 0) {
189
+ userSockets.delete(userId)
190
+ onlineUsers.delete(userId)
191
+ broadcastPresence(userId, false)
192
+ console.log(`[auth] User ${userId} offline (last socket disconnected)`)
193
+ }
194
+ }
195
+
196
+ function addSocketToUser(userId, socketId, profileName, avatar) {
197
+ if (!userSockets.has(userId)) userSockets.set(userId, new Set())
198
+ userSockets.get(userId).add(socketId)
199
+ onlineUsers.set(userId, { socketId, userId, profileName, avatar })
200
+ }
201
+
202
+ // ============================================================
203
+ // SOCKET.IO CONNECTION HANDLER
204
+ // ============================================================
205
+
206
+ io.on('connection', (socket) => {
207
+ console.log(`[socket] Connected: ${socket.id}`)
208
+
209
+ // ─── AUTH: User login (register presence) ───
210
+ socket.on('auth:login', (payload) => {
211
+ try {
212
+ const { userId, profileName, avatar } = payload || {}
213
+ if (!userId) {
214
+ console.warn('[auth:login] Missing userId')
215
+ return
216
+ }
217
+ socket.data.userId = userId
218
+ socket.data.profileName = profileName || 'Unknown'
219
+
220
+ // Add socket to user's socket set
221
+ addSocketToUser(userId, socket.id, profileName, avatar)
222
+
223
+ // Join personal room
224
+ socket.join(`user:${userId}`)
225
+
226
+ broadcastPresence(userId, true)
227
+ console.log(`[auth] ${profileName || userId} online — total online: ${onlineUsers.size}`)
228
+ } catch (err) {
229
+ console.error('[auth:login] error:', err.message)
230
+ }
231
+ })
232
+
233
+ // ─── CHAT: Send message ───
234
+ socket.on('chat:message', (payload) => {
235
+ try {
236
+ if (!payload || !payload.receiverId) return
237
+ // Deliver to receiver if online
238
+ emitToUser(payload.receiverId, 'chat:message', payload)
239
+ // Acknowledge sender
240
+ socket.emit('chat:ack', { messageId: payload.messageId, status: 'delivered' })
241
+ // Notify receiver's other devices
242
+ emitToUser(payload.receiverId, 'chat:delivered', {
243
+ messageId: payload.messageId,
244
+ conversationId: payload.conversationId,
245
+ })
246
+ } catch (err) {
247
+ console.error('[chat:message] error:', err.message)
248
+ }
249
+ })
250
+
251
+ // ─── CHAT: Typing indicator ───
252
+ socket.on('chat:typing', (payload) => {
253
+ try {
254
+ if (!payload || !payload.receiverId) return
255
+ emitToUser(payload.receiverId, 'chat:typing', payload)
256
+ } catch (err) {
257
+ console.error('[chat:typing] error:', err.message)
258
+ }
259
+ })
260
+
261
+ // ─── CHAT: Read receipts ───
262
+ socket.on('chat:read', (payload) => {
263
+ try {
264
+ if (!payload || !payload.senderId) return
265
+ emitToUser(payload.senderId, 'chat:read', payload)
266
+ } catch (err) {
267
+ console.error('[chat:read] error:', err.message)
268
+ }
269
+ })
270
+
271
+ // ─── PRESENCE: Check online status of users ───
272
+ socket.on('presence:check', (payload) => {
273
+ try {
274
+ const { userIds } = payload || {}
275
+ if (!Array.isArray(userIds)) return
276
+ const result = {}
277
+ for (const uid of userIds) {
278
+ result[uid] = onlineUsers.has(uid)
279
+ }
280
+ socket.emit('presence:status', result)
281
+ } catch (err) {
282
+ console.error('[presence:check] error:', err.message)
283
+ }
284
+ })
285
+
286
+ // ─── STATUS: New status posted ───
287
+ socket.on('status:new', (payload) => {
288
+ try {
289
+ // Broadcast to all online users (friends will filter on client)
290
+ socket.broadcast.emit('status:new', payload)
291
+ } catch (err) {
292
+ console.error('[status:new] error:', err.message)
293
+ }
294
+ })
295
+
296
+ // ─── STATUS: View ───
297
+ socket.on('status:view', (payload) => {
298
+ try {
299
+ if (!payload || !payload.ownerId) return
300
+ emitToUser(payload.ownerId, 'status:view', payload)
301
+ } catch (err) {
302
+ console.error('[status:view] error:', err.message)
303
+ }
304
+ })
305
+
306
+ // ─── POSTS: New post ───
307
+ socket.on('post:new', (payload) => {
308
+ try {
309
+ socket.broadcast.emit('post:new', payload)
310
+ } catch (err) {
311
+ console.error('[post:new] error:', err.message)
312
+ }
313
+ })
314
+
315
+ // ─── POSTS: Like ───
316
+ socket.on('post:like', (payload) => {
317
+ try {
318
+ if (!payload || !payload.ownerId) return
319
+ emitToUser(payload.ownerId, 'post:like', payload)
320
+ socket.broadcast.emit('post:like:broadcast', payload)
321
+ } catch (err) {
322
+ console.error('[post:like] error:', err.message)
323
+ }
324
+ })
325
+
326
+ // ─── POSTS: Comment ───
327
+ socket.on('post:comment', (payload) => {
328
+ try {
329
+ if (!payload || !payload.ownerId) return
330
+ emitToUser(payload.ownerId, 'post:comment', payload)
331
+ socket.broadcast.emit('post:comment:broadcast', payload)
332
+ } catch (err) {
333
+ console.error('[post:comment] error:', err.message)
334
+ }
335
+ })
336
+
337
+ // ─── REELS: Like ───
338
+ socket.on('reel:like', (payload) => {
339
+ try {
340
+ if (!payload || !payload.ownerId) return
341
+ emitToUser(payload.ownerId, 'reel:like', payload)
342
+ } catch (err) {
343
+ console.error('[reel:like] error:', err.message)
344
+ }
345
+ })
346
+
347
+ // ─── REELS: Comment ───
348
+ socket.on('reel:comment', (payload) => {
349
+ try {
350
+ if (!payload || !payload.ownerId) return
351
+ emitToUser(payload.ownerId, 'reel:comment', payload)
352
+ } catch (err) {
353
+ console.error('[reel:comment] error:', err.message)
354
+ }
355
+ })
356
+
357
+ // ─── FRIENDS: Request ───
358
+ socket.on('friend:request', (payload) => {
359
+ try {
360
+ if (!payload || !payload.receiverId) return
361
+ emitToUser(payload.receiverId, 'friend:request', payload)
362
+ } catch (err) {
363
+ console.error('[friend:request] error:', err.message)
364
+ }
365
+ })
366
+
367
+ // ─── FRIENDS: Accept ───
368
+ socket.on('friend:accept', (payload) => {
369
+ try {
370
+ if (!payload || !payload.requesterId) return
371
+ emitToUser(payload.requesterId, 'friend:accept', payload)
372
+ } catch (err) {
373
+ console.error('[friend:accept] error:', err.message)
374
+ }
375
+ })
376
+
377
+ // ─── CALLS: Invite (caller → receiver) ───
378
+ socket.on('call:invite', (payload) => {
379
+ try {
380
+ if (!payload || !payload.receiverId) return
381
+ emitToUser(payload.receiverId, 'call:incoming', payload)
382
+ } catch (err) {
383
+ console.error('[call:invite] error:', err.message)
384
+ }
385
+ })
386
+
387
+ // ─── CALLS: Accept (receiver → caller) ───
388
+ socket.on('call:accept', (payload) => {
389
+ try {
390
+ if (!payload || !payload.callerId) return
391
+ emitToUser(payload.callerId, 'call:accepted', payload)
392
+ } catch (err) {
393
+ console.error('[call:accept] error:', err.message)
394
+ }
395
+ })
396
+
397
+ // ─── CALLS: Reject (receiver → caller) ───
398
+ socket.on('call:reject', (payload) => {
399
+ try {
400
+ if (!payload || !payload.callerId) return
401
+ emitToUser(payload.callerId, 'call:rejected', payload)
402
+ } catch (err) {
403
+ console.error('[call:reject] error:', err.message)
404
+ }
405
+ })
406
+
407
+ // ─── CALLS: End (either party → other) ───
408
+ socket.on('call:end', (payload) => {
409
+ try {
410
+ if (!payload || (!payload.callerId && !payload.receiverId)) return
411
+ const otherUserId = socket.data.userId === payload.callerId
412
+ ? payload.receiverId
413
+ : payload.callerId
414
+ if (otherUserId) {
415
+ emitToUser(otherUserId, 'call:ended', payload)
416
+ }
417
+ } catch (err) {
418
+ console.error('[call:end] error:', err.message)
419
+ }
420
+ })
421
+
422
+ // ─── CALLS: ICE Candidate (WebRTC signaling) ───
423
+ socket.on('call:ice-candidate', (payload) => {
424
+ try {
425
+ if (!payload || !payload.targetId) return
426
+ emitToUser(payload.targetId, 'call:ice-candidate', payload)
427
+ } catch (err) {
428
+ console.error('[call:ice-candidate] error:', err.message)
429
+ }
430
+ })
431
+
432
+ // ─── CALLS: Missed (caller → caller, when timeout) ───
433
+ socket.on('call:missed', (payload) => {
434
+ try {
435
+ if (!payload || !payload.callerId) return
436
+ emitToUser(payload.callerId, 'call:missed', payload)
437
+ } catch (err) {
438
+ console.error('[call:missed] error:', err.message)
439
+ }
440
+ })
441
+
442
+ // ─── DISCONNECT ───
443
+ socket.on('disconnect', (reason) => {
444
+ try {
445
+ const userId = socket.data.userId
446
+ if (userId) {
447
+ removeSocketFromUser(userId, socket.id)
448
+ }
449
+ console.log(`[socket] Disconnected: ${socket.id} (reason: ${reason})`)
450
+ } catch (err) {
451
+ console.error('[disconnect] error:', err.message)
452
+ }
453
+ })
454
+
455
+ // ─── ERROR ───
456
+ socket.on('error', (err) => {
457
+ console.error(`[socket] Error on ${socket.id}:`, err?.message || err)
458
+ })
459
+ })
460
+
461
+ // ============================================================
462
+ // START SERVER + GRACEFUL SHUTDOWN
463
+ // ============================================================
464
+
465
+ async function start() {
466
+ console.log('==========================================')
467
+ console.log(' HangOut Socket.io Service v1.0.0')
468
+ console.log(' Single File Edition')
469
+ console.log(' Presented by RVK EDITION')
470
+ console.log('==========================================')
471
+ console.log(` Port: ${PORT}`)
472
+ console.log(` Host: ${HOST}`)
473
+ console.log(` CORS_ORIGINS: ${process.env.CORS_ORIGINS || '*'}`)
474
+ console.log(` Socket path: /socket.io/`)
475
+ console.log('==========================================')
476
+
477
+ httpServer.listen(PORT, HOST, () => {
478
+ console.log(`[server] ✓ Listening on http://${HOST}:${PORT}`)
479
+ console.log('[server] Endpoints:')
480
+ console.log(' GET /health — basic liveness probe')
481
+ console.log(' GET /info — service info + event list')
482
+ console.log(' GET / — root info')
483
+ console.log(' WS /socket.io/ — WebSocket connection')
484
+ console.log('')
485
+ console.log('[server] Waiting for client connections...')
486
+ })
487
+ }
488
+
489
+ async function shutdown(signal) {
490
+ console.log(`[server] ${signal} received, shutting down...`)
491
+ try {
492
+ // Disconnect all clients gracefully
493
+ if (io) {
494
+ io.disconnectSockets(true)
495
+ }
496
+ httpServer.close(() => {
497
+ console.log('[server] HTTP server closed')
498
+ process.exit(0)
499
+ })
500
+ // Force exit after 5s if close hangs
501
+ setTimeout(() => process.exit(0), 5000)
502
+ } catch (e) {
503
+ console.error('[server] Shutdown error:', e.message)
504
+ process.exit(1)
505
+ }
506
+ }
507
+
508
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
509
+ process.on('SIGINT', () => shutdown('SIGINT'))
510
+ process.on('unhandledRejection', (err) => console.error('[server] Unhandled rejection:', err))
511
+ process.on('uncaughtException', (err) => {
512
+ console.error('[server] Uncaught exception:', err)
513
+ shutdown('uncaughtException')
514
+ })
515
+
516
+ start()