V4
This commit is contained in:
parent
c49abcc049
commit
0911f86785
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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
23
includes/ai_filtering.php
Normal 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
111
index.php
@ -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; ?>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user