v2
This commit is contained in:
parent
2642f97c8b
commit
4883125cda
@ -1,11 +1,35 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once 'db/config.php';
|
||||
require_once 'auth/session.php';
|
||||
|
||||
// Check for Bot token in headers
|
||||
$headers = getallheaders();
|
||||
$bot_token = null;
|
||||
if (isset($headers['Authorization']) && preg_match('/Bot\s+(\S+)/', $headers['Authorization'], $matches)) {
|
||||
$bot_token = $matches[1];
|
||||
}
|
||||
|
||||
$user_id = null;
|
||||
if ($bot_token) {
|
||||
$stmt = db()->prepare("SELECT id FROM users WHERE bot_token = ? AND is_bot = TRUE");
|
||||
$stmt->execute([$bot_token]);
|
||||
$bot = $stmt->fetch();
|
||||
if ($bot) {
|
||||
$user_id = $bot['id'];
|
||||
} else {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid Bot Token']);
|
||||
exit;
|
||||
}
|
||||
} elseif (isset($_SESSION['user_id'])) {
|
||||
$user_id = $_SESSION['user_id'];
|
||||
} else {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$content = $data['content'] ?? '';
|
||||
$channel_id = (int)($data['channel_id'] ?? 1);
|
||||
$user_id = 1; // Mock logged in user
|
||||
|
||||
if (empty($content)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Empty content']);
|
||||
|
||||
44
api_v1_webhook.php
Normal file
44
api_v1_webhook.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once 'db/config.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.
|
||||
|
||||
$stmt = db()->prepare("INSERT INTO messages (channel_id, user_id, content) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$webhook['channel_id'], 1, $content]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@ -115,6 +115,50 @@ body {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.voice-item::before {
|
||||
content: "🔊";
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.channel-category {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* User Panel */
|
||||
.user-panel {
|
||||
height: 52px;
|
||||
background-color: #232428;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Chat Area */
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
|
||||
@ -6,6 +6,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Scroll to bottom
|
||||
messagesList.scrollTop = messagesList.scrollHeight;
|
||||
|
||||
// WebSocket for real-time
|
||||
let ws;
|
||||
try {
|
||||
ws = new WebSocket('ws://' + window.location.hostname + ':8080');
|
||||
ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.type === 'message') {
|
||||
const data = JSON.parse(msg.data);
|
||||
// Simple broadcast, we check if it belongs to current channel
|
||||
const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1;
|
||||
if (data.channel_id == currentChannel) {
|
||||
appendMessage(data);
|
||||
messagesList.scrollTop = messagesList.scrollHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn('WebSocket connection failed, falling back to REST only.');
|
||||
}
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const content = chatInput.value.trim();
|
||||
@ -13,20 +33,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
chatInput.value = '';
|
||||
|
||||
const channel_id = new URLSearchParams(window.location.search).get('channel_id') || 1;
|
||||
|
||||
try {
|
||||
const response = await fetch('api_v1_messages.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
channel_id: new URLSearchParams(window.location.search).get('channel_id') || 1
|
||||
channel_id: channel_id
|
||||
})
|
||||
});
|
||||
// Voice
|
||||
const voiceHandler = new VoiceChannel(ws);
|
||||
document.querySelectorAll('.voice-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const cid = item.dataset.channelId;
|
||||
voiceHandler.join(cid);
|
||||
|
||||
// UI Update
|
||||
document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
appendMessage(result.message);
|
||||
messagesList.scrollTop = messagesList.scrollHeight;
|
||||
// If WS is connected, we might want to let WS handle the UI update
|
||||
// But for simplicity, we append here and also send to WS
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
...result.message,
|
||||
channel_id: channel_id
|
||||
}));
|
||||
} else {
|
||||
appendMessage(result.message);
|
||||
messagesList.scrollTop = messagesList.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + result.error);
|
||||
}
|
||||
|
||||
30
assets/js/voice.js
Normal file
30
assets/js/voice.js
Normal file
@ -0,0 +1,30 @@
|
||||
// Placeholder for WebRTC Voice Logic
|
||||
class VoiceChannel {
|
||||
constructor(ws) {
|
||||
this.ws = ws;
|
||||
this.localStream = null;
|
||||
this.peers = {};
|
||||
}
|
||||
|
||||
async join(channelId) {
|
||||
console.log('Joining voice channel:', channelId);
|
||||
try {
|
||||
this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'voice_join',
|
||||
channel_id: channelId
|
||||
}));
|
||||
// Signalization would happen here via WS
|
||||
} catch (e) {
|
||||
console.error('Failed to get local stream:', e);
|
||||
alert('Could not access microphone.');
|
||||
}
|
||||
}
|
||||
|
||||
leave() {
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
this.ws.send(JSON.stringify({ type: 'voice_leave' }));
|
||||
}
|
||||
}
|
||||
69
auth/login.php
Normal file
69
auth/login.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/session.php';
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$email = $_POST['email'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($email && $password) {
|
||||
$stmt = db()->prepare("SELECT * FROM users WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password_hash'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = "Invalid email or password.";
|
||||
}
|
||||
} else {
|
||||
$error = "Please fill all fields.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login | Discord Clone</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/discord.css">
|
||||
<style>
|
||||
body { background-color: #313338; display: flex; align-items: center; justify-content: center; height: 100vh; }
|
||||
.auth-card { background-color: #2b2d31; padding: 32px; border-radius: 8px; width: 100%; max-width: 480px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); }
|
||||
.form-label { color: #b5bac1; font-size: 12px; font-weight: bold; text-transform: uppercase; }
|
||||
.form-control { background-color: #1e1f22; border: none; color: #dbdee1; padding: 10px; }
|
||||
.form-control:focus { background-color: #1e1f22; color: #dbdee1; box-shadow: none; }
|
||||
.btn-blurple { background-color: #5865f2; color: white; width: 100%; font-weight: bold; margin-top: 20px; }
|
||||
.btn-blurple:hover { background-color: #4752c4; color: white; }
|
||||
.auth-footer { color: #949ba4; font-size: 14px; margin-top: 10px; }
|
||||
.auth-footer a { color: #00a8fc; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-card">
|
||||
<h3 class="text-center mb-1">Welcome back!</h3>
|
||||
<p class="text-center mb-4" style="color: #b5bac1;">We're so excited to see you again!</p>
|
||||
<?php if($error): ?>
|
||||
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email or Phone Number</label>
|
||||
<input type="email" name="email" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<a href="#" style="color: #00a8fc; font-size: 14px; text-decoration: none;">Forgot your password?</a>
|
||||
<button type="submit" class="btn btn-blurple">Log In</button>
|
||||
<div class="auth-footer">
|
||||
Need an account? <a href="register.php">Register</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
5
auth/logout.php
Normal file
5
auth/logout.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
71
auth/register.php
Normal file
71
auth/register.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/session.php';
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$email = $_POST['email'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($username && $email && $password) {
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$username, $email, $hash]);
|
||||
$_SESSION['user_id'] = db()->lastInsertId();
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
$error = "Registration failed: " . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = "Please fill all fields.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Register | Discord Clone</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/discord.css">
|
||||
<style>
|
||||
body { background-color: #313338; display: flex; align-items: center; justify-content: center; height: 100vh; }
|
||||
.auth-card { background-color: #2b2d31; padding: 32px; border-radius: 8px; width: 100%; max-width: 480px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); }
|
||||
.form-label { color: #b5bac1; font-size: 12px; font-weight: bold; text-transform: uppercase; }
|
||||
.form-control { background-color: #1e1f22; border: none; color: #dbdee1; padding: 10px; }
|
||||
.form-control:focus { background-color: #1e1f22; color: #dbdee1; box-shadow: none; }
|
||||
.btn-blurple { background-color: #5865f2; color: white; width: 100%; font-weight: bold; margin-top: 20px; }
|
||||
.btn-blurple:hover { background-color: #4752c4; color: white; }
|
||||
.auth-footer { color: #949ba4; font-size: 14px; margin-top: 10px; }
|
||||
.auth-footer a { color: #00a8fc; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-card">
|
||||
<h3 class="text-center mb-4">Create an account</h3>
|
||||
<?php if($error): ?>
|
||||
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-blurple">Continue</button>
|
||||
<div class="auth-footer">
|
||||
Already have an account? <a href="login.php">Login</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
20
auth/session.php
Normal file
20
auth/session.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
function getCurrentUser() {
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$_SESSION['user_id']]);
|
||||
return $stmt->fetch();
|
||||
}
|
||||
|
||||
function requireLogin() {
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: auth/login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,8 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
avatar_url VARCHAR(255),
|
||||
status VARCHAR(20) DEFAULT 'offline',
|
||||
is_bot BOOLEAN DEFAULT FALSE,
|
||||
bot_token VARCHAR(64) UNIQUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
@ -38,13 +40,44 @@ CREATE TABLE IF NOT EXISTS messages (
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webhooks (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
token VARCHAR(64) NOT NULL UNIQUE,
|
||||
channel_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
server_id INT NOT NULL,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
color VARCHAR(7) DEFAULT '#99aab5',
|
||||
permissions INT DEFAULT 0,
|
||||
position INT DEFAULT 0,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id INT NOT NULL,
|
||||
role_id INT NOT NULL,
|
||||
PRIMARY KEY (user_id, role_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Seed initial data
|
||||
INSERT IGNORE INTO users (id, username, email, password_hash, status) VALUES
|
||||
(1, 'System', 'system@local', '$2y$10$xyz', 'online');
|
||||
|
||||
INSERT IGNORE INTO servers (id, name, owner_id, invite_code) VALUES
|
||||
(1, 'General Community', 1, 'GEN-123');
|
||||
(1, 'General Community', 1, 'GEN-123'),
|
||||
(2, 'Flatlogic Devs', 1, 'DEV-456');
|
||||
|
||||
INSERT IGNORE INTO channels (id, server_id, name) VALUES
|
||||
(1, 1, 'general'),
|
||||
(2, 1, 'random');
|
||||
INSERT IGNORE INTO channels (id, server_id, name, type) VALUES
|
||||
(1, 1, 'general', 'text'),
|
||||
(2, 1, 'random', 'text'),
|
||||
(3, 1, 'Voice General', 'voice'),
|
||||
(4, 2, 'announcements', 'text'),
|
||||
(5, 2, 'coding-help', 'text');
|
||||
|
||||
25
includes/permissions.php
Normal file
25
includes/permissions.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
class Permissions {
|
||||
const VIEW_CHANNEL = 1;
|
||||
const SEND_MESSAGES = 2;
|
||||
const MANAGE_MESSAGES = 4;
|
||||
const MANAGE_CHANNELS = 8;
|
||||
const MANAGE_SERVER = 16;
|
||||
const ADMINISTRATOR = 32;
|
||||
|
||||
public static function hasPermission($user_id, $server_id, $permission) {
|
||||
$stmt = db()->prepare("
|
||||
SELECT SUM(r.permissions) as total_perms
|
||||
FROM roles r
|
||||
JOIN user_roles ur ON r.id = ur.role_id
|
||||
WHERE ur.user_id = ? AND r.server_id = ?
|
||||
");
|
||||
$stmt->execute([$user_id, $server_id]);
|
||||
$row = $stmt->fetch();
|
||||
$perms = (int)($row['total_perms'] ?? 0);
|
||||
|
||||
if ($perms & self::ADMINISTRATOR) return true;
|
||||
return ($perms & $permission) === $permission;
|
||||
}
|
||||
}
|
||||
38
index.php
38
index.php
@ -1,8 +1,9 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
require_once 'auth/session.php';
|
||||
requireLogin();
|
||||
|
||||
// Simple session check (mock for now, assume User 1)
|
||||
$current_user_id = 1;
|
||||
$user = getCurrentUser();
|
||||
$current_user_id = $user['id'];
|
||||
|
||||
// Fetch servers
|
||||
$servers = db()->query("SELECT * FROM servers LIMIT 10")->fetchAll();
|
||||
@ -76,15 +77,37 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
<?php echo htmlspecialchars($servers[0]['name'] ?? 'Server'); ?>
|
||||
</div>
|
||||
<div class="channels-list">
|
||||
<div style="color: var(--text-muted); font-size: 0.75em; text-transform: uppercase; font-weight: bold; margin-bottom: 8px; padding-left: 8px;">
|
||||
Text Channels
|
||||
</div>
|
||||
<?php foreach($channels as $c): ?>
|
||||
<div class="channel-category">Text Channels</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>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="channel-category" style="margin-top: 16px;">Voice Channels</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>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="user-panel">
|
||||
<div class="user-info">
|
||||
<div class="message-avatar" style="width: 32px; height: 32px;"></div>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="font-weight: bold; font-size: 0.85em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<?php echo htmlspecialchars($user['username']); ?>
|
||||
</div>
|
||||
<div style="color: var(--text-muted); font-size: 0.75em;">#<?php echo str_pad($user['id'], 4, '0', STR_PAD_LEFT); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<a href="auth/logout.php" title="Logout" style="color: var(--text-muted);"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 10px; font-size: 10px; color: #4e5058; border-top: 1px solid #1e1f22;">
|
||||
PHP <?php echo PHP_VERSION; ?> | <?php echo date('H:i'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -137,6 +160,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
100
ws/server.php
Normal file
100
ws/server.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
// Very basic WebSocket server in pure PHP
|
||||
$host = '0.0.0.0';
|
||||
$port = 8080;
|
||||
|
||||
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
||||
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||
socket_bind($socket, $host, $port);
|
||||
socket_listen($socket);
|
||||
|
||||
$clients = [$socket];
|
||||
|
||||
echo "Server started on $host:$port\n";
|
||||
|
||||
while (true) {
|
||||
$read = $clients;
|
||||
$write = $except = null;
|
||||
socket_select($read, $write, $except, null);
|
||||
|
||||
if (in_array($socket, $read)) {
|
||||
$new_socket = socket_accept($socket);
|
||||
$clients[] = $new_socket;
|
||||
$header = socket_read($new_socket, 1024);
|
||||
perform_handshake($header, $new_socket, $host, $port);
|
||||
echo "New client connected\n";
|
||||
$key = array_search($socket, $read);
|
||||
unset($read[$key]);
|
||||
}
|
||||
|
||||
foreach ($read as $client_socket) {
|
||||
$data = socket_read($client_socket, 1024);
|
||||
if ($data === false || strlen($data) === 0) {
|
||||
$key = array_search($client_socket, $clients);
|
||||
unset($clients[$key]);
|
||||
socket_close($client_socket);
|
||||
echo "Client disconnected\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$decoded_data = unmask($data);
|
||||
if ($decoded_data) {
|
||||
$response = mask(json_encode(['type' => 'message', 'data' => $decoded_data]));
|
||||
foreach ($clients as $client) {
|
||||
if ($client != $socket) {
|
||||
@socket_write($client, $response, strlen($response));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function perform_handshake($receved_header, $client_conn, $host, $port) {
|
||||
$headers = array();
|
||||
$lines = preg_split("/\r\n/", $receved_header);
|
||||
foreach ($lines as $line) {
|
||||
$line = chop($line);
|
||||
if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
|
||||
$headers[$matches[1]] = $matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
$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" .
|
||||
"Upgrade: websocket\r\n" .
|
||||
"Connection: Upgrade\r\n" .
|
||||
"Sec-WebSocket-Accept: $secAccept\r\n\r\n";
|
||||
socket_write($client_conn, $upgrade, strlen($upgrade));
|
||||
}
|
||||
|
||||
function unmask($text) {
|
||||
$length = ord($text[1]) & 127;
|
||||
if ($length == 126) {
|
||||
$masks = substr($text, 4, 4);
|
||||
$data = substr($text, 8);
|
||||
} elseif ($length == 127) {
|
||||
$masks = substr($text, 10, 4);
|
||||
$data = substr($text, 14);
|
||||
} else {
|
||||
$masks = substr($text, 2, 4);
|
||||
$data = substr($text, 6);
|
||||
}
|
||||
$text = "";
|
||||
for ($i = 0; $i < strlen($data); ++$i) {
|
||||
$text .= $data[$i] ^ $masks[$i % 4];
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
function mask($text) {
|
||||
$b1 = 0x80 | (0x1 & 0x0f);
|
||||
$length = strlen($text);
|
||||
if ($length <= 125)
|
||||
$header = pack('CC', $b1, $length);
|
||||
elseif ($length > 125 && $length < 65536)
|
||||
$header = pack('CCn', $b1, 126, $length);
|
||||
elseif ($length >= 65536)
|
||||
$header = pack('CCNN', $b1, 127, $length);
|
||||
return $header . $text;
|
||||
}
|
||||
1
ws_output.log
Normal file
1
ws_output.log
Normal file
@ -0,0 +1 @@
|
||||
Server started on 0.0.0.0:8080
|
||||
Loading…
x
Reference in New Issue
Block a user