5a6f92ede6
- Закрепление/открепление сообщений (сервер + клиент) - Сохранение истории сообщений в localStorage (до 50) - Сохранение логина/пароля в localStorage и авто-вход - Авто-переподключение при разрыве соединения WebSocket
88 lines
3.0 KiB
JavaScript
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);
|
|
});
|
|
});
|