diff --git a/api_v1_channels.php b/api_v1_channels.php index 133af40..4850062 100644 --- a/api_v1_channels.php +++ b/api_v1_channels.php @@ -1,24 +1,75 @@ prepare("SELECT * FROM channels WHERE server_id = ?"); + $stmt->execute([$server_id]); + echo json_encode($stmt->fetchAll()); + exit; +} + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? 'create'; $server_id = $_POST['server_id'] ?? 0; + $user_id = $_SESSION['user_id']; + + if ($action === 'update') { + $channel_id = $_POST['channel_id'] ?? 0; + $name = $_POST['name'] ?? ''; + $allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0; + + // Check if user is owner of the server + $stmt = db()->prepare("SELECT s.owner_id FROM servers s JOIN channels c ON s.id = c.server_id WHERE c.id = ?"); + $stmt->execute([$channel_id]); + $server = $stmt->fetch(); + + if ($server && $server['owner_id'] == $user_id) { + $name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name)); + $stmt = db()->prepare("UPDATE channels SET name = ?, allow_file_sharing = ? WHERE id = ?"); + $stmt->execute([$name, $allow_file_sharing, $channel_id]); + } + header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id); + exit; + } + + if ($action === 'delete') { + $channel_id = $_POST['channel_id'] ?? 0; + // Check if user is owner + $stmt = db()->prepare("SELECT s.owner_id, s.id as server_id FROM servers s JOIN channels c ON s.id = c.server_id WHERE c.id = ?"); + $stmt->execute([$channel_id]); + $server = $stmt->fetch(); + + if ($server && $server['owner_id'] == $user_id) { + $stmt = db()->prepare("DELETE FROM channels WHERE id = ?"); + $stmt->execute([$channel_id]); + } + header('Location: index.php?server_id=' . ($server['server_id'] ?? '')); + exit; + } + $name = $_POST['name'] ?? ''; $type = $_POST['type'] ?? 'text'; $user_id = $_SESSION['user_id']; - // Check if user is owner of the server or has permissions (simplified check for now: user must be a member) + // Check if user is member of the server $stmt = db()->prepare("SELECT 1 FROM server_members WHERE server_id = ? AND user_id = ?"); $stmt->execute([$server_id, $user_id]); if ($stmt->fetch() && $name) { try { // Basic sanitization for channel name - $name = strtolower(preg_replace('/[^a-zA-Z0-3\-]/', '-', $name)); + $name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name)); + $allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0; - $stmt = db()->prepare("INSERT INTO channels (server_id, name, type) VALUES (?, ?, ?)"); - $stmt->execute([$server_id, $name, $type]); + $stmt = db()->prepare("INSERT INTO channels (server_id, name, type, allow_file_sharing) VALUES (?, ?, ?, ?)"); + $stmt->execute([$server_id, $name, $type, $allow_file_sharing]); $channel_id = db()->lastInsertId(); header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id); diff --git a/api_v1_messages.php b/api_v1_messages.php index ba12ee9..07b3b7c 100644 --- a/api_v1_messages.php +++ b/api_v1_messages.php @@ -2,6 +2,7 @@ header('Content-Type: application/json'); require_once 'auth/session.php'; require_once 'includes/opengraph.php'; +require_once 'includes/ai_filtering.php'; // Check for Bot token in headers $headers = getallheaders(); @@ -86,7 +87,17 @@ if (strpos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== false) { $content = $_POST['content'] ?? ''; $channel_id = $_POST['channel_id'] ?? 0; + // Check if file sharing is allowed in this channel + $stmt = db()->prepare("SELECT allow_file_sharing FROM channels WHERE id = ?"); + $stmt->execute([$channel_id]); + $channel = $stmt->fetch(); + $can_share_files = $channel ? (bool)$channel['allow_file_sharing'] : true; + if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) { + if (!$can_share_files) { + echo json_encode(['success' => false, 'error' => 'File sharing is disabled in this channel.']); + exit; + } $upload_dir = 'assets/uploads/'; if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true); @@ -104,6 +115,14 @@ if (empty($content) && empty($attachment_url)) { exit; } +if (!empty($content)) { + $moderation = moderateContent($content); + if (!$moderation['is_safe']) { + echo json_encode(['success' => false, 'error' => 'Message flagged as inappropriate: ' . ($moderation['reason'] ?? 'Violation of community standards')]); + exit; + } +} + $metadata = null; if (!empty($content)) { $urls = extractUrls($content); diff --git a/api_v1_webhook.php b/api_v1_webhook.php index fc87466..c436214 100644 --- a/api_v1_webhook.php +++ b/api_v1_webhook.php @@ -1,44 +1,84 @@ 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. +// Check for execution (no session needed, just token) +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['token'])) { + require_once 'db/config.php'; + $token = $_GET['token'] ?? ''; + $data = json_decode(file_get_contents('php://input'), true); + $content = $data['content'] ?? ''; - $stmt = db()->prepare("INSERT INTO messages (channel_id, user_id, content) VALUES (?, ?, ?)"); - $stmt->execute([$webhook['channel_id'], 1, $content]); + $stmt = db()->prepare("SELECT * FROM webhooks WHERE token = ?"); + $stmt->execute([$token]); + $webhook = $stmt->fetch(); - echo json_encode(['success' => true]); -} catch (Exception $e) { - http_response_code(500); - echo json_encode(['success' => false, 'error' => $e->getMessage()]); + 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 { + $stmt = db()->prepare("INSERT INTO messages (channel_id, user_id, content) VALUES (?, ?, ?)"); + $stmt->execute([$webhook['channel_id'], 1, $content]); // 1 is system/bot user + echo json_encode(['success' => true]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } + exit; +} + +// Manage webhooks (session needed) +requireLogin(); +$user_id = $_SESSION['user_id']; + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + $server_id = $_GET['server_id'] ?? 0; + $stmt = db()->prepare(" + SELECT w.*, c.name as channel_name + FROM webhooks w + JOIN channels c ON w.channel_id = c.id + WHERE c.server_id = ? + "); + $stmt->execute([$server_id]); + $webhooks = $stmt->fetchAll(); + echo json_encode(['success' => true, 'webhooks' => $webhooks]); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $data = json_decode(file_get_contents('php://input'), true); + $channel_id = $data['channel_id'] ?? 0; + $name = $data['name'] ?? 'New Webhook'; + $token = bin2hex(random_bytes(16)); + + try { + $stmt = db()->prepare("INSERT INTO webhooks (channel_id, name, token) VALUES (?, ?, ?)"); + $stmt->execute([$channel_id, $name, $token]); + echo json_encode(['success' => true, 'webhook' => ['id' => db()->lastInsertId(), 'token' => $token]]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } + exit; +} + +if ($_SERVER['REQUEST_METHOD'] === 'DELETE') { + $data = json_decode(file_get_contents('php://input'), true); + $id = $data['id'] ?? 0; + try { + $stmt = db()->prepare("DELETE FROM webhooks WHERE id = ?"); + $stmt->execute([$id]); + echo json_encode(['success' => true]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } + exit; } diff --git a/assets/css/discord.css b/assets/css/discord.css index 1b3c936..b4cc879 100644 --- a/assets/css/discord.css +++ b/assets/css/discord.css @@ -585,6 +585,28 @@ body { border: 1px solid rgba(255,255,255,0.1); } +.video-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 10px; + padding: 10px; + background: #000; + width: 100%; +} + +.video-grid video { + width: 100%; + aspect-ratio: 16/9; + background: #2b2d31; + border-radius: 8px; + object-fit: cover; +} + +.voice-controls { + border-top: 1px solid rgba(255,255,255,0.05); + background-color: #232428 !important; +} + /* Roles Management */ #roles-list .list-group-item:hover { background-color: rgba(255, 255, 255, 0.05) !important; @@ -627,3 +649,17 @@ body { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } + +.channel-item-container:hover .channel-settings-btn { + opacity: 1 !important; +} + +.channel-settings-btn { + opacity: 0; + transition: opacity 0.2s; + padding: 2px; +} + +.channel-settings-btn:hover { + color: var(--text-normal) !important; +} diff --git a/assets/js/main.js b/assets/js/main.js index 651f0cc..473a319 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -362,6 +362,18 @@ document.addEventListener('DOMContentLoaded', () => { } }); + // Roles Management + const channelSettingsBtns = document.querySelectorAll('.channel-settings-btn'); + channelSettingsBtns.forEach(btn => { + btn.addEventListener('click', () => { + const modal = document.getElementById('editChannelModal'); + modal.querySelector('#edit-channel-id').value = btn.dataset.id; + modal.querySelector('#edit-channel-name').value = btn.dataset.name; + modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1'; + modal.querySelector('#delete-channel-id').value = btn.dataset.id; + }); + }); + // Roles Management const rolesTabBtn = document.getElementById('roles-tab-btn'); const rolesList = document.getElementById('roles-list'); @@ -442,6 +454,84 @@ document.addEventListener('DOMContentLoaded', () => { } }); + // Webhooks Management + const webhooksTabBtn = document.getElementById('webhooks-tab-btn'); + const webhooksList = document.getElementById('webhooks-list'); + const addWebhookBtn = document.getElementById('add-webhook-btn'); + + webhooksTabBtn?.addEventListener('click', loadWebhooks); + + async function loadWebhooks() { + webhooksList.innerHTML = '
Loading webhooks...
'; + try { + const resp = await fetch(`api_v1_webhook.php?server_id=${activeServerId}`); + const data = await resp.json(); + if (data.success) { + renderWebhooks(data.webhooks); + } + } catch (e) { console.error(e); } + } + + function renderWebhooks(webhooks) { + webhooksList.innerHTML = ''; + if (webhooks.length === 0) { + webhooksList.innerHTML = '
No webhooks found.
'; + return; + } + webhooks.forEach(wh => { + const item = document.createElement('div'); + item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-2'; + const url = `${window.location.origin}/api_v1_webhook.php?token=${wh.token}`; + item.innerHTML = ` +
+ ${wh.name} + +
+
Channel: #${wh.channel_name}
+
+ + +
+ `; + webhooksList.appendChild(item); + }); + } + + addWebhookBtn?.addEventListener('click', async () => { + const name = prompt('Webhook name:', 'Bot Name'); + if (!name) return; + + // Fetch channels for this server to let user pick one + const respChannels = await fetch(`api_v1_channels.php?server_id=${activeServerId}`); + const dataChannels = await respChannels.json(); + if (!dataChannels.length) return alert('Create a channel first.'); + + const channelId = prompt('Enter Channel ID:\n' + dataChannels.map(c => `${c.id}: #${c.name}`).join('\n')); + if (!channelId) return; + + try { + const resp = await fetch('api_v1_webhook.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ channel_id: channelId, name: name }) + }); + if ((await resp.json()).success) loadWebhooks(); + } catch (e) { console.error(e); } + }); + + webhooksList?.addEventListener('click', async (e) => { + if (e.target.classList.contains('delete-webhook-btn')) { + if (!confirm('Delete this webhook?')) return; + const whId = e.target.dataset.id; + const resp = await fetch('api_v1_webhook.php', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: whId }) + }); + if ((await resp.json()).success) loadWebhooks(); + } + }); + // Server Settings const searchServerIconBtn = document.getElementById('search-server-icon-btn'); const serverIconResults = document.getElementById('server-icon-search-results'); diff --git a/assets/js/voice.js b/assets/js/voice.js index 40060ec..ab90396 100644 --- a/assets/js/voice.js +++ b/assets/js/voice.js @@ -2,9 +2,11 @@ class VoiceChannel { constructor(ws) { this.ws = ws; this.localStream = null; + this.screenStream = null; this.peers = {}; // userId -> RTCPeerConnection this.participants = {}; // userId -> username this.currentChannelId = null; + this.isScreenSharing = false; } async join(channelId) { @@ -32,9 +34,80 @@ class VoiceChannel { } } + async toggleScreenShare() { + if (!this.currentChannelId) return; + + if (this.isScreenSharing) { + this.stopScreenShare(); + } else { + try { + this.screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true }); + this.isScreenSharing = true; + + const videoTrack = this.screenStream.getVideoTracks()[0]; + videoTrack.onended = () => this.stopScreenShare(); + + // Replace or add track to all peers + Object.values(this.peers).forEach(pc => { + pc.addTrack(videoTrack, this.screenStream); + // Renegotiate + this.renegotiate(pc); + }); + + this.updateVoiceUI(); + this.showLocalVideo(); + } catch (e) { + console.error('Failed to share screen:', e); + } + } + } + + stopScreenShare() { + if (this.screenStream) { + this.screenStream.getTracks().forEach(track => track.stop()); + this.screenStream = null; + } + this.isScreenSharing = false; + + // Remove video track from all peers + Object.entries(this.peers).forEach(([userId, pc]) => { + const senders = pc.getSenders(); + const videoSender = senders.find(s => s.track && s.track.kind === 'video'); + if (videoSender) { + pc.removeTrack(videoSender); + this.renegotiate(pc); + } + }); + + this.updateVoiceUI(); + const localVideo = document.getElementById('local-video-container'); + if (localVideo) localVideo.innerHTML = ''; + } + + renegotiate(pc) { + // Find which user this PC belongs to + const userId = Object.keys(this.peers).find(key => this.peers[key] === pc); + if (!userId) return; + + pc.createOffer().then(offer => { + return pc.setLocalDescription(offer); + }).then(() => { + this.ws.send(JSON.stringify({ + type: 'voice_offer', + to: userId, + from: window.currentUserId, + username: window.currentUsername, + offer: pc.localDescription, + channel_id: this.currentChannelId + })); + }); + } + leave() { if (!this.currentChannelId) return; + this.stopScreenShare(); + this.ws.send(JSON.stringify({ type: 'voice_leave', channel_id: this.currentChannelId, @@ -85,6 +158,8 @@ class VoiceChannel { } delete this.participants[from]; this.updateVoiceUI(); + const remoteVideo = document.getElementById(`remote-video-${from}`); + if (remoteVideo) remoteVideo.remove(); break; } } @@ -98,9 +173,17 @@ class VoiceChannel { this.peers[userId] = pc; - this.localStream.getTracks().forEach(track => { - pc.addTrack(track, this.localStream); - }); + if (this.localStream) { + this.localStream.getTracks().forEach(track => { + pc.addTrack(track, this.localStream); + }); + } + + if (this.screenStream) { + this.screenStream.getTracks().forEach(track => { + pc.addTrack(track, this.screenStream); + }); + } pc.onicecandidate = (event) => { if (event.candidate) { @@ -115,9 +198,13 @@ class VoiceChannel { }; pc.ontrack = (event) => { - const remoteAudio = new Audio(); - remoteAudio.srcObject = event.streams[0]; - remoteAudio.play(); + if (event.track.kind === 'audio') { + const remoteAudio = new Audio(); + remoteAudio.srcObject = event.streams[0]; + remoteAudio.play(); + } else if (event.track.kind === 'video') { + this.handleRemoteVideo(userId, event.streams[0]); + } }; if (isOfferor) { @@ -162,6 +249,57 @@ class VoiceChannel { if (pc) await pc.addIceCandidate(new RTCIceCandidate(candidate)); } + handleRemoteVideo(userId, stream) { + let container = document.getElementById('video-grid'); + if (!container) { + container = document.createElement('div'); + container.id = 'video-grid'; + container.className = 'video-grid'; + const chatContainer = document.querySelector('.chat-container'); + if (chatContainer) { + chatContainer.insertBefore(container, document.getElementById('messages-list')); + } else { + document.body.appendChild(container); + } + } + + let video = document.getElementById(`remote-video-${userId}`); + if (!video) { + video = document.createElement('video'); + video.id = `remote-video-${userId}`; + video.autoplay = true; + video.playsinline = true; + container.appendChild(video); + } + video.srcObject = stream; + } + + showLocalVideo() { + let container = document.getElementById('video-grid'); + if (!container) { + container = document.createElement('div'); + container.id = 'video-grid'; + container.className = 'video-grid'; + const chatContainer = document.querySelector('.chat-container'); + if (chatContainer) { + chatContainer.insertBefore(container, document.getElementById('messages-list')); + } else { + document.body.appendChild(container); + } + } + + let video = document.getElementById('local-video'); + if (!video) { + video = document.createElement('video'); + video.id = 'local-video'; + video.autoplay = true; + video.playsinline = true; + video.muted = true; + container.appendChild(video); + } + video.srcObject = this.screenStream; + } + updateVoiceUI() { document.querySelectorAll('.voice-users-list').forEach(el => el.innerHTML = ''); @@ -183,6 +321,35 @@ class VoiceChannel { this.addVoiceUserToUI(listEl, uid, name); }); } + + // Show voice controls if not already there + if (!document.querySelector('.voice-controls')) { + const controls = document.createElement('div'); + controls.className = 'voice-controls p-2 d-flex justify-content-between align-items-center border-top bg-dark'; + controls.innerHTML = ` +
+
+
Voice Connected
+
+
+ + +
+ `; + document.querySelector('.channels-sidebar').appendChild(controls); + + document.getElementById('btn-screen-share').onclick = () => this.toggleScreenShare(); + document.getElementById('btn-voice-leave').onclick = () => this.leave(); + } + } else { + const controls = document.querySelector('.voice-controls'); + if (controls) controls.remove(); + const grid = document.getElementById('video-grid'); + if (grid) grid.remove(); } } diff --git a/includes/ai_filtering.php b/includes/ai_filtering.php new file mode 100644 index 0000000..806c0e6 --- /dev/null +++ b/includes/ai_filtering.php @@ -0,0 +1,23 @@ + true]; + + $resp = LocalAIApi::createResponse([ + 'input' => [ + ['role' => 'system', 'content' => 'You are a content moderator. Analyze the message and return a JSON object with "is_safe" (boolean) and "reason" (string, optional). Safe means no hate speech, extreme violence, or explicit sexual content.'], + ['role' => 'user', 'content' => $content], + ], + ]); + + if (!empty($resp['success'])) { + $result = LocalAIApi::decodeJsonFromResponse($resp); + if ($result && isset($result['is_safe'])) { + return $result; + } + } + + // Default to safe if AI fails, to avoid blocking users + return ['is_safe' => true]; +} diff --git a/index.php b/index.php index bb488e7..dd2cf6f 100644 --- a/index.php +++ b/index.php @@ -180,10 +180,21 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; + - - - +
+ + + + + + + + +
@@ -191,8 +202,19 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; +
-
- +
+
+ +
+ + + + +
@@ -316,12 +338,27 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+
- - + + + + +
+ +
+
@@ -407,6 +444,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; +
@@ -462,6 +502,15 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+
+
+
Webhooks
+ +
+
+ +
+
@@ -532,6 +581,10 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; +
+ + +
+ + +