Autosave: 20260217-122132

This commit is contained in:
Flatlogic Bot 2026-02-17 12:21:32 +00:00
parent 920e26ada3
commit d8c5bbb218
10 changed files with 625 additions and 261 deletions

View File

@ -26,10 +26,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$dnd_mode = isset($_POST['dnd_mode']) ? (int)$_POST['dnd_mode'] : 0;
$sound_notifications = isset($_POST['sound_notifications']) ? (int)$_POST['sound_notifications'] : 0;
$theme = !empty($_POST['theme']) ? $_POST['theme'] : $user['theme'];
$voice_mode = !empty($_POST['voice_mode']) ? $_POST['voice_mode'] : ($user['voice_mode'] ?? 'vox');
$voice_ptt_key = !empty($_POST['voice_ptt_key']) ? $_POST['voice_ptt_key'] : ($user['voice_ptt_key'] ?? 'v');
$voice_vox_threshold = isset($_POST['voice_vox_threshold']) ? (float)$_POST['voice_vox_threshold'] : ($user['voice_vox_threshold'] ?? 0.1);
try {
$stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ? WHERE id = ?");
$success = $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $user['id']]);
$stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ?, voice_mode = ?, voice_ptt_key = ?, voice_vox_threshold = ? WHERE id = ?");
$success = $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $voice_mode, $voice_ptt_key, $voice_vox_threshold, $user['id']]);
$log['db_success'] = $success;
file_put_contents('requests.log', json_encode($log) . "\n", FILE_APPEND);

44
api_v1_voice.php Normal file
View File

@ -0,0 +1,44 @@
<?php
require_once 'auth/session.php';
header('Content-Type: application/json');
$user = getCurrentUser();
if (!$user) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$channel_id = $_POST['channel_id'] ?? null;
if ($action === 'join' && $channel_id) {
$stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE channel_id = ?");
$stmt->execute([$user['id'], $channel_id, $channel_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'leave') {
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE user_id = ?");
$stmt->execute([$user['id']]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Invalid action']);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$action = $_GET['action'] ?? '';
if ($action === 'sessions') {
$stmt = db()->prepare("
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
");
$stmt->execute();
$sessions = $stmt->fetchAll();
echo json_encode(['success' => true, 'sessions' => $sessions]);
exit;
}
}
echo json_encode(['success' => false, 'error' => 'Invalid request']);

View File

@ -962,6 +962,29 @@ body {
background-color: #232428 !important;
}
.voice-status-icon {
animation: voice-pulse 2s infinite;
}
@keyframes voice-pulse {
0% { opacity: 0.4; }
50% { opacity: 1; }
100% { opacity: 0.4; }
}
[data-theme="light"] .voice-controls {
background-color: #ebedef !important;
border-top: 1px solid rgba(0,0,0,0.05);
}
.form-range::-webkit-slider-thumb {
background: var(--blurple);
}
.form-range::-moz-range-thumb {
background: var(--blurple);
}
/* Roles Management */
#roles-list .list-group-item:hover {
background-color: var(--separator-soft) !important;
@ -1344,3 +1367,49 @@ body {
[data-theme="light"] .message-text h3 {
color: #313338;
}
/* Voice System */
.voice-users-list {
margin-top: 2px;
}
.voice-user {
padding: 2px 0;
cursor: default;
}
.voice-user .message-avatar {
background-size: cover;
background-position: center;
border-radius: 50%;
background-color: var(--bg-servers);
}
.voice-controls {
margin-top: auto;
background-color: #232428;
z-index: 10;
}
.voice-status-icon {
animation: voice-pulse 2s infinite;
}
@keyframes voice-pulse {
0% { opacity: 0.4; }
50% { opacity: 1; }
100% { opacity: 0.4; }
}
[data-theme="light"] .voice-controls {
background-color: #ebedef;
border-top: 1px solid rgba(0,0,0,0.05);
}
.form-range::-webkit-slider-thumb {
background: var(--blurple);
}
.form-range::-moz-range-thumb {
background: var(--blurple);
}

View File

@ -458,11 +458,25 @@ document.addEventListener('DOMContentLoaded', () => {
let voiceHandler;
function connectWS() {
console.log('Connecting to WebSocket...');
try {
ws = new WebSocket('ws://' + window.location.hostname + ':8080');
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Try port 8080. If it fails due to WSS/WS mismatch or port block, we'll see it in console.
ws = new WebSocket(protocol + '//' + window.location.hostname + ':8080');
ws.onopen = () => {
console.log('WebSocket connected');
ws.send(JSON.stringify({
type: 'presence',
user_id: window.currentUserId,
status: 'online'
}));
};
if (typeof VoiceChannel !== 'undefined') {
voiceHandler = new VoiceChannel(ws);
voiceHandler = new VoiceChannel(ws, window.voiceSettings);
window.voiceHandler = voiceHandler;
console.log('VoiceHandler initialized');
}
ws.onmessage = (e) => {
@ -508,16 +522,12 @@ document.addEventListener('DOMContentLoaded', () => {
updatePresenceUI(msg.user_id, msg.status);
}
};
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'presence',
user_id: window.currentUserId,
status: 'online'
}));
ws.onclose = () => {
console.log('WebSocket connection closed. Reconnecting...');
setTimeout(connectWS, 3000);
};
ws.onclose = () => setTimeout(connectWS, 3000);
} catch (e) {
console.warn('WebSocket connection failed.');
console.warn('WebSocket connection failed:', e);
}
}
connectWS();
@ -653,8 +663,32 @@ document.addEventListener('DOMContentLoaded', () => {
xhr.send(formData);
});
// Handle Reaction Clicks
// Handle Click Events
document.addEventListener('click', (e) => {
// Voice Channel Click
const voiceItem = e.target.closest('.voice-item');
if (voiceItem) {
e.preventDefault();
console.log('Voice item clicked:', voiceItem.dataset.channelId);
const channelId = voiceItem.dataset.channelId;
if (voiceHandler) {
if (voiceHandler.currentChannelId == channelId) {
console.log('Leaving voice channel:', channelId);
voiceHandler.leave();
voiceItem.classList.remove('active');
} 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');
}
return;
}
const badge = e.target.closest('.reaction-badge');
if (badge) {
const msgId = badge.parentElement.dataset.messageId;
@ -730,21 +764,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
// Voice
if (voiceHandler) {
document.querySelectorAll('.voice-item').forEach(item => {
item.addEventListener('click', () => {
const cid = item.dataset.channelId;
if (voiceHandler.currentChannelId == cid) {
voiceHandler.leave();
item.classList.remove('active');
} else {
voiceHandler.join(cid);
document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
}
});
});
// Presence indicators initialization (can be expanded)
if (window.currentUserId) {
// ... (existing presence logic if any)
}
// Message Actions (Edit/Delete)

View File

@ -1,29 +1,90 @@
class VoiceChannel {
constructor(ws) {
constructor(ws, settings) {
this.ws = ws;
this.settings = settings || { mode: 'vox', pttKey: 'v', voxThreshold: 0.1 };
this.localStream = null;
this.screenStream = null;
this.peers = {}; // userId -> RTCPeerConnection
this.participants = {}; // userId -> username
this.participants = {}; // userId -> {username, avatarUrl}
this.currentChannelId = null;
this.isScreenSharing = false;
this.audioContext = null;
this.analyser = null;
this.microphone = null;
this.scriptProcessor = null;
this.isTalking = false;
this.pttPressed = false;
this.voxActive = false;
this.lastVoiceTime = 0;
this.voxHoldTime = 500; // ms to keep open after sound drops below threshold
this.setupPTTListeners();
window.addEventListener('beforeunload', () => this.leave());
}
setupPTTListeners() {
window.addEventListener('keydown', (e) => {
if (this.settings.mode === 'ptt' && e.key.toLowerCase() === this.settings.pttKey.toLowerCase()) {
if (!this.pttPressed) {
this.pttPressed = true;
this.updateMuteState();
}
}
});
window.addEventListener('keyup', (e) => {
if (this.settings.mode === 'ptt' && e.key.toLowerCase() === this.settings.pttKey.toLowerCase()) {
this.pttPressed = false;
this.updateMuteState();
}
});
}
async join(channelId) {
if (this.currentChannelId === channelId) return;
console.log('VoiceChannel.join called for channel:', channelId);
if (this.currentChannelId === channelId) {
console.log('Already in this channel');
return;
}
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.error('WebSocket not connected. State:', this.ws ? this.ws.readyState : 'null');
alert('Unable to join voice: Connection to signaling server not established. Please wait a few seconds and try again.');
return;
}
if (this.currentChannelId) this.leave();
console.log('Joining voice channel:', channelId);
this.currentChannelId = channelId;
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('Microphone access is only available on secure origins (HTTPS).');
}
this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
// Start muted
this.setMute(true);
if (this.settings.mode === 'vox') {
this.setupVOX();
}
// Persist in DB
const fd = new FormData();
fd.append('action', 'join');
fd.append('channel_id', channelId);
fetch('api_v1_voice.php', { method: 'POST', body: fd });
this.ws.send(JSON.stringify({
type: 'voice_join',
channel_id: channelId,
user_id: window.currentUserId,
username: window.currentUsername
username: window.currentUsername,
avatar_url: window.currentAvatarUrl
}));
this.updateVoiceUI();
@ -34,73 +95,78 @@ 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;
setupVOX() {
if (this.audioContext) this.audioContext.close();
// 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.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioContext.createAnalyser();
this.microphone = this.audioContext.createMediaStreamSource(this.localStream);
this.scriptProcessor = this.audioContext.createScriptProcessor(2048, 1, 1);
this.updateVoiceUI();
const localVideo = document.getElementById('local-video-container');
if (localVideo) localVideo.innerHTML = '';
this.analyser.smoothingTimeConstant = 0.8;
this.analyser.fftSize = 1024;
this.microphone.connect(this.analyser);
this.analyser.connect(this.scriptProcessor);
this.scriptProcessor.connect(this.audioContext.destination);
this.scriptProcessor.onaudioprocess = () => {
const array = new Uint8Array(this.analyser.frequencyBinCount);
this.analyser.getByteFrequencyData(array);
let values = 0;
for (let i = 0; i < array.length; i++) {
values += array[i];
}
const average = values / array.length;
const normalized = average / 128; // 0 to 2 approx
if (normalized > this.settings.voxThreshold) {
this.lastVoiceTime = Date.now();
if (!this.voxActive) {
this.voxActive = true;
this.updateMuteState();
}
} else {
if (this.voxActive && Date.now() - this.lastVoiceTime > this.voxHoldTime) {
this.voxActive = false;
this.updateMuteState();
}
}
};
}
renegotiate(pc) {
// Find which user this PC belongs to
const userId = Object.keys(this.peers).find(key => this.peers[key] === pc);
if (!userId) return;
updateMuteState() {
if (!this.currentChannelId || !this.localStream) return;
pc.createOffer().then(offer => {
return pc.setLocalDescription(offer);
}).then(() => {
let shouldTalk = false;
if (this.settings.mode === 'ptt') {
shouldTalk = this.pttPressed;
} else {
shouldTalk = this.voxActive;
}
if (this.isTalking !== shouldTalk) {
this.isTalking = shouldTalk;
this.setMute(!shouldTalk);
// Notify others
this.ws.send(JSON.stringify({
type: 'voice_offer',
to: userId,
from: window.currentUserId,
username: window.currentUsername,
offer: pc.localDescription,
channel_id: this.currentChannelId
type: 'voice_speaking',
channel_id: this.currentChannelId,
user_id: window.currentUserId,
speaking: shouldTalk
}));
});
this.updateSpeakingUI(window.currentUserId, shouldTalk);
}
}
setMute(mute) {
if (this.localStream) {
this.localStream.getAudioTracks().forEach(track => {
track.enabled = !mute;
});
}
}
leave() {
@ -108,6 +174,11 @@ class VoiceChannel {
this.stopScreenShare();
// Persist in DB
const fd = new FormData();
fd.append('action', 'leave');
fetch('api_v1_voice.php', { method: 'POST', body: fd });
this.ws.send(JSON.stringify({
type: 'voice_leave',
channel_id: this.currentChannelId,
@ -119,15 +190,21 @@ class VoiceChannel {
this.localStream = null;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
Object.values(this.peers).forEach(pc => pc.close());
this.peers = {};
this.participants = {};
this.currentChannelId = null;
this.isTalking = false;
this.updateVoiceUI();
}
async handleSignaling(data) {
const { type, from, to, offer, answer, candidate, channel_id, username } = data;
const { type, from, to, offer, answer, candidate, channel_id, username, avatar_url, speaking } = data;
if (channel_id != this.currentChannelId) return;
if (to && to != window.currentUserId) return;
@ -135,13 +212,13 @@ class VoiceChannel {
switch (type) {
case 'voice_join':
if (from != window.currentUserId) {
this.participants[from] = username || `User ${from}`;
this.participants[from] = { username: username || `User ${from}`, avatar_url: avatar_url };
this.createPeerConnection(from, true);
this.updateVoiceUI();
}
break;
case 'voice_offer':
this.participants[from] = username || `User ${from}`;
this.participants[from] = { username: username || `User ${from}`, avatar_url: avatar_url };
await this.handleOffer(from, offer);
this.updateVoiceUI();
break;
@ -151,6 +228,9 @@ class VoiceChannel {
case 'voice_ice_candidate':
await this.handleCandidate(from, candidate);
break;
case 'voice_speaking':
this.updateSpeakingUI(from, speaking);
break;
case 'voice_leave':
if (this.peers[from]) {
this.peers[from].close();
@ -158,8 +238,6 @@ class VoiceChannel {
}
delete this.participants[from];
this.updateVoiceUI();
const remoteVideo = document.getElementById(`remote-video-${from}`);
if (remoteVideo) remoteVideo.remove();
break;
}
}
@ -179,12 +257,6 @@ class VoiceChannel {
});
}
if (this.screenStream) {
this.screenStream.getTracks().forEach(track => {
pc.addTrack(track, this.screenStream);
});
}
pc.onicecandidate = (event) => {
if (event.candidate) {
this.ws.send(JSON.stringify({
@ -202,8 +274,6 @@ class VoiceChannel {
const remoteAudio = new Audio();
remoteAudio.srcObject = event.streams[0];
remoteAudio.play();
} else if (event.track.kind === 'video') {
this.handleRemoteVideo(userId, event.streams[0]);
}
};
@ -216,6 +286,7 @@ class VoiceChannel {
to: userId,
from: window.currentUserId,
username: window.currentUsername,
avatar_url: window.currentAvatarUrl,
offer: pc.localDescription,
channel_id: this.currentChannelId
}));
@ -249,117 +320,80 @@ 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 = '');
// Fetch all sessions to update all channels (or just rely on signaling for current one)
// For simplicity, we update the current channel from participants
if (this.currentChannelId) {
const channelEl = document.querySelector(`.voice-item[data-channel-id="${this.currentChannelId}"]`);
if (channelEl) {
let listEl = channelEl.querySelector('.voice-users-list');
if (!listEl) {
listEl = document.createElement('div');
listEl.className = 'voice-users-list ms-3';
channelEl.appendChild(listEl);
}
const listEls = document.querySelectorAll(`.voice-item[data-channel-id="${this.currentChannelId}"] + .voice-users-list`);
listEls.forEach(listEl => {
// Me
this.addVoiceUserToUI(listEl, window.currentUserId, window.currentUsername);
this.addVoiceUserToUI(listEl, window.currentUserId, window.currentUsername, window.currentAvatarUrl);
// Others
Object.entries(this.participants).forEach(([uid, name]) => {
this.addVoiceUserToUI(listEl, uid, name);
Object.entries(this.participants).forEach(([uid, data]) => {
this.addVoiceUserToUI(listEl, uid, data.username, data.avatar_url);
});
}
});
// Show voice controls if not already there
// Voice controls
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.style.backgroundColor = '#232428';
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 class="voice-status-icon text-success me-2" style="font-size: 8px;"></div>
<div class="small fw-bold" style="font-size: 11px; color: #248046;">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">
<button class="btn btn-sm text-muted" id="btn-voice-leave" title="Disconnect">
<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();
}
}
addVoiceUserToUI(container, userId, username) {
addVoiceUserToUI(container, userId, username, avatarUrl) {
const userEl = document.createElement('div');
userEl.className = 'voice-user small text-muted d-flex align-items-center mb-1';
userEl.className = 'voice-user small d-flex align-items-center mb-1';
userEl.dataset.userId = userId;
userEl.style.paddingLeft = '8px';
userEl.innerHTML = `
<div class="message-avatar me-2" style="width: 16px; height: 16px;"></div>
<span>${username}</span>
<div class="message-avatar me-2" style="width: 18px; height: 18px; border-radius: 50%; transition: box-shadow 0.2s; ${avatarUrl ? `background-image: url('${avatarUrl}');` : ""}"></div>
<span style="color: var(--text-muted); font-size: 13px;">${username}</span>
`;
container.appendChild(userEl);
}
updateSpeakingUI(userId, isSpeaking) {
const userEls = document.querySelectorAll(`.voice-user[data-user-id="${userId}"]`);
userEls.forEach(el => {
const avatar = el.querySelector('.message-avatar');
if (avatar) {
if (isSpeaking) {
avatar.style.boxShadow = '0 0 0 2px #23a559';
} else {
avatar.style.boxShadow = 'none';
}
}
});
}
stopScreenShare() {
// Not requested but kept for compatibility
if (this.screenStream) {
this.screenStream.getTracks().forEach(track => track.stop());
this.screenStream = null;
}
this.isScreenSharing = false;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,12 @@
-- Add voice settings to users and create voice sessions table
ALTER TABLE users ADD COLUMN voice_mode ENUM('vox', 'ptt') DEFAULT 'vox';
ALTER TABLE users ADD COLUMN voice_ptt_key VARCHAR(20) DEFAULT 'v';
ALTER TABLE users ADD COLUMN voice_vox_threshold FLOAT DEFAULT 0.1;
CREATE TABLE IF NOT EXISTS voice_sessions (
user_id INT PRIMARY KEY,
channel_id INT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);

296
index.php
View File

@ -293,6 +293,7 @@ if ($is_dm_view) {
} else {
// Fetch messages
$display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50;
$stmt = db()->prepare("
SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
@ -310,6 +311,19 @@ if ($is_dm_view) {
$current_channel_name = 'general';
foreach($channels as $c) if($c['id'] == $active_channel_id) $current_channel_name = $c['name'];
// Fetch voice sessions for the sidebar
$stmt_vs = db()->prepare("
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
");
$stmt_vs->execute();
$voice_sessions = $stmt_vs->fetchAll();
$voice_users_by_channel = [];
foreach($voice_sessions as $vs) {
$voice_users_by_channel[$vs['channel_id']][] = $vs;
}
// Fetch members
$stmt = db()->prepare("
SELECT u.id, u.display_name as username, u.username as login_name, u.avatar_url, u.status,
@ -468,6 +482,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php
// Helper to render a channel item
function renderChannelItem($c, $active_channel_id, $active_server_id, $can_manage_channels) {
global $voice_users_by_channel;
if ($c['type'] === 'separator') {
?>
<div class="channel-item-container separator-item d-flex align-items-center justify-content-between px-2 py-1" data-id="<?php echo $c['id']; ?>" data-type="separator" style="min-height: 24px;">
@ -515,6 +530,18 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<div class="channel-status small text-muted ms-4" style="font-size: 0.75em; margin-top: -2px;"><?php echo htmlspecialchars($c['status']); ?></div>
<?php endif; ?>
</a>
<?php if ($c['type'] === 'voice'): ?>
<div class="voice-users-list ms-4 mb-1">
<?php if (isset($voice_users_by_channel[$c['id']])): ?>
<?php foreach($voice_users_by_channel[$c['id']] as $v_user): ?>
<div class="voice-user small text-muted d-flex align-items-center mb-1" data-user-id="<?php echo $v_user['user_id']; ?>">
<div class="message-avatar me-2" style="width: 16px; height: 16px; <?php echo $v_user['avatar_url'] ? "background-image: url('{$v_user['avatar_url']}');" : ""; ?>"></div>
<span><?php echo htmlspecialchars($v_user['display_name'] ?? $v_user['username']); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
@ -1090,85 +1117,205 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<!-- User Settings Modal -->
<div class="modal fade" id="userSettingsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">User Settings</h5>
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="background-color: #313338; min-height: 500px;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">User Settings</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="user-settings-form">
<div class="row">
<div class="col-md-4 text-center">
<div class="message-avatar mx-auto mb-3" id="settings-avatar-preview" style="width: 100px; height: 100px; <?php echo $user['avatar_url'] ? "background-image: url('{$user['avatar_url']}');" : ""; ?>"></div>
<input type="hidden" name="avatar_url" id="settings-avatar-url" value="<?php echo htmlspecialchars($user['avatar_url'] ?? ''); ?>">
<p class="small text-muted">Pick an avatar from Pexels or search for one.</p>
</div>
<div class="col-md-8">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">ID connexion</label>
<input type="text" class="form-control" value="<?php echo htmlspecialchars($user['username']); ?>" readonly>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Username display</label>
<input type="text" name="display_name" class="form-control" value="<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>" required>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Settings</label>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="dnd_mode" id="dnd-switch" value="1" <?php echo ($user['dnd_mode'] ?? 0) ? 'checked' : ''; ?>>
<label class="form-check-label text-white" for="dnd-switch">Do Not Disturb</label>
<div class="form-text text-muted" style="font-size: 0.8em;">Mute all desktop notifications.</div>
<div class="modal-body p-0">
<div class="d-flex flex-row h-100" style="min-height: 450px;">
<!-- Settings Sidebar -->
<div class="settings-nav-sidebar p-3 border-end border-secondary" style="width: 200px; background-color: #2b2d31;">
<ul class="nav flex-column nav-pills" id="userSettingsTabs" role="tablist">
<li class="nav-item mb-1">
<button class="nav-link active w-100 text-start text-white border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-profile" type="button">My Profile</button>
</li>
<li class="nav-item mb-1">
<button class="nav-link w-100 text-start text-white border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-appearance" type="button">Appearance</button>
</li>
<li class="nav-item mb-1">
<button class="nav-link w-100 text-start text-white border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-voice" type="button">Voice & Video</button>
</li>
<li class="nav-item mb-1">
<button class="nav-link w-100 text-start text-white border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-notifications" type="button">Notifications</button>
</li>
</ul>
</div>
<!-- Settings Content -->
<div class="flex-grow-1 p-4 overflow-auto custom-scrollbar">
<form id="user-settings-form">
<div class="tab-content" id="userSettingsContent">
<!-- Profile Tab -->
<div class="tab-pane fade show active" id="settings-profile" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">User Profile</h5>
<div class="row align-items-center mb-4 p-3 rounded" style="background-color: #232428;">
<div class="col-md-3 text-center">
<div class="message-avatar mx-auto mb-2" id="settings-avatar-preview" style="width: 80px; height: 80px; <?php echo $user['avatar_url'] ? "background-image: url('{$user['avatar_url']}');" : ""; ?>"></div>
<input type="hidden" name="avatar_url" id="settings-avatar-url" value="<?php echo htmlspecialchars($user['avatar_url'] ?? ''); ?>">
</div>
<div class="col-md-9">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Username display</label>
<input type="text" name="display_name" class="form-control bg-dark text-white border-0" value="<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>" required>
</div>
</div>
</div>
<div class="mb-4">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Search Avatars</label>
<div class="input-group mb-2 shadow-sm">
<input type="text" id="avatar-search-query" class="form-control bg-dark border-0 text-white" placeholder="e.g. cat, abstract, gamer">
<button class="btn btn-primary px-3" type="button" id="search-avatar-btn">Search</button>
</div>
<div id="avatar-results" class="d-flex flex-wrap gap-2 overflow-auto p-2 rounded" style="max-height: 180px; background-color: #1e1f22;">
<div class="w-100 text-center text-muted py-3 small">Search for images to change your avatar.</div>
</div>
</div>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="sound_notifications" id="sound-switch" value="1" <?php echo ($user['sound_notifications'] ?? 0) ? 'checked' : ''; ?>>
<label class="form-check-label text-white" for="sound-switch">Sound Notifications</label>
<div class="form-text text-muted" style="font-size: 0.8em;">Play a sound when you are mentioned.</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Appearance</label>
<div class="row g-2">
<!-- Appearance Tab -->
<div class="tab-pane fade" id="settings-appearance" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Appearance Settings</h5>
<div class="row g-3">
<div class="col-6">
<input type="radio" class="btn-check" name="theme" id="theme-dark" value="dark" <?php echo ($user['theme'] ?? 'dark') == 'dark' ? 'checked' : ''; ?> onchange="document.body.setAttribute('data-theme', 'dark')">
<label class="btn btn-outline-secondary w-100 py-3" for="theme-dark">
<i class="fa-solid fa-moon d-block mb-1"></i>
Dark
<label class="btn btn-outline-secondary w-100 py-4 d-flex flex-column align-items-center" for="theme-dark">
<i class="fa-solid fa-moon mb-2 fs-3"></i>
<span>Dark Theme</span>
</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="theme" id="theme-light" value="light" <?php echo ($user['theme'] ?? 'dark') == 'light' ? 'checked' : ''; ?> onchange="document.body.setAttribute('data-theme', 'light')">
<label class="btn btn-outline-secondary w-100 py-3" for="theme-light">
<i class="fa-solid fa-sun d-block mb-1"></i>
Light
<label class="btn btn-outline-secondary w-100 py-4 d-flex flex-column align-items-center" for="theme-light">
<i class="fa-solid fa-sun mb-2 fs-3"></i>
<span>Light Theme</span>
</label>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Search Avatars</label>
<div class="input-group mb-2">
<input type="text" id="avatar-search-query" class="form-control" placeholder="e.g. cat, abstract, gamer">
<button class="btn btn-outline-secondary" type="button" id="search-avatar-btn">Search</button>
</div>
<div id="avatar-results" class="d-flex flex-wrap gap-2 overflow-auto" style="max-height: 200px;">
<!-- Pexels results here -->
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<!-- Voice Tab -->
<div class="tab-pane fade" id="settings-voice" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Voice Settings</h5>
<div class="p-3 rounded mb-4" style="background-color: #232428;">
<label class="form-label text-uppercase fw-bold mb-3" style="font-size: 0.7em; color: var(--text-muted);">Input Mode</label>
<div class="d-flex gap-3 mb-4">
<div class="form-check custom-radio-card flex-grow-1">
<input class="form-check-input d-none" type="radio" name="voice_mode" id="voice-mode-vox" value="vox" <?php echo ($user['voice_mode'] ?? 'vox') == 'vox' ? 'checked' : ''; ?> onchange="togglePTTSettings('vox')">
<label class="form-check-label w-100 p-3 rounded border border-secondary text-center cursor-pointer" for="voice-mode-vox" style="cursor: pointer;">
<i class="fa-solid fa-microphone mb-2 d-block"></i>
Voice Activity
</label>
</div>
<div class="form-check custom-radio-card flex-grow-1">
<input class="form-check-input d-none" type="radio" name="voice_mode" id="voice-mode-ptt" value="ptt" <?php echo ($user['voice_mode'] ?? 'vox') == 'ptt' ? 'checked' : ''; ?> onchange="togglePTTSettings('ptt')">
<label class="form-check-label w-100 p-3 rounded border border-secondary text-center cursor-pointer" for="voice-mode-ptt" style="cursor: pointer;">
<i class="fa-solid fa-keyboard mb-2 d-block"></i>
Push to Talk
</label>
</div>
</div>
<div id="ptt-settings-container" style="<?php echo ($user['voice_mode'] ?? 'vox') == 'ptt' ? '' : 'display: none;'; ?>">
<div class="mb-3">
<label class="form-label small text-white fw-bold">Shortcut Key</label>
<input type="text" name="voice_ptt_key" id="voice_ptt_key_input" class="form-control bg-dark text-white border-0" value="<?php echo htmlspecialchars($user['voice_ptt_key'] ?? 'v'); ?>" placeholder="Click and press a key..." readonly style="cursor: pointer; caret-color: transparent;">
<div class="form-text text-muted" style="font-size: 0.8em;">Click the box and press any key to set your PTT shortcut.</div>
</div>
</div>
<div id="vox-settings-container" style="<?php echo ($user['voice_mode'] ?? 'vox') == 'vox' ? '' : 'display: none;'; ?>">
<div class="mb-3">
<label class="form-label small text-white fw-bold">Input Sensitivity</label>
<input type="range" name="voice_vox_threshold" class="form-range" min="0" max="1" step="0.01" value="<?php echo $user['voice_vox_threshold'] ?? 0.1; ?>">
<div class="d-flex justify-content-between small text-muted mt-1">
<span>Loud</span>
<span>Quiet</span>
</div>
</div>
</div>
</div>
<div class="p-3 rounded border border-info border-opacity-25" style="background-color: rgba(0, 168, 252, 0.05);">
<div class="d-flex">
<i class="fa-solid fa-circle-info text-info me-3 mt-1"></i>
<div>
<div class="fw-bold text-info small mb-1">Microphone Access</div>
<div class="text-muted small">Voice channels require microphone permission. If you don't hear anything, check your browser's site settings.</div>
</div>
</div>
</div>
</div>
<!-- Notifications Tab -->
<div class="tab-pane fade" id="settings-notifications" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Notifications</h5>
<div class="p-3 rounded" style="background-color: #232428;">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="dnd_mode" id="dnd-switch" value="1" <?php echo ($user['dnd_mode'] ?? 0) ? 'checked' : ''; ?>>
<label class="form-check-label text-white" for="dnd-switch">Do Not Disturb</label>
<div class="form-text text-muted" style="font-size: 0.8em;">Mute all desktop notifications.</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="sound_notifications" id="sound-switch" value="1" <?php echo ($user['sound_notifications'] ?? 0) ? 'checked' : ''; ?>>
<label class="form-check-label text-white" for="sound-switch">Sound Notifications</label>
<div class="form-text text-muted" style="font-size: 0.8em;">Play a sound when you are mentioned.</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer border-0" style="background-color: #2b2d31;">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Cancel</button>
<button type="button" onclick="handleSaveUserSettings(this)" class="btn btn-primary" style="background-color: var(--blurple); border: none; padding: 10px 24px;">Save Changes</button>
<button type="button" onclick="handleSaveUserSettings(this)" class="btn btn-primary" style="background-color: var(--blurple); border: none; padding: 10px 32px; font-weight: 600;">Save Changes</button>
</div>
</div>
</div>
</div>
<style>
.custom-radio-card input:checked + label {
background-color: var(--blurple) !important;
border-color: var(--blurple) !important;
color: white !important;
}
.custom-radio-card label:hover {
background-color: rgba(255,255,255,0.05);
}
.cursor-pointer { cursor: pointer; }
</style>
<script>
function togglePTTSettings(mode) {
console.log('Toggling voice mode to:', mode);
const pttContainer = document.getElementById('ptt-settings-container');
const voxContainer = document.getElementById('vox-settings-container');
if (pttContainer) pttContainer.style.display = (mode === 'ptt' ? 'block' : 'none');
if (voxContainer) voxContainer.style.display = (mode === 'vox' ? 'block' : 'none');
}
// Special handler for PTT key input to make it more intuitive
document.addEventListener('DOMContentLoaded', () => {
const pttInput = document.getElementById('voice_ptt_key_input');
if (pttInput) {
pttInput.addEventListener('keydown', (e) => {
e.preventDefault();
pttInput.value = e.key;
});
}
});
function handlePTTKeyCapture(e, input) {
e.preventDefault();
input.value = e.key;
}
async function handleSaveUserSettings(btn) {
const originalContent = btn.innerHTML;
const form = document.getElementById('user-settings-form');
@ -1187,19 +1334,19 @@ async function handleSaveUserSettings(btn) {
formData.set('dnd_mode', dndMode);
formData.set('sound_notifications', soundNotifications);
// Explicitly get theme to ensure it's captured
// Explicitly get theme and voice_mode to ensure they are captured
const themeInput = form.querySelector('input[name="theme"]:checked');
const theme = themeInput ? themeInput.value : 'dark';
formData.set('theme', theme);
if (themeInput) formData.set('theme', themeInput.value);
const voiceModeInput = form.querySelector('input[name="voice_mode"]:checked');
if (voiceModeInput) formData.set('voice_mode', voiceModeInput.value);
try {
console.log('Sending save request...');
const resp = await fetch('api_v1_user.php?v=' + Date.now(), {
method: 'POST',
body: formData
});
const result = await resp.json();
console.log('Response received:', result);
if (result.success) {
btn.innerHTML = '<i class="fa-solid fa-check me-2"></i> Saved!';
@ -1218,6 +1365,7 @@ async function handleSaveUserSettings(btn) {
}
</script>
<!-- Server Settings Modal -->
<!-- Server Settings Modal -->
<div class="modal fade" id="serverSettingsModal" tabindex="-1">
@ -1768,7 +1916,6 @@ async function handleSaveUserSettings(btn) {
<ul class="dropdown-menu dropdown-menu-dark shadow border-secondary" id="add-permission-role-list" style="max-height: 300px; overflow-y: auto; min-width: 200px;">
<!-- Roles loaded here -->
</ul>
</div>
</div>
<div id="channel-permissions-roles-list" class="list-group list-group-flush overflow-auto flex-grow-1" style="max-height: 350px; overflow-x: hidden;">
<!-- List of roles with overrides -->
@ -1989,14 +2136,19 @@ async function handleSaveUserSettings(btn) {
</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; ?>;
window.currentUsername = "<?php echo addslashes($user['display_name'] ?? $user['username']); ?>";
window.currentChannelName = "<?php echo addslashes($current_channel_name); ?>";
window.isServerOwner = <?php echo ($is_owner ?? false) ? 'true' : 'false'; ?>;
window.canManageServer = <?php echo ($can_manage_server ?? false) ? 'true' : 'false'; ?>;
window.isDndMode = <?php echo ($user['dnd_mode'] ?? 0) ? 'true' : 'false'; ?>;
</script>
<script>
window.currentUserId = <?php echo $current_user_id; ?>;
window.currentUsername = '<?php echo addslashes($user['display_name'] ?? $user['username']); ?>';
window.currentAvatarUrl = '<?php echo addslashes($user['avatar_url'] ?? ''); ?>';
window.activeChannelId = <?php echo $active_channel_id; ?>;
window.serverRoles = <?php echo json_encode($server_roles ?? []); ?>;
window.voiceSettings = {
mode: '<?php echo $user['voice_mode'] ?? 'vox'; ?>',
pttKey: '<?php echo $user['voice_ptt_key'] ?? 'v'; ?>',
voxThreshold: <?php echo $user['voice_vox_threshold'] ?? 0.1; ?>
};
</script>
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<script>

View File

@ -481,3 +481,23 @@
2026-02-17 09:48:03 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 09:56:28 - GET /?fl_project=38443 - POST: []
2026-02-17 09:57:00 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 11:37:20 - GET /index.php?server_id=1&channel_id=21 - POST: []
2026-02-17 11:43:18 - GET /?fl_project=38443 - POST: []
2026-02-17 11:43:46 - GET /?fl_project=38443 - POST: []
2026-02-17 11:46:49 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 11:46:58 - GET /index.php?server_id=1&channel_id=3 - POST: []
2026-02-17 11:49:14 - GET /?fl_project=38443 - POST: []
2026-02-17 11:56:37 - GET /?fl_project=38443 - POST: []
2026-02-17 11:56:49 - GET /index.php?server_id=1&channel_id=3 - POST: []
2026-02-17 11:56:58 - GET /index.php?server_id=1&channel_id=3 - POST: []
2026-02-17 11:57:08 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 11:57:14 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:01:03 - GET /?fl_project=38443 - POST: []
2026-02-17 12:08:31 - GET /?fl_project=38443 - POST: []
2026-02-17 12:08:36 - GET /index.php?server_id=1&channel_id=6 - POST: []
{"date":"2026-02-17 12:09:05","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 12:09:05 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:14:02 - GET /?fl_project=38443 - POST: []
2026-02-17 12:14:47 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:15:05 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:18:43 - GET /?fl_project=38443 - POST: []

View File

@ -28,7 +28,7 @@ while (true) {
}
foreach ($read as $client_socket) {
$data = socket_read($client_socket, 1024);
$data = socket_read($client_socket, 65536);
if ($data === false || strlen($data) === 0) {
$key = array_search($client_socket, $clients);
unset($clients[$key]);
@ -67,6 +67,8 @@ function perform_handshake($receved_header, $client_conn, $host, $port) {
}
}
if (!isset($headers['Sec-WebSocket-Key'])) return;
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
@ -77,26 +79,32 @@ function perform_handshake($receved_header, $client_conn, $host, $port) {
}
function unmask($text) {
if (strlen($text) < 2) return null;
$length = ord($text[1]) & 127;
if ($length == 126) {
if (strlen($text) < 8) return null;
$masks = substr($text, 4, 4);
$data = substr($text, 8);
} elseif ($length == 127) {
if (strlen($text) < 14) return null;
$masks = substr($text, 10, 4);
$data = substr($text, 14);
} else {
if (strlen($text) < 6) return null;
$masks = substr($text, 2, 4);
$data = substr($text, 6);
}
$text = "";
$decoded = "";
for ($i = 0; $i < strlen($data); ++$i) {
$text .= $data[$i] ^ $masks[$i % 4];
$decoded .= $data[$i] ^ $masks[$i % 4];
}
return $text;
return $decoded;
}
function mask($text) {
$b1 = 0x80 | (0x1 & 0x0f);
$b1 = 0x81; // FIN + Opcode 1 (text)
$length = strlen($text);
if ($length <= 125)
$header = pack('CC', $b1, $length);