diff --git a/api_v1_messages.php b/api_v1_messages.php index c19c168..418c59b 100644 --- a/api_v1_messages.php +++ b/api_v1_messages.php @@ -1,11 +1,35 @@ prepare("SELECT id FROM users WHERE bot_token = ? AND is_bot = TRUE"); + $stmt->execute([$bot_token]); + $bot = $stmt->fetch(); + if ($bot) { + $user_id = $bot['id']; + } else { + http_response_code(401); + echo json_encode(['success' => false, 'error' => 'Invalid Bot Token']); + exit; + } +} elseif (isset($_SESSION['user_id'])) { + $user_id = $_SESSION['user_id']; +} else { + http_response_code(401); + echo json_encode(['success' => false, 'error' => 'Unauthorized']); + exit; +} $data = json_decode(file_get_contents('php://input'), true); -$content = $data['content'] ?? ''; -$channel_id = (int)($data['channel_id'] ?? 1); -$user_id = 1; // Mock logged in user if (empty($content)) { echo json_encode(['success' => false, 'error' => 'Empty content']); diff --git a/api_v1_webhook.php b/api_v1_webhook.php new file mode 100644 index 0000000..fc87466 --- /dev/null +++ b/api_v1_webhook.php @@ -0,0 +1,44 @@ + false, 'error' => 'Missing token']); + exit; +} + +$stmt = db()->prepare("SELECT * FROM webhooks WHERE token = ?"); +$stmt->execute([$token]); +$webhook = $stmt->fetch(); + +if (!$webhook) { + http_response_code(401); + echo json_encode(['success' => false, 'error' => 'Invalid token']); + exit; +} + +if (empty($content)) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Empty content']); + exit; +} + +try { + // We'll use a special System user or a placeholder user_id for webhooks + // Or we could create a bot user for each webhook. + // For now, let's assume we use user_id 1 (System) but override the name if provided. + + $stmt = db()->prepare("INSERT INTO messages (channel_id, user_id, content) VALUES (?, ?, ?)"); + $stmt->execute([$webhook['channel_id'], 1, $content]); + + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/assets/css/discord.css b/assets/css/discord.css index 2e91fd8..ac5dfee 100644 --- a/assets/css/discord.css +++ b/assets/css/discord.css @@ -115,6 +115,50 @@ body { font-weight: 300; } +.voice-item::before { + content: "🔊"; + font-size: 0.9em; +} + +.channel-category { + color: var(--text-muted); + font-size: 0.75em; + text-transform: uppercase; + font-weight: bold; + margin-bottom: 8px; + padding-left: 8px; +} + +/* User Panel */ +.user-panel { + height: 52px; + background-color: #232428; + padding: 0 8px; + display: flex; + align-items: center; + gap: 8px; +} + +.user-info { + flex: 1; + display: flex; + align-items: center; + gap: 8px; + padding: 4px; + border-radius: 4px; + cursor: pointer; + min-width: 0; +} + +.user-info:hover { + background-color: var(--hover); +} + +.user-actions { + display: flex; + gap: 4px; +} + /* Chat Area */ .chat-container { flex: 1; diff --git a/assets/js/main.js b/assets/js/main.js index fba2c42..edc58cf 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -6,6 +6,26 @@ document.addEventListener('DOMContentLoaded', () => { // Scroll to bottom messagesList.scrollTop = messagesList.scrollHeight; + // WebSocket for real-time + let ws; + try { + ws = new WebSocket('ws://' + window.location.hostname + ':8080'); + ws.onmessage = (e) => { + const msg = JSON.parse(e.data); + if (msg.type === 'message') { + const data = JSON.parse(msg.data); + // Simple broadcast, we check if it belongs to current channel + const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1; + if (data.channel_id == currentChannel) { + appendMessage(data); + messagesList.scrollTop = messagesList.scrollHeight; + } + } + }; + } catch (e) { + console.warn('WebSocket connection failed, falling back to REST only.'); + } + chatForm.addEventListener('submit', async (e) => { e.preventDefault(); const content = chatInput.value.trim(); @@ -13,20 +33,44 @@ document.addEventListener('DOMContentLoaded', () => { chatInput.value = ''; + const channel_id = new URLSearchParams(window.location.search).get('channel_id') || 1; + try { const response = await fetch('api_v1_messages.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: content, - channel_id: new URLSearchParams(window.location.search).get('channel_id') || 1 + channel_id: channel_id }) - }); + // Voice + const voiceHandler = new VoiceChannel(ws); + document.querySelectorAll('.voice-item').forEach(item => { + item.addEventListener('click', () => { + const cid = item.dataset.channelId; + voiceHandler.join(cid); + + // UI Update + document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active')); + item.classList.add('active'); + }); + }); +}); + const result = await response.json(); if (result.success) { - appendMessage(result.message); - messagesList.scrollTop = messagesList.scrollHeight; + // If WS is connected, we might want to let WS handle the UI update + // But for simplicity, we append here and also send to WS + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + ...result.message, + channel_id: channel_id + })); + } else { + appendMessage(result.message); + messagesList.scrollTop = messagesList.scrollHeight; + } } else { alert('Error: ' + result.error); } diff --git a/assets/js/voice.js b/assets/js/voice.js new file mode 100644 index 0000000..8a0d47f --- /dev/null +++ b/assets/js/voice.js @@ -0,0 +1,30 @@ +// Placeholder for WebRTC Voice Logic +class VoiceChannel { + constructor(ws) { + this.ws = ws; + this.localStream = null; + this.peers = {}; + } + + async join(channelId) { + console.log('Joining voice channel:', channelId); + try { + this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + this.ws.send(JSON.stringify({ + type: 'voice_join', + channel_id: channelId + })); + // Signalization would happen here via WS + } catch (e) { + console.error('Failed to get local stream:', e); + alert('Could not access microphone.'); + } + } + + leave() { + if (this.localStream) { + this.localStream.getTracks().forEach(track => track.stop()); + } + this.ws.send(JSON.stringify({ type: 'voice_leave' })); + } +} diff --git a/auth/login.php b/auth/login.php new file mode 100644 index 0000000..aa6ef5b --- /dev/null +++ b/auth/login.php @@ -0,0 +1,69 @@ +prepare("SELECT * FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password_hash'])) { + $_SESSION['user_id'] = $user['id']; + header('Location: ../index.php'); + exit; + } else { + $error = "Invalid email or password."; + } + } else { + $error = "Please fill all fields."; + } +} +?> + + + + + Login | Discord Clone + + + + + +
+

Welcome back!

+

We're so excited to see you again!

+ +
+ +
+
+ + +
+
+ + +
+ Forgot your password? + + +
+
+ + diff --git a/auth/logout.php b/auth/logout.php new file mode 100644 index 0000000..37bc5ab --- /dev/null +++ b/auth/logout.php @@ -0,0 +1,5 @@ +prepare("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)"); + $stmt->execute([$username, $email, $hash]); + $_SESSION['user_id'] = db()->lastInsertId(); + header('Location: ../index.php'); + exit; + } catch (Exception $e) { + $error = "Registration failed: " . $e->getMessage(); + } + } else { + $error = "Please fill all fields."; + } +} +?> + + + + + Register | Discord Clone + + + + + +
+

Create an account

+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + diff --git a/auth/session.php b/auth/session.php new file mode 100644 index 0000000..3f12928 --- /dev/null +++ b/auth/session.php @@ -0,0 +1,20 @@ +prepare("SELECT * FROM users WHERE id = ?"); + $stmt->execute([$_SESSION['user_id']]); + return $stmt->fetch(); +} + +function requireLogin() { + if (!isset($_SESSION['user_id'])) { + header('Location: auth/login.php'); + exit; + } +} diff --git a/database/schema.sql b/database/schema.sql index 359b2aa..fd495fd 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -6,6 +6,8 @@ CREATE TABLE IF NOT EXISTS users ( password_hash VARCHAR(255) NOT NULL, avatar_url VARCHAR(255), status VARCHAR(20) DEFAULT 'offline', + is_bot BOOLEAN DEFAULT FALSE, + bot_token VARCHAR(64) UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -38,13 +40,44 @@ CREATE TABLE IF NOT EXISTS messages ( FOREIGN KEY (user_id) REFERENCES users(id) ); +CREATE TABLE IF NOT EXISTS webhooks ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + token VARCHAR(64) NOT NULL UNIQUE, + channel_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS roles ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL, + name VARCHAR(50) NOT NULL, + color VARCHAR(7) DEFAULT '#99aab5', + permissions INT DEFAULT 0, + position INT DEFAULT 0, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS user_roles ( + user_id INT NOT NULL, + role_id INT NOT NULL, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE +); + -- Seed initial data INSERT IGNORE INTO users (id, username, email, password_hash, status) VALUES (1, 'System', 'system@local', '$2y$10$xyz', 'online'); INSERT IGNORE INTO servers (id, name, owner_id, invite_code) VALUES -(1, 'General Community', 1, 'GEN-123'); +(1, 'General Community', 1, 'GEN-123'), +(2, 'Flatlogic Devs', 1, 'DEV-456'); -INSERT IGNORE INTO channels (id, server_id, name) VALUES -(1, 1, 'general'), -(2, 1, 'random'); +INSERT IGNORE INTO channels (id, server_id, name, type) VALUES +(1, 1, 'general', 'text'), +(2, 1, 'random', 'text'), +(3, 1, 'Voice General', 'voice'), +(4, 2, 'announcements', 'text'), +(5, 2, 'coding-help', 'text'); diff --git a/includes/permissions.php b/includes/permissions.php new file mode 100644 index 0000000..5ec6fbd --- /dev/null +++ b/includes/permissions.php @@ -0,0 +1,25 @@ +prepare(" + SELECT SUM(r.permissions) as total_perms + FROM roles r + JOIN user_roles ur ON r.id = ur.role_id + WHERE ur.user_id = ? AND r.server_id = ? + "); + $stmt->execute([$user_id, $server_id]); + $row = $stmt->fetch(); + $perms = (int)($row['total_perms'] ?? 0); + + if ($perms & self::ADMINISTRATOR) return true; + return ($perms & $permission) === $permission; + } +} diff --git a/index.php b/index.php index 91d35d7..30479a0 100644 --- a/index.php +++ b/index.php @@ -1,8 +1,9 @@ query("SELECT * FROM servers LIMIT 10")->fetchAll(); @@ -76,15 +77,37 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
-
- Text Channels -
- +
Text Channels
+ + +
Voice Channels
+ +
+ +
+ +
+
+
+
+
+
+ +
+
#
+
+
+
+ +
+
+
+ PHP |
@@ -137,6 +160,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; + diff --git a/ws/server.php b/ws/server.php new file mode 100644 index 0000000..8791c03 --- /dev/null +++ b/ws/server.php @@ -0,0 +1,100 @@ + 'message', 'data' => $decoded_data])); + foreach ($clients as $client) { + if ($client != $socket) { + @socket_write($client, $response, strlen($response)); + } + } + } + } +} + +function perform_handshake($receved_header, $client_conn, $host, $port) { + $headers = array(); + $lines = preg_split("/\r\n/", $receved_header); + foreach ($lines as $line) { + $line = chop($line); + if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) { + $headers[$matches[1]] = $matches[2]; + } + } + + $secKey = $headers['Sec-WebSocket-Key']; + $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); + $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" . + "Upgrade: websocket\r\n" . + "Connection: Upgrade\r\n" . + "Sec-WebSocket-Accept: $secAccept\r\n\r\n"; + socket_write($client_conn, $upgrade, strlen($upgrade)); +} + +function unmask($text) { + $length = ord($text[1]) & 127; + if ($length == 126) { + $masks = substr($text, 4, 4); + $data = substr($text, 8); + } elseif ($length == 127) { + $masks = substr($text, 10, 4); + $data = substr($text, 14); + } else { + $masks = substr($text, 2, 4); + $data = substr($text, 6); + } + $text = ""; + for ($i = 0; $i < strlen($data); ++$i) { + $text .= $data[$i] ^ $masks[$i % 4]; + } + return $text; +} + +function mask($text) { + $b1 = 0x80 | (0x1 & 0x0f); + $length = strlen($text); + if ($length <= 125) + $header = pack('CC', $b1, $length); + elseif ($length > 125 && $length < 65536) + $header = pack('CCn', $b1, 126, $length); + elseif ($length >= 65536) + $header = pack('CCNN', $b1, 127, $length); + return $header . $text; +} diff --git a/ws_output.log b/ws_output.log new file mode 100644 index 0000000..d536511 --- /dev/null +++ b/ws_output.log @@ -0,0 +1 @@ +Server started on 0.0.0.0:8080