Current state
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import { createServer } from "http"
|
||||
import { Server } from "socket.io"
|
||||
import { SignJWT, jwtVerify } from "jose"
|
||||
import pg from "pg"
|
||||
|
||||
const PORT = process.env.SIGNALING_PORT || 3007
|
||||
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET || "crm-envr-super-secret-key-2026")
|
||||
const DATABASE_URL = process.env.DATABASE_URL || "postgres://postgres:postgres@localhost:5432/crm"
|
||||
|
||||
const pool = new pg.Pool({ connectionString: DATABASE_URL })
|
||||
|
||||
const httpServer = createServer()
|
||||
const io = new Server(httpServer, { cors: { origin: "*", methods: ["GET", "POST"] } })
|
||||
|
||||
const onlineUsers = new Map()
|
||||
const rooms = new Map()
|
||||
|
||||
io.use((socket, next) => {
|
||||
const token = socket.handshake.auth?.token
|
||||
if (token) {
|
||||
jwtVerify(token, JWT_SECRET)
|
||||
.then(({ payload }) => {
|
||||
socket.data.userId = payload.userId
|
||||
socket.data.role = payload.role
|
||||
next()
|
||||
})
|
||||
.catch(() => {
|
||||
socket.data.userId = null
|
||||
socket.data.role = null
|
||||
next()
|
||||
})
|
||||
} else {
|
||||
socket.data.userId = null
|
||||
socket.data.role = null
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
const userId = socket.data.userId
|
||||
|
||||
if (userId) {
|
||||
onlineUsers.set(userId, socket.id)
|
||||
socket.broadcast.emit("user:online", { userId })
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
socket.on("user:lookup", async ({ phone }, callback) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT id, username, first_name, last_name, phone, avatar_url
|
||||
FROM users
|
||||
WHERE phone = $1 AND deleted_at IS NULL
|
||||
LIMIT 1`,
|
||||
[phone],
|
||||
)
|
||||
if (result.rows.length > 0) {
|
||||
const user = result.rows[0]
|
||||
callback({
|
||||
found: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
phone: user.phone,
|
||||
avatar: user.avatar_url,
|
||||
online: onlineUsers.has(user.id),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
callback({ found: false })
|
||||
}
|
||||
} catch {
|
||||
callback({ found: false, error: "Lookup failed" })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("call:offer", async ({ to, sdp, callerInfo }) => {
|
||||
const targetSocketId = onlineUsers.get(to)
|
||||
if (targetSocketId) {
|
||||
let info = callerInfo
|
||||
if (!info) {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT id, username, first_name, last_name, avatar_url
|
||||
FROM users WHERE id = $1`,
|
||||
[userId],
|
||||
)
|
||||
if (result.rows.length > 0) {
|
||||
const u = result.rows[0]
|
||||
info = {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
firstName: u.first_name,
|
||||
lastName: u.last_name,
|
||||
avatar: u.avatar_url,
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
io.to(targetSocketId).emit("call:incoming", {
|
||||
from: userId,
|
||||
sdp,
|
||||
callerInfo: info,
|
||||
})
|
||||
} else {
|
||||
socket.emit("call:user-offline", { userId: to })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("call:answer", ({ to, sdp }) => {
|
||||
const targetSocketId = onlineUsers.get(to)
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit("call:answered", { from: userId, sdp })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("call:ice-candidate", ({ to, candidate }) => {
|
||||
const targetSocketId = onlineUsers.get(to)
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit("call:ice-candidate", { from: userId, candidate })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("call:end", ({ to }) => {
|
||||
const targetSocketId = onlineUsers.get(to)
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit("call:ended", { from: userId })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("call:reject", ({ to }) => {
|
||||
const targetSocketId = onlineUsers.get(to)
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit("call:rejected", { from: userId })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("call:busy", ({ to }) => {
|
||||
const targetSocketId = onlineUsers.get(to)
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit("call:busy", { from: userId })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
socket.on("room:join", ({ roomId, displayName }) => {
|
||||
const participantId = userId || `anon-${socket.id}`
|
||||
const participantLabel = displayName || participantId
|
||||
|
||||
if (!rooms.has(roomId)) {
|
||||
rooms.set(roomId, [])
|
||||
}
|
||||
const participants = rooms.get(roomId)
|
||||
participants.push({ socketId: socket.id, id: participantId, label: participantLabel })
|
||||
socket.join(roomId)
|
||||
|
||||
const list = participants.map(p => ({ id: p.id, label: p.label }))
|
||||
|
||||
if (participants.length === 1) {
|
||||
socket.emit("room:waiting", { roomId, participants: list })
|
||||
} else if (participants.length === 2) {
|
||||
io.to(participants[0].socketId).emit("room:initiate-offer", { roomId, participants: list })
|
||||
io.to(participants[1].socketId).emit("room:participant-joined", { roomId, participants: list })
|
||||
} else {
|
||||
socket.emit("room:participant-joined", { roomId, participants: list })
|
||||
socket.to(roomId).emit("room:participant-joined", { roomId, participants: list })
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("room:offer", ({ roomId, sdp }) => {
|
||||
socket.to(roomId).emit("room:offer", { sdp })
|
||||
})
|
||||
|
||||
socket.on("room:answer", ({ roomId, sdp }) => {
|
||||
socket.to(roomId).emit("room:answer", { sdp })
|
||||
})
|
||||
|
||||
socket.on("room:ice-candidate", ({ roomId, candidate }) => {
|
||||
socket.to(roomId).emit("room:ice-candidate", { candidate })
|
||||
})
|
||||
|
||||
socket.on("room:leave", ({ roomId }) => {
|
||||
socket.leave(roomId)
|
||||
const participants = rooms.get(roomId)
|
||||
if (participants) {
|
||||
const idx = participants.findIndex(p => p.socketId === socket.id)
|
||||
if (idx !== -1) participants.splice(idx, 1)
|
||||
if (participants.length === 0) {
|
||||
rooms.delete(roomId)
|
||||
} else {
|
||||
const list = participants.map(p => ({ id: p.id, label: p.label }))
|
||||
io.to(roomId).emit("room:participant-left", { roomId, participants: list })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
for (const [roomId, participants] of rooms) {
|
||||
const idx = participants.findIndex(p => p.socketId === socket.id)
|
||||
if (idx !== -1) {
|
||||
participants.splice(idx, 1)
|
||||
if (participants.length === 0) {
|
||||
rooms.delete(roomId)
|
||||
} else {
|
||||
const list = participants.map(p => ({ id: p.id, label: p.label }))
|
||||
io.to(roomId).emit("room:participant-left", { roomId, participants: list })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (userId) {
|
||||
onlineUsers.delete(userId)
|
||||
socket.broadcast.emit("user:offline", { userId })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`[signaling] server running on port ${PORT}`)
|
||||
})
|
||||
Reference in New Issue
Block a user