235 lines
7.8 KiB
PHP
235 lines
7.8 KiB
PHP
<?php
|
|
// api/messages.php
|
|
define('CYBERCHAT_API', true);
|
|
require_once __DIR__ . '/../bootstrap.php';
|
|
sendCorsHeaders();
|
|
|
|
header('Content-Type: application/json');
|
|
header('Cache-Control: no-store');
|
|
|
|
// Run archiving silently
|
|
try { archiveYesterdayIfNeeded(); } catch (Throwable $e) { /* silent */ }
|
|
|
|
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
|
|
|
switch ($action) {
|
|
case 'send':
|
|
handleSend();
|
|
break;
|
|
case 'send_voice':
|
|
handleSendVoice();
|
|
break;
|
|
case 'poll':
|
|
handlePoll();
|
|
break;
|
|
case 'history':
|
|
handleHistory();
|
|
break;
|
|
default:
|
|
jsonResponse(['error' => 'Unknown action'], 400);
|
|
}
|
|
|
|
function messagePayload(array $row): array {
|
|
return [
|
|
'id' => (int)$row['id'],
|
|
'username' => $row['username'],
|
|
'color' => $row['color'],
|
|
'message' => $row['message'],
|
|
'message_type' => $row['message_type'] ?? 'text',
|
|
'voice_url' => !empty($row['voice_file']) ? voicePublicPath($row['voice_file']) : null,
|
|
'voice_mime' => $row['voice_mime'] ?? null,
|
|
'voice_duration' => isset($row['voice_duration']) ? (float)$row['voice_duration'] : null,
|
|
'created_at' => (int)$row['created_at'],
|
|
'day_key' => $row['day_key'] ?? dayKey(),
|
|
];
|
|
}
|
|
|
|
function handleSend(): never {
|
|
$session = authRequired();
|
|
$db = getDB();
|
|
|
|
$message = trim($_POST['message'] ?? '');
|
|
$maxLen = getConfigVal('chat.max_message_length', 500);
|
|
|
|
if (!$message) jsonResponse(['error' => 'Empty message'], 400);
|
|
if (strlen($message) > $maxLen) {
|
|
jsonResponse(['error' => "Message too long (max {$maxLen} chars)"], 400);
|
|
}
|
|
|
|
$dayKey = dayKey();
|
|
|
|
$stmt = $db->prepare("
|
|
INSERT INTO messages (user_id, username, color, message, created_at, day_key)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$session['uid'],
|
|
$session['username'],
|
|
$session['color'],
|
|
$message,
|
|
time(),
|
|
$dayKey
|
|
]);
|
|
|
|
$id = $db->lastInsertId();
|
|
|
|
jsonResponse(['success' => true, 'message' => messagePayload([
|
|
'id' => $id, 'username' => $session['username'], 'color' => $session['color'],
|
|
'message' => $message, 'message_type' => 'text', 'created_at' => time(), 'day_key' => $dayKey,
|
|
])]);
|
|
}
|
|
|
|
function handleSendVoice(): never {
|
|
$session = authRequired();
|
|
if (!getConfigVal('voice.enabled', true)) {
|
|
jsonResponse(['error' => 'Voice clips are disabled'], 403);
|
|
}
|
|
if (empty($_FILES['voice']) || !is_array($_FILES['voice'])) {
|
|
jsonResponse(['error' => 'No voice clip uploaded'], 400);
|
|
}
|
|
|
|
$file = $_FILES['voice'];
|
|
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
|
jsonResponse(['error' => 'Voice upload failed'], 400);
|
|
}
|
|
|
|
$maxBytes = (int)getConfigVal('voice.max_upload_bytes', 12582912);
|
|
if (($file['size'] ?? 0) < 1 || $file['size'] > $maxBytes) {
|
|
jsonResponse(['error' => 'Voice clip exceeds the upload limit'], 400);
|
|
}
|
|
|
|
$duration = (float)($_POST['duration'] ?? 0);
|
|
$maxSeconds = max(1, (int)getConfigVal('voice.max_duration_seconds', 60));
|
|
if ($duration <= 0 || $duration > $maxSeconds + 1) {
|
|
jsonResponse(['error' => "Voice clip must be {$maxSeconds} seconds or less"], 400);
|
|
}
|
|
|
|
$mime = class_exists('finfo')
|
|
? (string)(new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name'])
|
|
: '';
|
|
$header = (string)file_get_contents($file['tmp_name'], false, null, 0, 12);
|
|
if (str_starts_with($header, "\x1A\x45\xDF\xA3")) {
|
|
$mime = 'audio/webm';
|
|
} elseif (str_starts_with($header, 'RIFF') && substr($header, 8, 4) === 'WAVE') {
|
|
$mime = 'audio/wav';
|
|
}
|
|
$allowed = [
|
|
'audio/webm' => 'webm',
|
|
'video/webm' => 'webm',
|
|
'audio/wav' => 'wav',
|
|
'audio/x-wav' => 'wav',
|
|
'audio/wave' => 'wav',
|
|
];
|
|
if (!isset($allowed[$mime])) {
|
|
jsonResponse(['error' => 'Voice clips must be WAV or WebM'], 400);
|
|
}
|
|
|
|
$dir = voiceUploadDir();
|
|
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
|
|
jsonResponse(['error' => 'Voice storage is unavailable'], 500);
|
|
}
|
|
$filename = bin2hex(random_bytes(20)) . '.' . $allowed[$mime];
|
|
if (!move_uploaded_file($file['tmp_name'], $dir . '/' . $filename)) {
|
|
jsonResponse(['error' => 'Could not store voice clip'], 500);
|
|
}
|
|
|
|
$db = getDB();
|
|
$created = time();
|
|
$dayKey = dayKey();
|
|
try {
|
|
$stmt = $db->prepare("
|
|
INSERT INTO messages
|
|
(user_id, username, color, message, message_type, voice_file, voice_mime, voice_duration, created_at, day_key)
|
|
VALUES (?, ?, ?, ?, 'voice', ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$session['uid'], $session['username'], $session['color'], '[Voice clip]',
|
|
$filename, $mime === 'video/webm' ? 'audio/webm' : $mime, $duration, $created, $dayKey
|
|
]);
|
|
} catch (Throwable $e) {
|
|
deleteVoiceFile($filename);
|
|
throw $e;
|
|
}
|
|
|
|
jsonResponse(['success' => true, 'message' => messagePayload([
|
|
'id' => $db->lastInsertId(), 'username' => $session['username'], 'color' => $session['color'],
|
|
'message' => '[Voice clip]', 'message_type' => 'voice', 'voice_file' => $filename,
|
|
'voice_mime' => $mime === 'video/webm' ? 'audio/webm' : $mime,
|
|
'voice_duration' => $duration, 'created_at' => $created, 'day_key' => $dayKey,
|
|
])]);
|
|
}
|
|
|
|
function handlePoll(): never {
|
|
$session = authRequired();
|
|
$db = getDB();
|
|
|
|
$since = (int)($_GET['since'] ?? 0);
|
|
$dayKey = dayKey();
|
|
$limit = getConfigVal('chat.messages_per_page', 100);
|
|
|
|
if ($since === 0) {
|
|
// Initial load — get last N messages for today
|
|
$stmt = $db->prepare("
|
|
SELECT id, username, color, message, message_type, voice_file, voice_mime, voice_duration, created_at, day_key
|
|
FROM messages
|
|
WHERE day_key = ?
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT ?
|
|
");
|
|
$stmt->execute([$dayKey, $limit]);
|
|
$rows = array_reverse($stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
} else {
|
|
// Poll for new messages since last id
|
|
$stmt = $db->prepare("
|
|
SELECT id, username, color, message, message_type, voice_file, voice_mime, voice_duration, created_at, day_key
|
|
FROM messages
|
|
WHERE day_key = ? AND id > ?
|
|
ORDER BY created_at ASC, id ASC
|
|
LIMIT ?
|
|
");
|
|
$stmt->execute([$dayKey, $since, $limit]);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
// Cast types
|
|
$messages = array_map('messagePayload', $rows);
|
|
|
|
// Get online user count (active in last 5 minutes)
|
|
$cutoff5min = time() - 300;
|
|
$onlineStmt = $db->prepare("SELECT COUNT(*) FROM sessions WHERE last_active > ?");
|
|
$onlineStmt->execute([$cutoff5min]);
|
|
$onlineCount = (int)$onlineStmt->fetchColumn();
|
|
|
|
jsonResponse([
|
|
'messages' => $messages,
|
|
'online' => $onlineCount,
|
|
'day_key' => $dayKey,
|
|
'server_time' => time(),
|
|
]);
|
|
}
|
|
|
|
function handleHistory(): never {
|
|
// Return list of archived days
|
|
$archiveDir = ROOT_DIR . '/' . getConfigVal('archive.archive_dir', 'archive');
|
|
$days = [];
|
|
|
|
if (is_dir($archiveDir)) {
|
|
$years = glob($archiveDir . '/*', GLOB_ONLYDIR);
|
|
foreach ($years as $yearDir) {
|
|
$year = basename($yearDir);
|
|
$months = glob($yearDir . '/*', GLOB_ONLYDIR);
|
|
foreach ($months as $monthDir) {
|
|
$month = basename($monthDir);
|
|
$files = glob($monthDir . '/*.sqlite');
|
|
foreach ($files as $file) {
|
|
$day = basename($file, '.sqlite');
|
|
$days[] = "$year-$month-$day";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rsort($days);
|
|
jsonResponse(['days' => $days]);
|
|
}
|