This commit is contained in:
Flatlogic Bot 2026-02-15 10:30:17 +00:00
parent 2642f97c8b
commit 4883125cda
14 changed files with 553 additions and 19 deletions

View File

@ -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
View 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()]);
}

View File

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

View File

@ -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
View 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
View 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
View File

@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header('Location: login.php');
exit;

71
auth/register.php Normal file
View 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
View 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;
}
}

View File

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

View File

@ -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
View 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
View File

@ -0,0 +1 @@
Server started on 0.0.0.0:8080