Files
Комаров Данил Анатольевич 6 5a6f92ede6 Добавлены закреплённые сообщения, сохранение истории и авто-переподключение
- Закрепление/открепление сообщений (сервер + клиент)
- Сохранение истории сообщений в localStorage (до 50)
- Сохранение логина/пароля в localStorage и авто-вход
- Авто-переподключение при разрыве соединения WebSocket
2026-05-29 15:57:18 +03:00

88 lines
3.0 KiB
JavaScript

const { WebSocketServer } = require('ws');
const config = require('./config');
const { authenticate } = require('./auth');
const { createJsonSender, createClientHandlers, validateFile, broadcast } = require('./clients');
const server = new WebSocketServer({ port: config.port });
console.log(`WebSocket server running on ws://0.0.0.0:${config.port}`);
let pinnedMessage = null;
server.on('connection', (ws, req) => {
const clientIp = req.socket.remoteAddress;
console.log(`Connection from ${clientIp}`);
const send = createJsonSender(ws);
const client = createClientHandlers(ws, send);
const handle = client.wrapHandler((parsed) => {
switch (parsed.type) {
case 'auth': {
const user = authenticate(parsed.login, parsed.password);
if (user) {
const sysMsg = client.handleAuth(user.login);
if (pinnedMessage) {
send({ type: 'pinned', message: pinnedMessage.message, pinnedBy: pinnedMessage.from });
}
return sysMsg;
}
client.handleAuthFail('Invalid login or password');
return null;
}
case 'pin': {
pinnedMessage = {
type: 'pinned', from: client.getLogin(), timestamp: Date.now(),
message: parsed.message,
};
console.log(`${client.getLogin()} pinned a message`);
return pinnedMessage;
}
case 'unpin': {
pinnedMessage = null;
console.log(`${client.getLogin()} unpinned the message`);
return { type: 'unpinned', from: client.getLogin(), timestamp: Date.now() };
}
case 'text': {
if (!parsed.text || typeof parsed.text !== 'string') {
send({ type: 'error', message: 'Invalid text message' });
return null;
}
console.log(`Text from ${client.getLogin()}: ${parsed.text.substring(0, 50)}`);
return { type: 'text', from: client.getLogin(), timestamp: Date.now(), text: parsed.text };
}
case 'file': {
const error = validateFile(parsed);
if (error) {
send({ type: 'error', message: error });
return null;
}
console.log(`File from ${client.getLogin()}: ${parsed.filename} (${parsed.mime})`);
return {
type: 'file', from: client.getLogin(), timestamp: Date.now(),
filename: parsed.filename, mime: parsed.mime, data: parsed.data,
};
}
default:
send({ type: 'error', message: 'Unknown message type' });
return null;
}
});
ws.on('message', (raw) => {
const msg = handle(raw);
if (msg) {
broadcast(server, msg, null);
}
});
ws.on('close', (code) => {
console.log(`Disconnected: ${client.getLogin() || 'unauthenticated'} (code: ${code})`);
if (client.getLogin()) {
broadcast(server, { type: 'system', text: `${client.getLogin()} left the chat` }, null);
}
});
ws.on('error', (err) => {
console.error(`Socket error (${client.getLogin() || 'unknown'}):`, err.message);
});
});