hangout-socket / server.js
iosrvk0's picture
Upload 3 files
4fe5060 verified
Raw
History Blame Contribute Delete
15.7 kB
// ============================================================
// 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()