Spaces:
Runtime error
Runtime error
| // ============================================================ | |
| // HangOut Socket.io Service — SINGLE FILE EDITION | |
| // ============================================================ | |
| // Real-time chat, calls, presence, typing indicators, | |
| // status updates, post/reel events, friend requests | |
| // Designed for Hugging Face Docker Space (port 7860) | |
| // Presented by RVK EDITION · v1.0.0 | |
| // ============================================================ | |
| // यह फाइल पूरा Socket.io service है — बस यह फाइल + | |
| // package.json + Dockerfile HF Space पर push करो। | |
| // ============================================================ | |
| require('dotenv').config() | |
| const express = require('express') | |
| const cors = require('cors') | |
| const http = require('http') | |
| const { Server } = require('socket.io') | |
| const PORT = process.env.PORT || 7860 | |
| const HOST = process.env.HOST || '0.0.0.0' | |
| // ============================================================ | |
| // EXPRESS APP (health checks + Socket.io attach) | |
| // ============================================================ | |
| const app = express() | |
| // CORS — APK WebView से WebSocket connections आएंगी | |
| const allowedOrigins = (process.env.CORS_ORIGINS || '*') | |
| .split(',') | |
| .map((s) => s.trim()) | |
| .filter(Boolean) | |
| app.use( | |
| cors({ | |
| origin: allowedOrigins.includes('*') ? true : allowedOrigins, | |
| methods: ['GET', 'POST', 'HEAD', 'OPTIONS'], | |
| allowedHeaders: ['Content-Type', 'Authorization'], | |
| credentials: true, | |
| }) | |
| ) | |
| app.use(express.json()) | |
| // ─── Health endpoints (HF Space healthcheck के लिए) ─── | |
| app.get('/health', (req, res) => { | |
| res.json({ | |
| status: 'ok', | |
| service: 'hangout-socket-service', | |
| version: '1.0.0', | |
| timestamp: new Date().toISOString(), | |
| uptime: process.uptime(), | |
| connectedClients: io ? io.engine.clientsCount : 0, | |
| }) | |
| }) | |
| app.get('/info', (req, res) => { | |
| res.json({ | |
| service: 'hangout-socket-service', | |
| version: '1.0.0', | |
| description: 'Real-time chat, calls, presence, typing indicators', | |
| socketPath: '/socket.io/', | |
| transports: ['websocket', 'polling'], | |
| events: { | |
| clientToServer: [ | |
| 'auth:login', | |
| 'chat:message', | |
| 'chat:typing', | |
| 'chat:read', | |
| 'presence:check', | |
| 'status:new', | |
| 'status:view', | |
| 'post:new', | |
| 'post:like', | |
| 'post:comment', | |
| 'reel:like', | |
| 'reel:comment', | |
| 'friend:request', | |
| 'friend:accept', | |
| 'call:invite', | |
| 'call:accept', | |
| 'call:reject', | |
| 'call:end', | |
| 'call:ice-candidate', | |
| 'call:missed', | |
| ], | |
| serverToClient: [ | |
| 'chat:message', | |
| 'chat:ack', | |
| 'chat:delivered', | |
| 'chat:typing', | |
| 'chat:read', | |
| 'presence:update', | |
| 'presence:status', | |
| 'status:new', | |
| 'status:view', | |
| 'post:new', | |
| 'post:like', | |
| 'post:like:broadcast', | |
| 'post:comment', | |
| 'post:comment:broadcast', | |
| 'reel:like', | |
| 'reel:comment', | |
| 'friend:request', | |
| 'friend:accept', | |
| 'call:incoming', | |
| 'call:accepted', | |
| 'call:rejected', | |
| 'call:ended', | |
| 'call:ice-candidate', | |
| 'call:missed', | |
| ], | |
| }, | |
| }) | |
| }) | |
| app.get('/', (req, res) => { | |
| res.json({ | |
| name: 'HangOut Socket.io Service', | |
| version: '1.0.0', | |
| description: 'Real-time backend for HangOut — chat, calls, presence, typing', | |
| author: 'RVK EDITION', | |
| endpoints: { | |
| health: 'GET /health', | |
| info: 'GET /info', | |
| socket: 'WebSocket on /socket.io/', | |
| }, | |
| connectedClients: io ? io.engine.clientsCount : 0, | |
| }) | |
| }) | |
| // ============================================================ | |
| // HTTP SERVER + SOCKET.IO | |
| // ============================================================ | |
| const httpServer = http.createServer(app) | |
| const io = new Server(httpServer, { | |
| path: '/socket.io/', // default Socket.io path | |
| cors: { | |
| origin: allowedOrigins.includes('*') ? true : allowedOrigins, | |
| methods: ['GET', 'POST'], | |
| credentials: true, | |
| }, | |
| pingTimeout: 60000, // 60s — slow mobile connections के लिए | |
| pingInterval: 25000, // 25s — keepalive ping | |
| connectTimeout: 10000, | |
| // Hugging Face के behind-reverse-proxy setup के लिए | |
| allowEIO3: true, | |
| }) | |
| // ============================================================ | |
| // IN-MEMORY STATE (no database — सब RAM में) | |
| // ============================================================ | |
| // userId → { socketId, profileName, avatar } | |
| const onlineUsers = new Map() | |
| // userId → Set of socketIds (multi-device support) | |
| const userSockets = new Map() | |
| // ============================================================ | |
| // HELPER FUNCTIONS | |
| // ============================================================ | |
| function emitToUser(userId, event, payload) { | |
| const sockets = userSockets.get(userId) | |
| if (!sockets) return | |
| for (const sid of sockets) { | |
| io.to(sid).emit(event, payload) | |
| } | |
| } | |
| function broadcastPresence(userId, isOnline) { | |
| io.emit('presence:update', { | |
| userId, | |
| isOnline, | |
| lastSeen: new Date().toISOString(), | |
| }) | |
| } | |
| function removeSocketFromUser(userId, socketId) { | |
| const sockets = userSockets.get(userId) | |
| if (!sockets) return | |
| sockets.delete(socketId) | |
| if (sockets.size === 0) { | |
| userSockets.delete(userId) | |
| onlineUsers.delete(userId) | |
| broadcastPresence(userId, false) | |
| console.log(`[auth] User ${userId} offline (last socket disconnected)`) | |
| } | |
| } | |
| function addSocketToUser(userId, socketId, profileName, avatar) { | |
| if (!userSockets.has(userId)) userSockets.set(userId, new Set()) | |
| userSockets.get(userId).add(socketId) | |
| onlineUsers.set(userId, { socketId, userId, profileName, avatar }) | |
| } | |
| // ============================================================ | |
| // SOCKET.IO CONNECTION HANDLER | |
| // ============================================================ | |
| io.on('connection', (socket) => { | |
| console.log(`[socket] Connected: ${socket.id}`) | |
| // ─── AUTH: User login (register presence) ─── | |
| socket.on('auth:login', (payload) => { | |
| try { | |
| const { userId, profileName, avatar } = payload || {} | |
| if (!userId) { | |
| console.warn('[auth:login] Missing userId') | |
| return | |
| } | |
| socket.data.userId = userId | |
| socket.data.profileName = profileName || 'Unknown' | |
| // Add socket to user's socket set | |
| addSocketToUser(userId, socket.id, profileName, avatar) | |
| // Join personal room | |
| socket.join(`user:${userId}`) | |
| broadcastPresence(userId, true) | |
| console.log(`[auth] ${profileName || userId} online — total online: ${onlineUsers.size}`) | |
| } catch (err) { | |
| console.error('[auth:login] error:', err.message) | |
| } | |
| }) | |
| // ─── CHAT: Send message ─── | |
| socket.on('chat:message', (payload) => { | |
| try { | |
| if (!payload || !payload.receiverId) return | |
| // Deliver to receiver if online | |
| emitToUser(payload.receiverId, 'chat:message', payload) | |
| // Acknowledge sender | |
| socket.emit('chat:ack', { messageId: payload.messageId, status: 'delivered' }) | |
| // Notify receiver's other devices | |
| emitToUser(payload.receiverId, 'chat:delivered', { | |
| messageId: payload.messageId, | |
| conversationId: payload.conversationId, | |
| }) | |
| } catch (err) { | |
| console.error('[chat:message] error:', err.message) | |
| } | |
| }) | |
| // ─── CHAT: Typing indicator ─── | |
| socket.on('chat:typing', (payload) => { | |
| try { | |
| if (!payload || !payload.receiverId) return | |
| emitToUser(payload.receiverId, 'chat:typing', payload) | |
| } catch (err) { | |
| console.error('[chat:typing] error:', err.message) | |
| } | |
| }) | |
| // ─── CHAT: Read receipts ─── | |
| socket.on('chat:read', (payload) => { | |
| try { | |
| if (!payload || !payload.senderId) return | |
| emitToUser(payload.senderId, 'chat:read', payload) | |
| } catch (err) { | |
| console.error('[chat:read] error:', err.message) | |
| } | |
| }) | |
| // ─── PRESENCE: Check online status of users ─── | |
| socket.on('presence:check', (payload) => { | |
| try { | |
| const { userIds } = payload || {} | |
| if (!Array.isArray(userIds)) return | |
| const result = {} | |
| for (const uid of userIds) { | |
| result[uid] = onlineUsers.has(uid) | |
| } | |
| socket.emit('presence:status', result) | |
| } catch (err) { | |
| console.error('[presence:check] error:', err.message) | |
| } | |
| }) | |
| // ─── STATUS: New status posted ─── | |
| socket.on('status:new', (payload) => { | |
| try { | |
| // Broadcast to all online users (friends will filter on client) | |
| socket.broadcast.emit('status:new', payload) | |
| } catch (err) { | |
| console.error('[status:new] error:', err.message) | |
| } | |
| }) | |
| // ─── STATUS: View ─── | |
| socket.on('status:view', (payload) => { | |
| try { | |
| if (!payload || !payload.ownerId) return | |
| emitToUser(payload.ownerId, 'status:view', payload) | |
| } catch (err) { | |
| console.error('[status:view] error:', err.message) | |
| } | |
| }) | |
| // ─── POSTS: New post ─── | |
| socket.on('post:new', (payload) => { | |
| try { | |
| socket.broadcast.emit('post:new', payload) | |
| } catch (err) { | |
| console.error('[post:new] error:', err.message) | |
| } | |
| }) | |
| // ─── POSTS: Like ─── | |
| socket.on('post:like', (payload) => { | |
| try { | |
| if (!payload || !payload.ownerId) return | |
| emitToUser(payload.ownerId, 'post:like', payload) | |
| socket.broadcast.emit('post:like:broadcast', payload) | |
| } catch (err) { | |
| console.error('[post:like] error:', err.message) | |
| } | |
| }) | |
| // ─── POSTS: Comment ─── | |
| socket.on('post:comment', (payload) => { | |
| try { | |
| if (!payload || !payload.ownerId) return | |
| emitToUser(payload.ownerId, 'post:comment', payload) | |
| socket.broadcast.emit('post:comment:broadcast', payload) | |
| } catch (err) { | |
| console.error('[post:comment] error:', err.message) | |
| } | |
| }) | |
| // ─── REELS: Like ─── | |
| socket.on('reel:like', (payload) => { | |
| try { | |
| if (!payload || !payload.ownerId) return | |
| emitToUser(payload.ownerId, 'reel:like', payload) | |
| } catch (err) { | |
| console.error('[reel:like] error:', err.message) | |
| } | |
| }) | |
| // ─── REELS: Comment ─── | |
| socket.on('reel:comment', (payload) => { | |
| try { | |
| if (!payload || !payload.ownerId) return | |
| emitToUser(payload.ownerId, 'reel:comment', payload) | |
| } catch (err) { | |
| console.error('[reel:comment] error:', err.message) | |
| } | |
| }) | |
| // ─── FRIENDS: Request ─── | |
| socket.on('friend:request', (payload) => { | |
| try { | |
| if (!payload || !payload.receiverId) return | |
| emitToUser(payload.receiverId, 'friend:request', payload) | |
| } catch (err) { | |
| console.error('[friend:request] error:', err.message) | |
| } | |
| }) | |
| // ─── FRIENDS: Accept ─── | |
| socket.on('friend:accept', (payload) => { | |
| try { | |
| if (!payload || !payload.requesterId) return | |
| emitToUser(payload.requesterId, 'friend:accept', payload) | |
| } catch (err) { | |
| console.error('[friend:accept] error:', err.message) | |
| } | |
| }) | |
| // ─── CALLS: Invite (caller → receiver) ─── | |
| socket.on('call:invite', (payload) => { | |
| try { | |
| if (!payload || !payload.receiverId) return | |
| emitToUser(payload.receiverId, 'call:incoming', payload) | |
| } catch (err) { | |
| console.error('[call:invite] error:', err.message) | |
| } | |
| }) | |
| // ─── CALLS: Accept (receiver → caller) ─── | |
| socket.on('call:accept', (payload) => { | |
| try { | |
| if (!payload || !payload.callerId) return | |
| emitToUser(payload.callerId, 'call:accepted', payload) | |
| } catch (err) { | |
| console.error('[call:accept] error:', err.message) | |
| } | |
| }) | |
| // ─── CALLS: Reject (receiver → caller) ─── | |
| socket.on('call:reject', (payload) => { | |
| try { | |
| if (!payload || !payload.callerId) return | |
| emitToUser(payload.callerId, 'call:rejected', payload) | |
| } catch (err) { | |
| console.error('[call:reject] error:', err.message) | |
| } | |
| }) | |
| // ─── CALLS: End (either party → other) ─── | |
| socket.on('call:end', (payload) => { | |
| try { | |
| if (!payload || (!payload.callerId && !payload.receiverId)) return | |
| const otherUserId = socket.data.userId === payload.callerId | |
| ? payload.receiverId | |
| : payload.callerId | |
| if (otherUserId) { | |
| emitToUser(otherUserId, 'call:ended', payload) | |
| } | |
| } catch (err) { | |
| console.error('[call:end] error:', err.message) | |
| } | |
| }) | |
| // ─── CALLS: ICE Candidate (WebRTC signaling) ─── | |
| socket.on('call:ice-candidate', (payload) => { | |
| try { | |
| if (!payload || !payload.targetId) return | |
| emitToUser(payload.targetId, 'call:ice-candidate', payload) | |
| } catch (err) { | |
| console.error('[call:ice-candidate] error:', err.message) | |
| } | |
| }) | |
| // ─── CALLS: Missed (caller → caller, when timeout) ─── | |
| socket.on('call:missed', (payload) => { | |
| try { | |
| if (!payload || !payload.callerId) return | |
| emitToUser(payload.callerId, 'call:missed', payload) | |
| } catch (err) { | |
| console.error('[call:missed] error:', err.message) | |
| } | |
| }) | |
| // ─── DISCONNECT ─── | |
| socket.on('disconnect', (reason) => { | |
| try { | |
| const userId = socket.data.userId | |
| if (userId) { | |
| removeSocketFromUser(userId, socket.id) | |
| } | |
| console.log(`[socket] Disconnected: ${socket.id} (reason: ${reason})`) | |
| } catch (err) { | |
| console.error('[disconnect] error:', err.message) | |
| } | |
| }) | |
| // ─── ERROR ─── | |
| socket.on('error', (err) => { | |
| console.error(`[socket] Error on ${socket.id}:`, err?.message || err) | |
| }) | |
| }) | |
| // ============================================================ | |
| // START SERVER + GRACEFUL SHUTDOWN | |
| // ============================================================ | |
| async function start() { | |
| console.log('==========================================') | |
| console.log(' HangOut Socket.io Service v1.0.0') | |
| console.log(' Single File Edition') | |
| console.log(' Presented by RVK EDITION') | |
| console.log('==========================================') | |
| console.log(` Port: ${PORT}`) | |
| console.log(` Host: ${HOST}`) | |
| console.log(` CORS_ORIGINS: ${process.env.CORS_ORIGINS || '*'}`) | |
| console.log(` Socket path: /socket.io/`) | |
| console.log('==========================================') | |
| httpServer.listen(PORT, HOST, () => { | |
| console.log(`[server] ✓ Listening on http://${HOST}:${PORT}`) | |
| console.log('[server] Endpoints:') | |
| console.log(' GET /health — basic liveness probe') | |
| console.log(' GET /info — service info + event list') | |
| console.log(' GET / — root info') | |
| console.log(' WS /socket.io/ — WebSocket connection') | |
| console.log('') | |
| console.log('[server] Waiting for client connections...') | |
| }) | |
| } | |
| async function shutdown(signal) { | |
| console.log(`[server] ${signal} received, shutting down...`) | |
| try { | |
| // Disconnect all clients gracefully | |
| if (io) { | |
| io.disconnectSockets(true) | |
| } | |
| httpServer.close(() => { | |
| console.log('[server] HTTP server closed') | |
| process.exit(0) | |
| }) | |
| // Force exit after 5s if close hangs | |
| setTimeout(() => process.exit(0), 5000) | |
| } catch (e) { | |
| console.error('[server] Shutdown error:', e.message) | |
| process.exit(1) | |
| } | |
| } | |
| process.on('SIGTERM', () => shutdown('SIGTERM')) | |
| process.on('SIGINT', () => shutdown('SIGINT')) | |
| process.on('unhandledRejection', (err) => console.error('[server] Unhandled rejection:', err)) | |
| process.on('uncaughtException', (err) => { | |
| console.error('[server] Uncaught exception:', err) | |
| shutdown('uncaughtException') | |
| }) | |
| start() | |