156 lines
5.2 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|
|
} |