This commit is contained in:
Flatlogic Bot 2026-02-15 11:01:34 +00:00
parent c49abcc049
commit 0911f86785
8 changed files with 575 additions and 58 deletions

View File

@ -1,24 +1,75 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$server_id = $_GET['server_id'] ?? 0;
if (!$server_id) {
echo json_encode([]);
exit;
}
$stmt = db()->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);

View File

@ -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);

View File

@ -1,44 +1,84 @@
<?php
header('Content-Type: application/json');
require_once 'db/config.php';
require_once 'auth/session.php';
$token = $_GET['token'] ?? '';
$data = json_decode(file_get_contents('php://input'), true);
$content = $data['content'] ?? '';
$username = $data['username'] ?? null;
if (empty($token)) {
http_response_code(401);
echo json_encode(['success' => 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;
}

View File

@ -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;
}

View File

@ -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 = '<div class="text-center p-3 text-muted">Loading webhooks...</div>';
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 = '<div class="text-center p-3 text-muted">No webhooks found.</div>';
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 = `
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold">${wh.name}</span>
<button class="btn btn-sm btn-outline-danger delete-webhook-btn" data-id="${wh.id}">×</button>
</div>
<div class="small text-muted mb-2">Channel: #${wh.channel_name}</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control bg-dark text-white border-secondary" value="${url}" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="navigator.clipboard.writeText('${url}')">Copy</button>
</div>
`;
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');

View File

@ -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 = `
<div class="d-flex align-items-center">
<div class="voice-status-icon text-success me-2"></div>
<div class="small">Voice Connected</div>
</div>
<div>
<button class="btn btn-sm btn-outline-light me-2" id="btn-screen-share">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
</button>
<button class="btn btn-sm btn-danger" id="btn-voice-leave">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"></path><line x1="23" y1="1" x2="1" y2="23"></line></svg>
</button>
</div>
`;
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();
}
}

23
includes/ai_filtering.php Normal file
View File

@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/../ai/LocalAIApi.php';
function moderateContent($content) {
if (empty(trim($content))) return ['is_safe' => 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];
}

111
index.php
View File

@ -180,10 +180,21 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="text">+</span>
</div>
<?php foreach($channels as $c): if($c['type'] !== 'text') continue; ?>
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $c['id']; ?>"
class="channel-item <?php echo $c['id'] == $active_channel_id ? 'active' : ''; ?>">
<?php echo htmlspecialchars($c['name']); ?>
</a>
<div class="channel-item-container d-flex align-items-center">
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $c['id']; ?>"
class="channel-item flex-grow-1 <?php echo $c['id'] == $active_channel_id ? 'active' : ''; ?>">
<?php echo htmlspecialchars($c['name']); ?>
</a>
<?php if ($is_owner): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $c['id']; ?>"
data-name="<?php echo htmlspecialchars($c['name']); ?>"
data-files="<?php echo $c['allow_file_sharing']; ?>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
<div class="channel-category" style="margin-top: 16px;">
@ -191,8 +202,19 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="voice">+</span>
</div>
<?php foreach($channels as $c): if($c['type'] !== 'voice') continue; ?>
<div class="channel-item voice-item" data-channel-id="<?php echo $c['id']; ?>">
<?php echo htmlspecialchars($c['name']); ?>
<div class="channel-item-container d-flex align-items-center">
<div class="channel-item voice-item flex-grow-1" data-channel-id="<?php echo $c['id']; ?>">
<?php echo htmlspecialchars($c['name']); ?>
</div>
<?php if ($is_owner): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $c['id']; ?>"
data-name="<?php echo htmlspecialchars($c['name']); ?>"
data-files="<?php echo $c['allow_file_sharing']; ?>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
@ -316,12 +338,27 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
<div id="typing-indicator" class="typing-indicator"></div>
<div class="chat-input-container">
<?php
$allow_files = true;
foreach($channels as $c) {
if($c['id'] == $active_channel_id) {
$allow_files = (bool)$c['allow_file_sharing'];
break;
}
}
?>
<form id="chat-form" enctype="multipart/form-data">
<div class="chat-input-wrapper">
<label for="file-upload" class="upload-btn-label" title="Upload File">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
</label>
<input type="file" id="file-upload" style="display: none;">
<?php if ($allow_files): ?>
<label for="file-upload" class="upload-btn-label" title="Upload File">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
</label>
<input type="file" id="file-upload" style="display: none;">
<?php else: ?>
<div class="upload-btn-label disabled" title="File sharing disabled" style="opacity: 0.3; cursor: not-allowed;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="8" x2="16" y2="16"></line><line x1="16" y1="8" x2="8" y2="16"></line></svg>
</div>
<?php endif; ?>
<input type="text" id="chat-input" class="chat-input" placeholder="Message #<?php echo htmlspecialchars($current_channel_name); ?>" autocomplete="off">
</div>
</form>
@ -407,6 +444,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="roles-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-roles" type="button">Roles</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="webhooks-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-webhooks" type="button">Webhooks</button>
</li>
</ul>
<div class="tab-content p-3">
<div class="tab-pane fade show active" id="settings-general">
@ -462,6 +502,15 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<!-- Roles will be loaded here -->
</div>
</div>
<div class="tab-pane fade" id="settings-webhooks">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Webhooks</h6>
<button class="btn btn-sm btn-primary" id="add-webhook-btn">+ Create Webhook</button>
</div>
<div id="webhooks-list" class="list-group list-group-flush bg-transparent">
<!-- Webhooks will be loaded here -->
</div>
</div>
</div>
</div>
</div>
@ -532,6 +581,10 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<input type="text" name="name" class="form-control" placeholder="new-channel" required>
</div>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="add-channel-files" value="1" checked>
<label class="form-check-label text-white" for="add-channel-files">Allow File Sharing</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Cancel</button>
@ -542,6 +595,44 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<!-- Edit Channel Modal -->
<div class="modal fade" id="editChannelModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Channel Settings</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form action="api_v1_channels.php" method="POST">
<input type="hidden" name="action" value="update">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<input type="hidden" name="channel_id" id="edit-channel-id">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Name</label>
<input type="text" name="name" id="edit-channel-name" class="form-control" required>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="edit-channel-files" value="1">
<label class="form-check-label text-white" for="edit-channel-files">Allow File Sharing</label>
<div class="form-text text-muted" style="font-size: 0.8em;">When disabled, users cannot upload files in this channel.</div>
</div>
<button type="submit" class="btn btn-primary w-100 mb-2">Save Changes</button>
</form>
<form action="api_v1_channels.php" method="POST" onsubmit="return confirm('Are you sure you want to delete this channel?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<input type="hidden" name="channel_id" id="delete-channel-id">
<button type="submit" class="btn btn-danger w-100">Delete Channel</button>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
window.currentUserId = <?php echo $current_user_id; ?>;