Files
InviCanvas/server/socket.ts
2026-01-16 17:06:43 +01:00

156 lines
5.2 KiB
TypeScript

import { Server as SocketIOServer, Socket } from 'socket.io';
import { upsertPixelsWithConflictResolution, getAllPixels, db, validatePixel } from './database';
import { Cursor, Pixel } from '../shared/types';
import passport from 'passport';
function generateRandomColor(): string {
const colors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2'
];
return colors[Math.floor(Math.random() * colors.length)];
}
interface UserSocket extends Socket {
userId: string;
username: string;
color: string;
user?: any;
}
interface PaintRequest {
pixels: Pixel[];
timestamp: number;
}
interface PaintAck {
success: boolean;
saved: number;
conflicts: number;
pixels?: Pixel[];
message?: string;
}
function getUserById(userId: number) {
try {
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
return stmt.get(userId);
} catch (error) {
console.error('Error fetching user:', error);
return null;
}
}
export function setupSocketIO(io: SocketIOServer): void {
const connectedUsers = new Map<string, { x: number; y: number; color: string; username: string; userId: string }>();
io.on('connection', (socket: Socket) => {
const userSocket = socket as UserSocket;
socket.on('join-canvas', (token: string) => {
try {
const decoded = JSON.parse(Buffer.from(token, 'base64').toString());
if (Date.now() - decoded.timestamp > 5 * 60 * 1000) {
socket.emit('error', 'Token expired');
socket.disconnect();
return;
}
const userData = getUserById(decoded.userId);
if (userData) {
userSocket.user = userData;
const userId = `auth_${userData.id}`;
const username = userData.username || userData.display_name;
const color = generateRandomColor();
connectedUsers.set(userId, {
x: 0,
y: 0,
color: color,
username: username,
userId: userData.id.toString()
});
socket.emit('canvas-state', getAllPixels());
socket.emit('cursor-update', Array.from(connectedUsers.values()));
socket.emit('auth-success', { user: userData });
socket.broadcast.emit('cursor-update', Array.from(connectedUsers.values()));
socket.on('cursor-move', (cursor: Cursor) => {
const user = connectedUsers.get(userId);
if (user) {
user.x = cursor.x;
user.y = cursor.y;
socket.broadcast.emit('cursor-update', Array.from(connectedUsers.values()));
}
});
socket.on('paint-pixels', async (request: PaintRequest, callback: (ack: PaintAck) => void) => {
try {
if (!userSocket.user) {
const ack: PaintAck = { success: false, saved: 0, conflicts: 0, message: 'Authentication required' };
callback(ack);
return;
}
const { pixels, timestamp } = request;
if (!Array.isArray(pixels) || pixels.length === 0) {
const ack: PaintAck = { success: false, saved: 0, conflicts: 0, message: 'Invalid pixels data' };
callback(ack);
return;
}
for (const pixel of pixels) {
const validationError = validatePixel(pixel);
if (validationError) {
const ack: PaintAck = { success: false, saved: 0, conflicts: 0, message: validationError };
callback(ack);
return;
}
}
const userIdNum = typeof userData.id === 'number' ? userData.id : parseInt(userData.id, 10);
const result = upsertPixelsWithConflictResolution(
pixels.map(p => ({ ...p, userId: userIdNum })),
timestamp || 0
);
if (result.saved > 0) {
socket.broadcast.emit('canvas-update', { pixels });
}
const ack: PaintAck = {
success: true,
saved: result.saved,
conflicts: result.conflicts,
pixels: result.conflicts > 0 ? pixels : undefined
};
callback(ack);
} catch (error) {
console.error('Error saving pixels:', error);
const ack: PaintAck = { success: false, saved: 0, conflicts: 0, message: 'Failed to save pixels' };
callback(ack);
}
});
socket.on('disconnect', () => {
connectedUsers.delete(userId);
socket.broadcast.emit('cursor-update', Array.from(connectedUsers.values()));
});
} else {
console.error('User not found for token:', decoded.userId);
socket.emit('error', 'Invalid user');
socket.disconnect();
}
} catch (error) {
console.error('Token verification failed:', error);
socket.emit('error', 'Invalid token');
socket.disconnect();
}
});
});
}