// ============================================================ // 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()