Autosave: 20260218-155536
This commit is contained in:
parent
05c351eb39
commit
631deb62c1
224
api_v1_voice.php
224
api_v1_voice.php
@ -1,5 +1,4 @@
|
||||
<?php // Vocal secours — WebRTC P2P + signalisation PHP + participants + PTT + VOX (gating via GainNode)
|
||||
// Mutualisé OVH : nécessite ./data écrivable.
|
||||
<?php // Vocal secours — WebRTC P2P + signalisation PHP via DB + participants via DB
|
||||
declare(strict_types=1);
|
||||
require_once "auth/session.php";
|
||||
header("X-Content-Type-Options: nosniff");
|
||||
@ -7,9 +6,6 @@ header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type");
|
||||
|
||||
$DATA_DIR = __DIR__ . "/data";
|
||||
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
|
||||
|
||||
$user = getCurrentUser();
|
||||
$current_user_id = $user ? (int)$user["id"] : 0;
|
||||
|
||||
@ -22,20 +18,6 @@ function peer_id(): string {
|
||||
return bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
function room_log_file(string $room): string {
|
||||
return __DIR__ . "/data/" . $room . ".log";
|
||||
}
|
||||
|
||||
function room_participants_file(string $room): string {
|
||||
return __DIR__ . "/data/" . $room . ".participants.json";
|
||||
}
|
||||
|
||||
function chat_log_file_for_today(): string {
|
||||
// Un fichier par jour : YYYY-MM-DD.chat.log
|
||||
$d = date("Y-m-d");
|
||||
return __DIR__ . "/data/" . $d . ".chat.log";
|
||||
}
|
||||
|
||||
function now_ms(): int {
|
||||
return (int) floor(microtime(true) * 1000);
|
||||
}
|
||||
@ -47,50 +29,6 @@ function json_out($data, int $code = 200): void {
|
||||
exit;
|
||||
}
|
||||
|
||||
function read_json_file(string $path): array {
|
||||
if (!file_exists($path)) return [];
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false || $raw === "") return [];
|
||||
$j = json_decode($raw, true);
|
||||
return is_array($j) ? $j : [];
|
||||
}
|
||||
|
||||
function write_json_file(string $path, array $data): void {
|
||||
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
|
||||
}
|
||||
|
||||
function tail_lines(string $path, int $maxLines = 120): array {
|
||||
if (!is_file($path)) return [];
|
||||
$fp = fopen($path, "rb");
|
||||
if (!$fp) return [];
|
||||
|
||||
$lines = [];
|
||||
fseek($fp, 0, SEEK_END);
|
||||
$pos = ftell($fp);
|
||||
$buffer = "";
|
||||
|
||||
while ($pos > 0 && count($lines) < $maxLines) {
|
||||
$readSize = min($pos, 4096);
|
||||
$pos -= $readSize;
|
||||
fseek($fp, $pos);
|
||||
$chunk = fread($fp, $readSize);
|
||||
$buffer = $chunk . $buffer;
|
||||
|
||||
$chunkLines = explode("\n", $buffer);
|
||||
$buffer = array_shift($chunkLines);
|
||||
|
||||
while (!empty($chunkLines)) {
|
||||
$line = array_pop($chunkLines);
|
||||
if (trim($line) !== "") {
|
||||
array_unshift($lines, trim($line));
|
||||
if (count($lines) >= $maxLines) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose($fp);
|
||||
return $lines;
|
||||
}
|
||||
|
||||
// Logic for signaling
|
||||
$action = $_REQUEST["action"] ?? "";
|
||||
$room = room_id($_REQUEST["room"] ?? "secours");
|
||||
@ -98,76 +36,94 @@ $my_id = $_REQUEST["peer_id"] ?? "";
|
||||
|
||||
if ($action === "join") {
|
||||
$name = $_REQUEST["name"] ?? "User";
|
||||
$p_file = room_participants_file($room);
|
||||
$ps = read_json_file($p_file);
|
||||
|
||||
// Cleanup old participants (> 10s)
|
||||
$stale_time = now_ms() - 10000;
|
||||
foreach ($ps as $id => $p) {
|
||||
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
|
||||
}
|
||||
|
||||
$new_id = peer_id();
|
||||
$ps[$new_id] = [
|
||||
"id" => $new_id,
|
||||
"user_id" => $current_user_id,
|
||||
"name" => $name,
|
||||
"avatar_url" => $user["avatar_url"] ?? "",
|
||||
"last_seen" => now_ms()
|
||||
];
|
||||
write_json_file($p_file, $ps);
|
||||
|
||||
// DB Integration for sidebar
|
||||
if ($current_user_id > 0) {
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id, last_seen) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = ?, last_seen = ?");
|
||||
$stmt->execute([$current_user_id, $room, now_ms(), $room, now_ms()]);
|
||||
} catch (Exception $e) {}
|
||||
$stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id, peer_id, name, last_seen) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = ?, peer_id = ?, name = ?, last_seen = ?");
|
||||
$stmt->execute([$current_user_id, $room, $new_id, $name, now_ms(), $room, $new_id, $name, now_ms()]);
|
||||
} catch (Exception $e) {
|
||||
json_out(["error" => "DB Error: " . $e->getMessage()], 500);
|
||||
}
|
||||
} else {
|
||||
json_out(["error" => "Not logged in"], 401);
|
||||
}
|
||||
|
||||
// Fetch all participants in this room
|
||||
$ps = [];
|
||||
try {
|
||||
$stmt = db()->prepare("SELECT vs.*, u.avatar_url FROM voice_sessions vs JOIN users u ON vs.user_id = u.id WHERE vs.channel_id = ? AND vs.last_seen > ?");
|
||||
$stmt->execute([$room, now_ms() - 7000]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as $row) {
|
||||
$ps[$row["peer_id"]] = [
|
||||
"id" => $row["peer_id"],
|
||||
"user_id" => (int)$row["user_id"],
|
||||
"name" => $row["name"],
|
||||
"avatar_url" => $row["avatar_url"],
|
||||
"last_seen" => (int)$row["last_seen"]
|
||||
];
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
|
||||
json_out(["success" => true, "peer_id" => $new_id, "participants" => $ps]);
|
||||
}
|
||||
|
||||
if ($action === "poll") {
|
||||
if (!$my_id) json_out(["error" => "Missing peer_id"], 400);
|
||||
$p_file = room_participants_file($room);
|
||||
$ps = read_json_file($p_file);
|
||||
|
||||
if (isset($ps[$my_id])) {
|
||||
$ps[$my_id]["last_seen"] = now_ms();
|
||||
}
|
||||
|
||||
$stale_time = now_ms() - 10000;
|
||||
foreach ($ps as $id => $p) {
|
||||
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
|
||||
}
|
||||
write_json_file($p_file, $ps);
|
||||
|
||||
// Update DB last_seen
|
||||
// Update last_seen
|
||||
if ($current_user_id > 0) {
|
||||
try {
|
||||
$stmt = db()->prepare("UPDATE voice_sessions SET last_seen = ? WHERE user_id = ?");
|
||||
$stmt->execute([now_ms(), $current_user_id]);
|
||||
$stmt = db()->prepare("UPDATE voice_sessions SET last_seen = ? WHERE user_id = ? AND peer_id = ?");
|
||||
$stmt->execute([now_ms(), $current_user_id, $my_id]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
// Read signals
|
||||
$log_file = room_log_file($room);
|
||||
$signals = [];
|
||||
if (file_exists($log_file)) {
|
||||
$lines = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$remaining = [];
|
||||
$now = now_ms();
|
||||
foreach ($lines as $line) {
|
||||
$sig = json_decode($line, true);
|
||||
if ($sig && isset($sig["to"]) && $sig["to"] === $my_id) {
|
||||
$signals[] = $sig;
|
||||
} elseif ($sig && ($now - ($sig["time"] ?? 0) < 30000)) {
|
||||
$remaining[] = $line;
|
||||
}
|
||||
// Fetch participants
|
||||
$ps = [];
|
||||
try {
|
||||
$stmt = db()->prepare("SELECT vs.*, u.avatar_url FROM voice_sessions vs JOIN users u ON vs.user_id = u.id WHERE vs.channel_id = ? AND vs.last_seen > ?");
|
||||
$stmt->execute([$room, now_ms() - 7000]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as $row) {
|
||||
$ps[$row["peer_id"]] = [
|
||||
"id" => $row["peer_id"],
|
||||
"user_id" => (int)$row["user_id"],
|
||||
"name" => $row["name"],
|
||||
"avatar_url" => $row["avatar_url"],
|
||||
"last_seen" => (int)$row["last_seen"]
|
||||
];
|
||||
}
|
||||
file_put_contents($log_file, implode("\n", $remaining) . (empty($remaining) ? "" : "\n"), LOCK_EX);
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
|
||||
// Read signals
|
||||
$signals = [];
|
||||
try {
|
||||
$stmt = db()->prepare("SELECT * FROM voice_signals WHERE to_peer_id = ? AND room_id = ?");
|
||||
$stmt->execute([$my_id, $room]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$ids_to_delete = [];
|
||||
foreach ($rows as $row) {
|
||||
$signals[] = [
|
||||
"from" => $row["from_peer_id"],
|
||||
"to" => $row["to_peer_id"],
|
||||
"data" => json_decode($row["data"], true),
|
||||
"time" => (int)$row["created_at"]
|
||||
];
|
||||
$ids_to_delete[] = $row["id"];
|
||||
}
|
||||
if (!empty($ids_to_delete)) {
|
||||
$placeholders = implode(',', array_fill(0, count($ids_to_delete), '?'));
|
||||
db()->prepare("DELETE FROM voice_signals WHERE id IN ($placeholders)")->execute($ids_to_delete);
|
||||
}
|
||||
|
||||
// Occasional cleanup of old signals
|
||||
if (rand(1, 20) === 1) {
|
||||
$stale_sig = now_ms() - 60000;
|
||||
db()->prepare("DELETE FROM voice_signals WHERE created_at < ?")->execute([$stale_sig]);
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
|
||||
json_out(["success" => true, "participants" => $ps, "signals" => $signals]);
|
||||
}
|
||||
@ -178,27 +134,16 @@ if ($action === "signal") {
|
||||
$data = $_REQUEST["data"] ?? "";
|
||||
if (!$to || !$data) json_out(["error" => "Missing to/data"], 400);
|
||||
|
||||
$sig = [
|
||||
"from" => $my_id,
|
||||
"to" => $to,
|
||||
"data" => json_decode($data, true),
|
||||
"time" => now_ms()
|
||||
];
|
||||
|
||||
file_put_contents(room_log_file($room), json_encode($sig) . "\n", FILE_APPEND | LOCK_EX);
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO voice_signals (room_id, from_peer_id, to_peer_id, data, created_at) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$room, $my_id, $to, $data, now_ms()]);
|
||||
} catch (Exception $e) {
|
||||
json_out(["error" => "Signal Error: " . $e->getMessage()], 500);
|
||||
}
|
||||
json_out(["success" => true]);
|
||||
}
|
||||
|
||||
if ($action === "list_all") {
|
||||
// Periodic cleanup of the DB table (stale sessions > 15s)
|
||||
if (rand(1, 10) === 1) {
|
||||
try {
|
||||
$stale_db_time = now_ms() - 15000;
|
||||
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?");
|
||||
$stmt->execute([$stale_db_time]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("
|
||||
SELECT vs.channel_id, vs.user_id, u.username, u.display_name, u.avatar_url
|
||||
@ -206,7 +151,7 @@ if ($action === "list_all") {
|
||||
JOIN users u ON vs.user_id = u.id
|
||||
WHERE vs.last_seen > ?
|
||||
");
|
||||
$stale_db_time = now_ms() - 15000;
|
||||
$stale_db_time = now_ms() - 30000;
|
||||
$stmt->execute([$stale_db_time]);
|
||||
$sessions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@ -222,12 +167,11 @@ if ($action === "list_all") {
|
||||
|
||||
if ($action === "leave") {
|
||||
if ($my_id) {
|
||||
$p_file = room_participants_file($room);
|
||||
$ps = read_json_file($p_file);
|
||||
unset($ps[$my_id]);
|
||||
write_json_file($p_file, $ps);
|
||||
}
|
||||
if ($current_user_id > 0) {
|
||||
try {
|
||||
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE peer_id = ?");
|
||||
$stmt->execute([$my_id]);
|
||||
} catch (Exception $e) {}
|
||||
} elseif ($current_user_id > 0) {
|
||||
try {
|
||||
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE user_id = ?");
|
||||
$stmt->execute([$current_user_id]);
|
||||
@ -236,4 +180,4 @@ if ($action === "leave") {
|
||||
json_out(["success" => true]);
|
||||
}
|
||||
|
||||
json_out(["error" => "Unknown action"], 404);
|
||||
json_out(["error" => "Unknown action"], 404);
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileUpload = document.getElementById('file-upload');
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const messagesList = document.getElementById('messages-list');
|
||||
const typingIndicator = document.getElementById('typing-indicator');
|
||||
|
||||
function scrollToBottom(force = false) {
|
||||
// Scroll to bottom helper
|
||||
const messagesList = document.getElementById('messages-list');
|
||||
if (!messagesList) return;
|
||||
|
||||
// Smart scroll: only scroll if user is already at the bottom or if forced (e.g. sending a message)
|
||||
@ -20,8 +15,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Backup for non-smooth support or rendering delays
|
||||
setTimeout(() => {
|
||||
if (force || messagesList.scrollHeight - messagesList.scrollTop <= messagesList.clientHeight + threshold + 200) {
|
||||
messagesList.scrollTop = messagesList.scrollHeight;
|
||||
const currentList = document.getElementById('messages-list');
|
||||
if (currentList && (force || currentList.scrollHeight - currentList.scrollTop <= currentList.clientHeight + threshold + 200)) {
|
||||
currentList.scrollTop = currentList.scrollHeight;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
@ -444,10 +440,68 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Scroll to bottom
|
||||
scrollToBottom(true);
|
||||
|
||||
const currentChannel = window.activeChannelId || new URLSearchParams(window.location.search).get('channel_id') || 1;
|
||||
const currentThread = new URLSearchParams(window.location.search).get('thread_id');
|
||||
let currentChannel = window.activeChannelId || new URLSearchParams(window.location.search).get('channel_id') || 1;
|
||||
let currentThread = new URLSearchParams(window.location.search).get('thread_id');
|
||||
let typingTimeout;
|
||||
|
||||
async function loadChannel(url, pushState = true) {
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
const html = await resp.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const newChat = doc.querySelector('.chat-container');
|
||||
const newMembers = doc.querySelector('.members-sidebar');
|
||||
const newChannels = doc.querySelector('.channels-list');
|
||||
|
||||
if (newChat) {
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
if (chatContainer) chatContainer.innerHTML = newChat.innerHTML;
|
||||
}
|
||||
if (newMembers) {
|
||||
const membersSidebar = document.querySelector('.members-sidebar');
|
||||
if (membersSidebar) {
|
||||
membersSidebar.innerHTML = newMembers.innerHTML;
|
||||
if (newMembers.classList.contains('hidden')) membersSidebar.classList.add('hidden');
|
||||
else membersSidebar.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
if (newChannels) {
|
||||
const channelsList = document.querySelector('.channels-list');
|
||||
if (channelsList) channelsList.innerHTML = newChannels.innerHTML;
|
||||
}
|
||||
|
||||
// Update state
|
||||
const urlObj = new URL(url, window.location.origin);
|
||||
const params = urlObj.searchParams;
|
||||
currentChannel = params.get('channel_id') || 1;
|
||||
currentThread = params.get('thread_id');
|
||||
window.activeChannelId = currentChannel;
|
||||
|
||||
// Update Title
|
||||
document.title = doc.title;
|
||||
|
||||
// Re-init features
|
||||
findLastMessageId();
|
||||
scrollToBottom(true);
|
||||
|
||||
if (pushState) history.pushState(null, '', url);
|
||||
|
||||
// Re-sync voice UI to ensure green highlight is correct
|
||||
if (window.voiceHandler) {
|
||||
window.voiceHandler.updateVoiceUI();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load channel:', e);
|
||||
window.location.href = url; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
loadChannel(window.location.href, false);
|
||||
});
|
||||
|
||||
// Notification Permission
|
||||
if ("Notification" in window && Notification.permission === "default") {
|
||||
Notification.requestPermission();
|
||||
@ -563,139 +617,160 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}, 1000);
|
||||
|
||||
function showTyping(username) {
|
||||
const typingIndicator = document.getElementById('typing-indicator');
|
||||
if (!typingIndicator) return;
|
||||
typingIndicator.textContent = `${username} is typing...`;
|
||||
clearTimeout(typingTimeout);
|
||||
typingTimeout = setTimeout(() => {
|
||||
if (typingIndicator) typingIndicator.textContent = '';
|
||||
const currentIndicator = document.getElementById('typing-indicator');
|
||||
if (currentIndicator) currentIndicator.textContent = '';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
chatInput?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.id === 'chat-input') {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('chat-form');
|
||||
if (form) form.dispatchEvent(new Event('submit', { cancelable: true }));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('input', (e) => {
|
||||
if (e.target.id === 'chat-input') {
|
||||
const chatInput = e.target;
|
||||
chatInput.style.height = 'auto';
|
||||
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
|
||||
if (chatInput.scrollHeight > 200) {
|
||||
chatInput.style.overflowY = 'auto';
|
||||
} else {
|
||||
chatInput.style.overflowY = 'hidden';
|
||||
}
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'typing',
|
||||
channel_id: currentChannel,
|
||||
user_id: window.currentUserId,
|
||||
username: window.currentUsername
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('submit', (e) => {
|
||||
if (e.target.id === 'chat-form') {
|
||||
e.preventDefault();
|
||||
chatForm?.dispatchEvent(new Event('submit', { cancelable: true }));
|
||||
}
|
||||
});
|
||||
|
||||
chatInput?.addEventListener('input', () => {
|
||||
chatInput.style.height = 'auto';
|
||||
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
|
||||
if (chatInput.scrollHeight > 200) {
|
||||
chatInput.style.overflowY = 'auto';
|
||||
} else {
|
||||
chatInput.style.overflowY = 'hidden';
|
||||
}
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'typing',
|
||||
channel_id: currentChannel,
|
||||
user_id: window.currentUserId,
|
||||
username: window.currentUsername
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
chatForm?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const content = chatInput.value.trim();
|
||||
const file = fileUpload.files[0];
|
||||
if (!content && !file) return;
|
||||
|
||||
chatInput.value = '';
|
||||
chatInput.style.height = '24px';
|
||||
chatInput.style.overflowY = 'hidden';
|
||||
const formData = new FormData();
|
||||
formData.append('content', content);
|
||||
formData.append('channel_id', currentChannel);
|
||||
if (currentThread) {
|
||||
formData.append('thread_id', currentThread);
|
||||
}
|
||||
|
||||
const progressContainer = document.getElementById('upload-progress-container');
|
||||
const progressBar = document.getElementById('upload-progress-bar');
|
||||
const progressPercent = document.getElementById('upload-percentage');
|
||||
const progressFilename = document.getElementById('upload-filename');
|
||||
|
||||
if (file) {
|
||||
formData.append('file', file);
|
||||
fileUpload.value = ''; // Clear file input
|
||||
const form = e.target;
|
||||
const chatInput = form.querySelector('#chat-input');
|
||||
const fileUpload = form.querySelector('#file-upload');
|
||||
|
||||
// Show progress bar
|
||||
progressContainer.style.display = 'block';
|
||||
progressFilename.textContent = `Uploading: ${file.name}`;
|
||||
progressBar.style.width = '0%';
|
||||
progressPercent.textContent = '0%';
|
||||
}
|
||||
const content = chatInput.value.trim();
|
||||
const file = fileUpload.files[0];
|
||||
if (!content && !file) return;
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', 'api_v1_messages.php', true);
|
||||
|
||||
xhr.upload.onprogress = (ev) => {
|
||||
if (ev.lengthComputable && file) {
|
||||
const percent = Math.round((ev.loaded / ev.total) * 100);
|
||||
progressBar.style.width = percent + '%';
|
||||
progressPercent.textContent = percent + '%';
|
||||
chatInput.value = '';
|
||||
chatInput.style.height = '24px';
|
||||
chatInput.style.overflowY = 'hidden';
|
||||
const formData = new FormData();
|
||||
formData.append('content', content);
|
||||
formData.append('channel_id', currentChannel);
|
||||
if (currentThread) {
|
||||
formData.append('thread_id', currentThread);
|
||||
}
|
||||
};
|
||||
|
||||
const progressContainer = document.getElementById('upload-progress-container');
|
||||
const progressBar = document.getElementById('upload-progress-bar');
|
||||
const progressPercent = document.getElementById('upload-percentage');
|
||||
const progressFilename = document.getElementById('upload-filename');
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
const result = JSON.parse(xhr.responseText);
|
||||
if (result.success) {
|
||||
appendMessage(result.message);
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'message',
|
||||
data: JSON.stringify({
|
||||
...result.message,
|
||||
channel_id: currentChannel
|
||||
})
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
alert(result.error || 'Failed to send message');
|
||||
if (file) {
|
||||
formData.append('file', file);
|
||||
fileUpload.value = ''; // Clear file input
|
||||
|
||||
// Show progress bar
|
||||
if (progressContainer) progressContainer.style.display = 'block';
|
||||
if (progressFilename) progressFilename.textContent = `Uploading: ${file.name}`;
|
||||
if (progressBar) progressBar.style.width = '0%';
|
||||
if (progressPercent) progressPercent.textContent = '0%';
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', 'api_v1_messages.php', true);
|
||||
|
||||
xhr.upload.onprogress = (ev) => {
|
||||
if (ev.lengthComputable && file) {
|
||||
const percent = Math.round((ev.loaded / ev.total) * 100);
|
||||
if (progressBar) progressBar.style.width = percent + '%';
|
||||
if (progressPercent) progressPercent.textContent = percent + '%';
|
||||
}
|
||||
}
|
||||
progressContainer.style.display = 'none';
|
||||
};
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
console.error('XHR Error');
|
||||
progressContainer.style.display = 'none';
|
||||
alert('An error occurred during the upload.');
|
||||
};
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
const result = JSON.parse(xhr.responseText);
|
||||
if (result.success) {
|
||||
appendMessage(result.message);
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'message',
|
||||
data: JSON.stringify({
|
||||
...result.message,
|
||||
channel_id: currentChannel
|
||||
})
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
alert(result.error || 'Failed to send message');
|
||||
}
|
||||
}
|
||||
if (progressContainer) progressContainer.style.display = 'none';
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
xhr.onerror = () => {
|
||||
console.error('XHR Error');
|
||||
if (progressContainer) progressContainer.style.display = 'none';
|
||||
alert('An error occurred during the upload.');
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Click Events
|
||||
document.addEventListener('click', (e) => {
|
||||
const link = e.target.closest('a');
|
||||
if (link && link.href && link.href.startsWith(window.location.origin) && !link.dataset.bsToggle && !link.dataset.bsTarget && link.target !== '_blank') {
|
||||
const url = new URL(link.href);
|
||||
const targetServerId = url.searchParams.get('server_id');
|
||||
const targetChannelId = url.searchParams.get('channel_id');
|
||||
const targetThreadId = url.searchParams.get('thread_id');
|
||||
|
||||
// If it's a channel or thread within the same server, use AJAX
|
||||
if (targetServerId === activeServerId && (targetChannelId || targetThreadId)) {
|
||||
e.preventDefault();
|
||||
loadChannel(link.href);
|
||||
return;
|
||||
}
|
||||
|
||||
// Also handle DM switching
|
||||
if (targetServerId === 'dms' && activeServerId === 'dms' && targetChannelId) {
|
||||
e.preventDefault();
|
||||
loadChannel(link.href);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Global click at:', e.target);
|
||||
// Voice Channel Click
|
||||
const voiceItem = e.target.closest('.voice-item');
|
||||
if (voiceItem) {
|
||||
e.preventDefault();
|
||||
console.log('Voice item clicked, Channel ID:', voiceItem.dataset.channelId);
|
||||
const channelId = voiceItem.dataset.channelId;
|
||||
if (voiceHandler) {
|
||||
if (voiceHandler.currentChannelId == channelId) {
|
||||
console.log('Already in this channel:', channelId);
|
||||
return;
|
||||
} else {
|
||||
console.log('Joining voice channel:', channelId);
|
||||
voiceHandler.join(channelId);
|
||||
// Update active state in UI
|
||||
document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active'));
|
||||
voiceItem.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
console.error('voiceHandler not initialized');
|
||||
}
|
||||
if (window.voiceHandler) window.voiceHandler.join(channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const badge = e.target.closest('.reaction-badge');
|
||||
if (badge) {
|
||||
const msgId = badge.parentElement.dataset.messageId;
|
||||
@ -993,57 +1068,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
// Global Search
|
||||
const searchInput = document.getElementById('global-search');
|
||||
const searchType = document.getElementById('search-type');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
|
||||
searchInput?.addEventListener('input', async () => {
|
||||
const q = searchInput.value.trim();
|
||||
const type = searchType.value;
|
||||
if (q.length < 2) {
|
||||
searchResults.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&type=${type}&channel_id=${currentChannel}`);
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.success && data.results.length > 0) {
|
||||
searchResults.innerHTML = '';
|
||||
data.results.forEach(res => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'search-result-item d-flex align-items-center gap-2';
|
||||
if (type === 'users') {
|
||||
item.innerHTML = `
|
||||
<div class="message-avatar" style="width: 24px; height: 24px; ${res.avatar_url ? `background-image: url('${res.avatar_url}');` : ''}"></div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="search-result-author">${res.username}</div>
|
||||
<div class="small text-muted" style="font-size: 0.7em;">Click to start conversation</div>
|
||||
</div>
|
||||
`;
|
||||
item.onclick = () => {
|
||||
const formData = new FormData();
|
||||
formData.append('user_id', res.id);
|
||||
fetch('api_v1_dms.php', { method: 'POST', body: formData })
|
||||
.then(r => r.json())
|
||||
.then(resDM => {
|
||||
if (resDM.success) window.location.href = `?server_id=dms&channel_id=${resDM.channel_id}`;
|
||||
});
|
||||
};
|
||||
document.addEventListener('input', async (e) => {
|
||||
if (e.target.id === 'global-search') {
|
||||
const searchInput = e.target;
|
||||
const searchType = document.getElementById('search-type');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
|
||||
const q = searchInput.value.trim();
|
||||
const type = searchType ? searchType.value : 'messages';
|
||||
if (q.length < 2) {
|
||||
if (searchResults) searchResults.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&type=${type}&channel_id=${currentChannel}`);
|
||||
const data = await resp.json();
|
||||
|
||||
if (searchResults) {
|
||||
if (data.success && data.results.length > 0) {
|
||||
searchResults.innerHTML = '';
|
||||
data.results.forEach(res => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'search-result-item d-flex align-items-center gap-2';
|
||||
if (type === 'users') {
|
||||
item.innerHTML = `
|
||||
<div class="message-avatar" style="width: 24px; height: 24px; ${res.avatar_url ? `background-image: url('${res.avatar_url}');` : ''}"></div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="search-result-author">${res.username}</div>
|
||||
<div class="small text-muted" style="font-size: 0.7em;">Click to start conversation</div>
|
||||
</div>
|
||||
`;
|
||||
item.onclick = () => {
|
||||
const formData = new FormData();
|
||||
formData.append('user_id', res.id);
|
||||
fetch('api_v1_dms.php', { method: 'POST', body: formData })
|
||||
.then(r => r.json())
|
||||
.then(resDM => {
|
||||
if (resDM.success) loadChannel(`?server_id=dms&channel_id=${resDM.channel_id}`);
|
||||
});
|
||||
};
|
||||
} else {
|
||||
item.innerHTML = `
|
||||
<div class="flex-grow-1">
|
||||
<div class="search-result-author">${escapeHTML(res.username)}</div>
|
||||
<div class="search-result-text">${parseCustomEmotes(res.content)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
searchResults.appendChild(item);
|
||||
});
|
||||
searchResults.style.display = 'block';
|
||||
} else {
|
||||
item.innerHTML = `
|
||||
<div class="flex-grow-1">
|
||||
<div class="search-result-author">${escapeHTML(res.username)}</div>
|
||||
<div class="search-result-text">${parseCustomEmotes(res.content)}</div>
|
||||
</div>
|
||||
`;
|
||||
searchResults.innerHTML = '<div class="p-2 text-muted">No results found</div>';
|
||||
searchResults.style.display = 'block';
|
||||
}
|
||||
searchResults.appendChild(item);
|
||||
});
|
||||
searchResults.style.display = 'block';
|
||||
} else {
|
||||
searchResults.innerHTML = '<div class="p-2 text-muted">No results found</div>';
|
||||
searchResults.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -2427,17 +2506,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
// Toggle members sidebar
|
||||
const toggleMembersBtn = document.getElementById('toggle-members-btn');
|
||||
const membersSidebar = document.querySelector('.members-sidebar');
|
||||
if (toggleMembersBtn && membersSidebar) {
|
||||
toggleMembersBtn.addEventListener('click', () => {
|
||||
if (window.innerWidth > 992) {
|
||||
membersSidebar.classList.toggle('hidden');
|
||||
} else {
|
||||
membersSidebar.classList.toggle('show');
|
||||
document.addEventListener('click', (e) => {
|
||||
const toggleMembersBtn = e.target.closest('#toggle-members-btn');
|
||||
if (toggleMembersBtn) {
|
||||
const membersSidebar = document.querySelector('.members-sidebar');
|
||||
if (membersSidebar) {
|
||||
if (window.innerWidth > 992) {
|
||||
membersSidebar.classList.toggle('hidden');
|
||||
} else {
|
||||
membersSidebar.classList.toggle('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// User Settings - Save handled in index.php
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ class VoiceChannel {
|
||||
this.speakingUsers = new Set();
|
||||
|
||||
this.setupPTTListeners();
|
||||
window.addEventListener('beforeunload', () => this.leave());
|
||||
window.addEventListener('beforeunload', () => this.leave(true));
|
||||
}
|
||||
|
||||
setupPTTListeners() {
|
||||
@ -79,6 +79,7 @@ class VoiceChannel {
|
||||
}
|
||||
|
||||
this.currentChannelId = channelId;
|
||||
this.updateVoiceUI();
|
||||
|
||||
try {
|
||||
console.log('Requesting microphone access...');
|
||||
@ -102,9 +103,10 @@ class VoiceChannel {
|
||||
|
||||
// Start polling
|
||||
this.startPolling();
|
||||
this.updateVoiceUI();
|
||||
} else {
|
||||
console.error('API join failed:', data.error);
|
||||
this.currentChannelId = null;
|
||||
this.updateVoiceUI();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to join voice:', e);
|
||||
@ -380,12 +382,13 @@ class VoiceChannel {
|
||||
}
|
||||
}
|
||||
|
||||
leave() {
|
||||
leave(isUnloading = false) {
|
||||
if (!this.currentChannelId) return;
|
||||
console.log('Leaving voice channel:', this.currentChannelId);
|
||||
if (this.pollInterval) clearInterval(this.pollInterval);
|
||||
|
||||
fetch(`api_v1_voice.php?action=leave&room=${this.currentChannelId}&peer_id=${this.myPeerId}`);
|
||||
const url = `api_v1_voice.php?action=leave&room=${this.currentChannelId}&peer_id=${this.myPeerId}`;
|
||||
fetch(url);
|
||||
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach(track => track.stop());
|
||||
@ -425,12 +428,23 @@ class VoiceChannel {
|
||||
this.myPeerId = null;
|
||||
this.speakingUsers.clear();
|
||||
this.updateVoiceUI();
|
||||
|
||||
return leavePromise;
|
||||
}
|
||||
|
||||
updateVoiceUI() {
|
||||
// We now use a global update mechanism for all channels
|
||||
VoiceChannel.refreshAllVoiceUsers();
|
||||
|
||||
// Update active class on channel items to reflect current voice connection
|
||||
document.querySelectorAll('.voice-item').forEach(item => {
|
||||
if (this.currentChannelId && item.dataset.channelId == this.currentChannelId) {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
if (this.currentChannelId) {
|
||||
if (!document.querySelector('.voice-controls')) {
|
||||
const controls = document.createElement('div');
|
||||
@ -479,6 +493,18 @@ class VoiceChannel {
|
||||
const resp = await fetch('api_v1_voice.php?action=list_all');
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
// Update active class for local user to ensure UI is in sync
|
||||
if (window.voiceHandler) {
|
||||
const currentChannelId = window.voiceHandler.currentChannelId;
|
||||
document.querySelectorAll('.voice-item').forEach(item => {
|
||||
if (currentChannelId && item.dataset.channelId == currentChannelId) {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear all lists first
|
||||
document.querySelectorAll('.voice-users-list').forEach(el => el.innerHTML = '');
|
||||
|
||||
|
||||
BIN
assets/pasted-20260218-144808-f4591d31.png
Normal file
BIN
assets/pasted-20260218-144808-f4591d31.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets/pasted-20260218-153932-fac8d7bf.png
Normal file
BIN
assets/pasted-20260218-153932-fac8d7bf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@ -1 +0,0 @@
|
||||
[]
|
||||
@ -1,4 +0,0 @@
|
||||
{"from":"d52b05b241a00981","to":"dab810918717cee5","data":{"type":"voice_speaking","channel_id":"3","user_id":2,"speaking":true},"time":1771423958367}
|
||||
{"from":"d52b05b241a00981","to":"dab810918717cee5","data":{"type":"voice_speaking","channel_id":"3","user_id":2,"speaking":false},"time":1771423959219}
|
||||
{"from":"d52b05b241a00981","to":"dab810918717cee5","data":{"type":"voice_speaking","channel_id":"3","user_id":2,"speaking":true},"time":1771423959434}
|
||||
{"from":"d52b05b241a00981","to":"dab810918717cee5","data":{"type":"voice_speaking","channel_id":"3","user_id":2,"speaking":false},"time":1771423967708}
|
||||
@ -1 +0,0 @@
|
||||
{"d52b05b241a00981":{"id":"d52b05b241a00981","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771423987533},"c96c42356d6a3ee4":{"id":"c96c42356d6a3ee4","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771423988087}}
|
||||
10
data/6.log
10
data/6.log
@ -1,10 +0,0 @@
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"offer","offer":{"type":"offer","sdp":"v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 1705174900585877835 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 AA:37:B9:FA:14:15:DC:34:29:97:DC:55:77:28:8E:74:C0:94:15:08:DF:5B:E9:CC:36:81:E5:D9:5C:49:FB:46\r\na=group:BUNDLE 0\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=audio 9 UDP\/TLS\/RTP\/SAVPF 109 9 0 8 101\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2\/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap-allow-mixed\r\na=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\na=fmtp:101 0-15\r\na=ice-pwd:075627a25c9f07d3908305504803057d\r\na=ice-ufrag:03d0add1\r\na=mid:0\r\na=msid:{7a2c5afc-d053-43fd-9196-3caf4510feb3} {8439b857-ead8-4cb1-8111-cb3400348461}\r\na=rtcp-mux\r\na=rtpmap:109 opus\/48000\/2\r\na=rtpmap:9 G722\/8000\/1\r\na=rtpmap:0 PCMU\/8000\r\na=rtpmap:8 PCMA\/8000\r\na=rtpmap:101 telephone-event\/8000\r\na=setup:actpass\r\na=ssrc:1663488241 cname:{7dcb8038-2b50-4e42-827c-c4617a225c01}\r\n"}},"time":1771336456644}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 1 UDP 2122252543 192.168.26.26 62572 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456645}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 1 TCP 2105524479 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456647}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 2 TCP 2105524478 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456652}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 2 UDP 2122252542 192.168.26.26 62573 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456654}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052351 78.246.210.10 31184 typ srflx raddr 192.168.26.26 rport 62572","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456720}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052350 78.246.210.10 31185 typ srflx raddr 192.168.26.26 rport 62573","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456757}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052863 78.246.210.10 31184 typ srflx raddr 192.168.26.26 rport 62572","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456776}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052862 78.246.210.10 31185 typ srflx raddr 192.168.26.26 rport 62573","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456802}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456805}
|
||||
@ -1 +0,0 @@
|
||||
{"0cce3619c0f299fa":{"id":"0cce3619c0f299fa","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771336452806},"75b3938d276e81e1":{"id":"75b3938d276e81e1","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771336462293}}
|
||||
@ -1 +0,0 @@
|
||||
{"0fbf720bc2f110c0":{"id":"0fbf720bc2f110c0","name":"AI","last_seen":1771336229774}}
|
||||
17
db/migrations/20260218_voice_db_signaling.sql
Normal file
17
db/migrations/20260218_voice_db_signaling.sql
Normal file
@ -0,0 +1,17 @@
|
||||
-- Migration to move voice signaling and participants to the database
|
||||
ALTER TABLE voice_sessions ADD COLUMN peer_id VARCHAR(16) NOT NULL;
|
||||
ALTER TABLE voice_sessions ADD COLUMN name VARCHAR(50);
|
||||
-- Reset voice_sessions as we changed schema
|
||||
TRUNCATE TABLE voice_sessions;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS voice_signals (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
room_id VARCHAR(50) NOT NULL,
|
||||
from_peer_id VARCHAR(16) NOT NULL,
|
||||
to_peer_id VARCHAR(16) NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
INDEX (to_peer_id),
|
||||
INDEX (room_id),
|
||||
INDEX (created_at)
|
||||
);
|
||||
@ -329,8 +329,9 @@ if ($is_dm_view) {
|
||||
SELECT vs.channel_id, vs.user_id, u.username, u.display_name, u.avatar_url
|
||||
FROM voice_sessions vs
|
||||
JOIN users u ON vs.user_id = u.id
|
||||
WHERE vs.last_seen > ?
|
||||
");
|
||||
$stmt_vs->execute();
|
||||
$stmt_vs->execute([ (int)floor(microtime(true) * 1000) - 60000 ]); // 60s grace period
|
||||
$voice_sessions = $stmt_vs->fetchAll();
|
||||
$voice_users_by_channel = [];
|
||||
foreach($voice_sessions as $vs) {
|
||||
|
||||
49
requests.log
49
requests.log
@ -764,3 +764,52 @@
|
||||
2026-02-18 13:54:26 - GET /index.php?server_id=1 - POST: []
|
||||
2026-02-18 13:54:43 - GET /index.php?server_id=1 - POST: []
|
||||
2026-02-18 14:13:01 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 14:14:21 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 14:18:52 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 14:31:53 - GET / - POST: []
|
||||
2026-02-18 14:31:56 - HEAD / - POST: []
|
||||
2026-02-18 14:32:15 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 14:45:08 - GET /index.php?server_id=1 - POST: []
|
||||
2026-02-18 14:45:20 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 14:45:32 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-18 14:45:42 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 14:46:15 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 14:46:49 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 14:46:51 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 14:49:47 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 14:51:06 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 14:51:16 - GET /index.php - POST: []
|
||||
2026-02-18 14:51:45 - GET /index.php - POST: []
|
||||
2026-02-18 14:51:46 - GET /index.php - POST: []
|
||||
2026-02-18 14:52:18 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-18 14:52:43 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 14:57:03 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 15:28:39 - GET / - POST: []
|
||||
2026-02-18 15:28:47 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 15:28:56 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 15:29:22 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 15:29:27 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-18 15:29:34 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-18 15:29:38 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-18 15:29:41 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-18 15:30:03 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-18 15:36:16 - GET / - POST: []
|
||||
2026-02-18 15:36:35 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 15:38:12 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-18 15:38:19 - GET /index.php - POST: []
|
||||
2026-02-18 15:44:09 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 15:45:23 - GET / - POST: []
|
||||
2026-02-18 15:46:47 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 15:48:17 - GET /index.php - POST: []
|
||||
2026-02-18 15:48:20 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-18 15:48:23 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-18 15:48:26 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-18 15:48:37 - GET /index.php - POST: []
|
||||
2026-02-18 15:48:39 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-18 15:48:54 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-18 15:48:55 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-18 15:50:35 - GET / - POST: []
|
||||
2026-02-18 15:51:01 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-18 15:54:51 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-18 15:54:55 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-18 15:55:21 - GET /?fl_project=38443 - POST: []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user