63bf73d2c4
Сервер: - Вынесена конфигурация в server/config.js (порт, размер файла из env) - Вынесена аутентификация в server/auth.js (загрузка users.json, authenticate()) - Вынесено управление соединениями в server/clients.js (createJsonSender, broadcast, validateFile) - server/server.js стал точкой входа — минимальный код Клиент: - Вынесены хуки: hooks/useWebSocket.js (WebSocket + auth), hooks/useChatMessages.js (сообщения + уведомления) - Вынесены утилиты: utils/blob.js (base64 → Blob URL), utils/linkify.jsx (URL → ссылки), utils/notify.js (звук + favicon) Новые функции: - VoiceRecorder — запись голоса через MediaRecorder, отправка как файл - Аудио/видео плеер в Message (audio/*, video/* с controls) - URL linkification — http/https ссылки автоматически кликабельны - Звуковое уведомление (Web Audio API) при сообщении на неактивной вкладке - Красная точка на favicon при непрочитанных сообщениях Инфраструктура: - docker-entrypoint.sh: авто-перезапуск Node.js сервера при падении - Обновлён README.md: новая структура проекта, список функций, примеры - Кастомный тонкий скроллбар в Message.css
69 lines
2.3 KiB
JavaScript
69 lines
2.3 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}`);
|
|
|
|
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) {
|
|
return client.handleAuth(user.login);
|
|
}
|
|
client.handleAuthFail('Invalid login or password');
|
|
return null;
|
|
}
|
|
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);
|
|
});
|
|
});
|