{ "name": "global-chat", "version": "1.0.0", "description": "نظام شات عالمي بغرف ورتب مع دعم صوتي", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2", "socket.io": "^4.7.2", "peer": "^1.0.0" } }const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const { ExpressPeerServer } = require('peer'); const app = express(); const server = http.createServer(app); const io = socketIo(server); // إعداد PeerJS للصوت const peerServer = ExpressPeerServer(server, { debug: true, path: '/' }); app.use('/peerjs', peerServer); // خدمة الملفات الثابتة app.use(express.static('public')); // نظام الرتب const ROLES = { OWNER: 'مالك', ADMIN: 'مشرف', MOD: 'مراقب', MEMBER: 'عضو', GUEST: 'زائر' }; // بيانات الغرف والمستخدمين const rooms = new Map(); // roomId -> { name, users: Map(socketId -> userData) } // إنشاء غرفة افتراضية rooms.set('general', { name: 'الغرفة العامة', users: new Map() }); // مساعدة لإنشاء معرف فريد function generateId() { return Math.random().toString(36).substring(2, 10); } // إرسال قائمة الغرف function emitRoomsList() { const roomsList = Array.from(rooms.entries()).map(([id, room]) => ({ id, name: room.name, usersCount: room.users.size })); io.emit('rooms:list', roomsList); } // إرسال بيانات المستخدمين في غرفة function emitRoomUsers(roomId) { const room = rooms.get(roomId); if (!room) return; const users = Array.from(room.users.values()).map(u => ({ id: u.id, username: u.username, role: u.role, peerId: u.peerId })); io.to(roomId).emit('room:users', users); } // تعيين رتبة افتراضية حسب الحالة function getDefaultRole(isFirstUser) { return isFirstUser ? ROLES.OWNER : ROLES.MEMBER; } io.on('connection', (socket) => { console.log('اتصال جديد:', socket.id); let currentRoom = null; let userData = null; // إرسال قائمة الغرف عند الاتصال socket.emit('rooms:list', Array.from(rooms.entries()).map(([id, room]) => ({ id, name: room.name, usersCount: room.users.size }))); // الانضمام إلى غرفة socket.on('room:join', ({ roomId, username, peerId }) => { // مغادرة الغرفة الحالية if (currentRoom && rooms.has(currentRoom)) { const oldRoom = rooms.get(currentRoom); oldRoom.users.delete(socket.id); socket.leave(currentRoom); emitRoomUsers(currentRoom); emitRoomsList(); } // إنشاء الغرفة إذا لم تكن موجودة if (!rooms.has(roomId)) { rooms.set(roomId, { name: roomId, users: new Map() }); } const room = rooms.get(roomId); currentRoom = roomId; // تحديد الرتبة const isFirstUser = room.users.size === 0; const role = getDefaultRole(isFirstUser); userData = { id: socket.id, username: username || 'زائر_' + socket.id.substring(0, 4), role: role, peerId: peerId || null }; room.users.set(socket.id, userData); socket.join(roomId); // إرسال تأكيد الانضمام socket.emit('room:joined', { roomId, roomName: room.name, user: userData }); // إشعار باقي المستخدمين socket.to(roomId).emit('chat:message', { type: 'system', text: `${userData.username} انضم إلى الغرفة`, timestamp: Date.now() }); emitRoomUsers(roomId); emitRoomsList(); }); // استقبال رسالة نصية socket.on('chat:message', (text) => { if (!currentRoom || !userData) return; const message = { type: 'text', userId: socket.id, username: userData.username, role: userData.role, text: text.substring(0, 500), // حد أقصى 500 حرف timestamp: Date.now() }; io.to(currentRoom).emit('chat:message', message); }); // مشاركة إشارات الصوت (WebRTC signaling) socket.on('voice:signal', ({ targetPeerId, signal }) => { if (!currentRoom) return; const room = rooms.get(currentRoom); if (!room) return; const target = room.users.get(targetPeerId); if (target && target.peerId) { socket.to(currentRoom).emit('voice:signal', { fromPeerId: socket.id, signal: signal }); } }); // مغادرة الغرفة socket.on('room:leave', () => { if (currentRoom && rooms.has(currentRoom)) { const room = rooms.get(currentRoom); room.users.delete(socket.id); socket.to(currentRoom).emit('chat:message', { type: 'system', text: `${userData?.username || 'مستخدم'} غادر الغرفة`, timestamp: Date.now() }); socket.leave(currentRoom); emitRoomUsers(currentRoom); emitRoomsList(); currentRoom = null; userData = null; } }); // فصل الاتصال socket.on('disconnect', () => { if (currentRoom && rooms.has(currentRoom)) { const room = rooms.get(currentRoom); room.users.delete(socket.id); socket.to(currentRoom).emit('chat:message', { type: 'system', text: `${userData?.username || 'مستخدم'} قطع الاتصال`, timestamp: Date.now() }); emitRoomUsers(currentRoom); emitRoomsList(); } }); // إنشاء غرفة جديدة socket.on('room:create', (roomName) => { const roomId = generateId(); rooms.set(roomId, { name: roomName || 'غرفة_' + roomId, users: new Map() }); emitRoomsList(); socket.emit('room:created', { roomId, name: roomName }); }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`الخادم يعمل على المنفذ ${PORT}`); }); الشات العالمي - غرف ورتب وصوت

الغرفة العامة

* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, sans-serif; background: #1a1a2e; color: #fff; height: 100vh; overflow: hidden; } .app { display: flex; height: 100vh; } /* الشريط الجانبي */ .sidebar { width: 280px; background: #16213e; padding: 20px; display: flex; flex-direction: column; gap: 20px; } .sidebar-header { display: flex; justify-content: space-between; align-items: center; } .rooms-list, .users-list { list-style: none; max-height: 300px; overflow-y: auto; } .room-item { padding: 10px; margin: 5px 0; background: #0f3460; border-radius: 8px; cursor: pointer; transition: all 0.3s; } .room-item:hover { background: #533483; } .room-item.active { background: #e94560; } .user-item { padding: 8px; margin: 3px 0; display: flex; align-items: center; gap: 8px; font-size: 14px; } .role-badge { padding: 4px 10px; border-radius: 12px; font-size: 12px; background: #e94560; } .role-مالك { background: #f39c12; } .role-مشرف { background: #e74c3c; } .role-مراقب { background: #3498db; } .role-عضو { background: #2ecc71; } .role-زائر { background: #95a5a6; } /* منطقة الشات */ .main { flex: 1; display: flex; flex-direction: column; } .room-header { background: #16213e; padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #0f3460; } .user-info { display: flex; align-items: center; gap: 10px; } .messages { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; } .message { max-width: 70%; padding: 10px 15px; border-radius: 15px; background: #0f3460; align-self: flex-start; } .message.own { align-self: flex-end; background: #533483; } .message .meta { display: flex; gap: 8px; font-size: 12px; color: #aaa; margin-bottom: 5px; } .message.system { align-self: center; background: transparent; color: #ffd700; font-style: italic; text-align: center; } .message-input { display: flex; padding: 20px; background: #16213e; gap: 10px; } .message-input input { flex: 1; padding: 12px; border: none; border-radius: 8px; background: #0f3460; color: #fff; } .btn { padding: 10px 20px; background: #e94560; border: none; border-radius: 8px; color: #fff; cursor: pointer; transition: all 0.3s; } .btn:hover { background: #c73e54; } .btn-small { padding: 5px 12px; font-size: 13px; } .btn-danger { background: #c0392b; } /* نافذة تسجيل الدخول */ .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); display: flex; justify-content: center; align-items: center; } .modal-content { background: #16213e; padding: 30px; border-radius: 15px; display: flex; flex-direction: column; gap: 15px; width: 300px; } .modal-content input, .modal-content select { padding: 10px; border: none; border-radius: 8px; background: #0f3460; color: #fff; }// الاتصال بالخادم const socket = io(); let peer = null; let myPeerId = null; let currentRoomId = null; let currentUser = null; let voiceCalls = {}; // تخزين المكالمات الصوتية // عناصر DOM const loginModal = document.getElementById('login-modal'); const usernameInput = document.getElementById('username-input'); const roomSelect = document.getElementById('room-select'); const joinBtn = document.getElementById('join-btn'); const roomsList = document.getElementById('rooms-list'); const usersList = document.getElementById('users-list'); const messagesDiv = document.getElementById('messages'); const messageInput = document.getElementById('message-input'); const sendBtn = document.getElementById('send-btn'); const currentRoomName = document.getElementById('current-room-name'); const usernameDisplay = document.getElementById('username-display'); const roleBadge = document.getElementById('role-badge'); const voiceToggleBtn = document.getElementById('voice-toggle'); const leaveRoomBtn = document.getElementById('leave-room-btn'); const createRoomBtn = document.getElementById('create-room-btn'); // تهيئة PeerJS للصوت function initPeer() { peer = new Peer(undefined, { host: '/', port: 3000, path: '/peerjs' }); peer.on('open', (id) => { myPeerId = id; console.log('Peer ID:', myPeerId); }); peer.on('call', (call) => { // استقبال مكالمة صوتية navigator.mediaDevices.getUserMedia({ audio: true }) .then((stream) => { call.answer(stream); call.on('stream', (remoteStream) => { playRemoteAudio(remoteStream, call.peer); }); }) .catch(err => console.error('خطأ في الميكروفون:', err)); }); } // تشغيل الصوت القادم function playRemoteAudio(stream, peerId) { let audio = document.getElementById('audio-' + peerId); if (!audio) { audio = document.createElement('audio'); audio.id = 'audio-' + peerId; audio.autoplay = true; document.body.appendChild(audio); } audio.srcObject = stream; voiceCalls[peerId] = audio; } // إجراء مكالمة صوتية لجميع المستخدمين في الغرفة async function startVoiceCall() { if (!peer || !myPeerId) return; try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // جلب قائمة المستخدمين من الغرفة الحالية socket.emit('room:get-users', currentRoomId); } catch (err) { console.error('لا يمكن الوصول للميكروفون:', err); alert('يرجى السماح بالوصول إلى الميكروفون'); } } // عند استقبال قائمة المستخدمين للاتصال الصوتي socket.on('voice:connect-to-users', (users) => { if (!peer) return; navigator.mediaDevices.getUserMedia({ audio: true }) .then((stream) => { users.forEach(user => { if (user.id !== socket.id && user.peerId && !voiceCalls[user.id]) { const call = peer.call(user.peerId, stream); call.on('stream', (remoteStream) => { playRemoteAudio(remoteStream, user.id); }); voiceCalls[user.id] = call; } }); }) .catch(err => console.error(err)); }); // عرض قائمة الغرف function renderRooms(rooms) { roomsList.innerHTML = ''; rooms.forEach(room => { const li = document.createElement('li'); li.className = 'room-item' + (room.id === currentRoomId ? ' active' : ''); li.textContent = `${room.name} (${room.usersCount})`; li.onclick = () => switchRoom(room.id); roomsList.appendChild(li); // تحديث قائمة الغرف في نافذة الدخول const option = document.createElement('option'); option.value = room.id; option.textContent = room.name; if (!roomSelect.querySelector(`option[value="${room.id}"]`)) { roomSelect.appendChild(option); } }); } // عرض قائمة المستخدمين function renderUsers(users) { usersList.innerHTML = ''; users.forEach(user => { const li = document.createElement('li'); li.className = 'user-item'; li.innerHTML = ` ${user.username} ${user.role} ${user.peerId ? '🎤' : ''} `; usersList.appendChild(li); }); } // إضافة رسالة إلى الشاشة function addMessage(message) { const div = document.createElement('div'); div.className = 'message'; if (message.type === 'system') { div.className = 'message system'; div.textContent = message.text; } else { if (message.userId === socket.id) { div.classList.add('own'); } div.innerHTML = `
${message.username} ${message.role} ${new Date(message.timestamp).toLocaleTimeString('ar')}
${message.text}
`; } messagesDiv.appendChild(div); messagesDiv.scrollTop = messagesDiv.scrollHeight; } // الانضمام إلى غرفة function joinRoom(roomId, username) { currentRoomId = roomId; socket.emit('room:join', { roomId, username, peerId: myPeerId }); } // تبديل الغرفة function switchRoom(roomId) { if (roomId !== currentRoomId) { // إيقاف جميع المكالمات الصوتية Object.values(voiceCalls).forEach(call => { try { call.close(); } catch(e) {} }); voiceCalls = {}; // إزالة عناصر الصوت document.querySelectorAll('audio').forEach(a => a.remove()); joinRoom(roomId, currentUser?.username || 'زائر'); } } // الأحداث من الخادم socket.on('rooms:list', renderRooms); socket.on('room:users', renderUsers); socket.on('room:joined', (data) => { currentRoomId = data.roomId; currentRoomName.textContent = data.roomName; currentUser = data.user; usernameDisplay.textContent = data.user.username; roleBadge.textContent = data.user.role; roleBadge.className = 'role-badge role-' + data.user.role; messagesDiv.innerHTML = ''; loginModal.style.display = 'none'; }); socket.on('chat:message', addMessage); socket.on('room:created', ({ roomId, name }) => { joinRoom(roomId, currentUser?.username || 'زائر'); }); socket.on('voice:signal', ({ fromPeerId, signal }) => { // استقبال إشارة صوتية - يمكن استخدامها مع PeerJS }); // أحداث الواجهة joinBtn.addEventListener('click', () => { const username = usernameInput.value.trim() || 'زائر'; const roomId = roomSelect.value; joinRoom(roomId, username); }); sendBtn.addEventListener('click', sendMessage); messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); function sendMessage() { const text = messageInput.value.trim(); if (text) { socket.emit('chat:message', text); messageInput.value = ''; } } voiceToggleBtn.addEventListener('click', () => { if (voiceToggleBtn.textContent.includes('تفعيل')) { startVoiceCall(); voiceToggleBtn.textContent = '🔇 إيقاف الصوت'; } else { // إيقاف المكالمات Object.values(voiceCalls).forEach(call => { try { call.close(); } catch(e) {} }); voiceCalls = {}; document.querySelectorAll('audio').forEach(a => a.remove()); voiceToggleBtn.textContent = '🎤 تفعيل الصوت'; } }); leaveRoomBtn.addEventListener('click', () => { socket.emit('room:leave'); loginModal.style.display = 'flex'; currentRoomId = null; }); createRoomBtn.addEventListener('click', () => { const roomName = prompt('اسم الغرفة الجديدة:'); if (roomName) { socket.emit('room:create', roomName); } }); // بدء الاتصال initPeer();