a1971f9515
Groups is fully removed: No homepage navigation, Account feature, tier setting, API, or client code. Legacy group tables, entitlements, messages, voice files, and config are purged automatically. New cache-busted client: cyberchat-app.js?v=20260609.3. Browser and SQLite migration tests passed with no console errors.
204 lines
8.0 KiB
PHP
204 lines
8.0 KiB
PHP
<?php
|
|
define('CYBERCHAT_API', true);
|
|
require_once __DIR__ . '/../bootstrap.php';
|
|
sendCorsHeaders();
|
|
|
|
try { archiveYesterdayIfNeeded(); } catch (Throwable $e) { error_log($e->getMessage()); }
|
|
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
|
|
|
match ($action) {
|
|
'send' => sendTextMessage(),
|
|
'send_voice' => sendVoiceMessage(),
|
|
'recording' => setRecordingStatus(),
|
|
'poll' => pollMessages(),
|
|
'history' => archiveDays(),
|
|
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 sendTextMessage(): never {
|
|
$user = authRequired();
|
|
$message = trim((string)($_POST['message'] ?? ''));
|
|
$max = (int)getConfigVal('chat.max_message_length', 500);
|
|
if ($message === '') jsonResponse(['error' => 'Empty message'], 400);
|
|
if (strlen($message) > $max) jsonResponse(['error' => "Message too long (max $max chars)"], 400);
|
|
|
|
$created = time();
|
|
$db = getDB();
|
|
$db->prepare("INSERT INTO messages (user_id,username,color,message,created_at,day_key)
|
|
VALUES (?,?,?,?,?,?)")->execute([
|
|
$user['id'], $user['username'], $user['color'], $message, $created, dayKey(),
|
|
]);
|
|
trackEvent('message_text', '/api/messages.php', (int)$user['id']);
|
|
jsonResponse(['success' => true, 'message' => messagePayload([
|
|
'id' => $db->lastInsertId(),
|
|
'username' => $user['username'],
|
|
'color' => $user['color'],
|
|
'message' => $message,
|
|
'message_type' => 'text',
|
|
'created_at' => $created,
|
|
'day_key' => dayKey(),
|
|
])]);
|
|
}
|
|
|
|
function sendVoiceMessage(): never {
|
|
$user = authRequired();
|
|
if (!getConfigVal('voice.enabled', true) || !featureValue($user, 'voice_messages', true)) {
|
|
jsonResponse(['error' => 'Your plan does not include voice messages'], 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();
|
|
ensureWritableDirectory($dir, 'Voice storage');
|
|
$filename = bin2hex(random_bytes(20)) . '.' . $allowed[$mime];
|
|
if (!move_uploaded_file($file['tmp_name'], $dir . '/' . $filename)) {
|
|
jsonResponse(['error' => 'Could not store voice clip'], 500);
|
|
}
|
|
|
|
$created = time();
|
|
$storedMime = $mime === 'video/webm' ? 'audio/webm' : $mime;
|
|
$db = getDB();
|
|
try {
|
|
$db->prepare("INSERT INTO messages
|
|
(user_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key)
|
|
VALUES (?,?,?,?,'voice',?,?,?,?,?)")->execute([
|
|
$user['id'], $user['username'], $user['color'], '[Voice clip]',
|
|
$filename, $storedMime, $duration, $created, dayKey(),
|
|
]);
|
|
} catch (Throwable $e) {
|
|
deleteVoiceFile($filename);
|
|
throw $e;
|
|
}
|
|
$db->prepare("DELETE FROM recording_status WHERE channel_key='public' AND user_id=?")
|
|
->execute([$user['id']]);
|
|
trackEvent('message_voice', '/api/messages.php', (int)$user['id']);
|
|
jsonResponse(['success' => true, 'message' => messagePayload([
|
|
'id' => $db->lastInsertId(),
|
|
'username' => $user['username'],
|
|
'color' => $user['color'],
|
|
'message' => '[Voice clip]',
|
|
'message_type' => 'voice',
|
|
'voice_file' => $filename,
|
|
'voice_mime' => $storedMime,
|
|
'voice_duration' => $duration,
|
|
'created_at' => $created,
|
|
'day_key' => dayKey(),
|
|
])]);
|
|
}
|
|
|
|
function setRecordingStatus(): never {
|
|
$user = authRequired();
|
|
if (!featureValue($user, 'recording_status', false)) {
|
|
jsonResponse(['error' => 'Your plan does not include recording indicators'], 403);
|
|
}
|
|
$active = filter_var($_POST['active'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
|
$db = getDB();
|
|
if ($active) {
|
|
$db->prepare("INSERT INTO recording_status (channel_key,user_id,expires_at) VALUES ('public',?,?)
|
|
ON CONFLICT(channel_key,user_id) DO UPDATE SET expires_at=excluded.expires_at")
|
|
->execute([$user['id'], time() + 10]);
|
|
} else {
|
|
$db->prepare("DELETE FROM recording_status WHERE channel_key='public' AND user_id=?")
|
|
->execute([$user['id']]);
|
|
}
|
|
jsonResponse(['success' => true]);
|
|
}
|
|
|
|
function pollMessages(): never {
|
|
$user = authRequired();
|
|
$since = max(0, (int)($_GET['since'] ?? 0));
|
|
$limit = max(1, min(250, (int)getConfigVal('chat.messages_per_page', 100)));
|
|
$db = getDB();
|
|
|
|
if ($since === 0) {
|
|
$stmt = $db->prepare("SELECT * FROM messages
|
|
WHERE day_key=? ORDER BY id DESC LIMIT ?");
|
|
$stmt->execute([dayKey(), $limit]);
|
|
$rows = array_reverse($stmt->fetchAll());
|
|
$hasMore = false;
|
|
} else {
|
|
$stmt = $db->prepare("SELECT * FROM messages
|
|
WHERE day_key=? AND id>? ORDER BY id LIMIT ?");
|
|
$stmt->execute([dayKey(), $since, $limit + 1]);
|
|
$rows = $stmt->fetchAll();
|
|
$hasMore = count($rows) > $limit;
|
|
if ($hasMore) $rows = array_slice($rows, 0, $limit);
|
|
}
|
|
|
|
$cursor = $since;
|
|
foreach ($rows as $row) $cursor = max($cursor, (int)$row['id']);
|
|
|
|
$cutoff = time() - 300;
|
|
$onlineStmt = $db->prepare("SELECT COUNT(*) FROM sessions WHERE last_active>?");
|
|
$onlineStmt->execute([$cutoff]);
|
|
|
|
$recording = [];
|
|
$db->prepare("DELETE FROM recording_status WHERE expires_at<=?")->execute([time()]);
|
|
if (featureValue($user, 'recording_status', false)) {
|
|
$recordStmt = $db->prepare("SELECT u.id,u.username,u.color FROM recording_status r
|
|
JOIN users u ON u.id=r.user_id
|
|
WHERE r.channel_key='public' AND r.expires_at>? AND r.user_id!=?");
|
|
$recordStmt->execute([time(), $user['id']]);
|
|
$recording = $recordStmt->fetchAll();
|
|
}
|
|
|
|
jsonResponse([
|
|
'messages' => array_map('messagePayload', $rows),
|
|
'cursor' => $cursor,
|
|
'has_more' => $hasMore,
|
|
'online' => (int)$onlineStmt->fetchColumn(),
|
|
'recording' => $recording,
|
|
'server_time' => time(),
|
|
]);
|
|
}
|
|
|
|
function archiveDays(): never {
|
|
$user = authRequired();
|
|
$rows = personalArchiveRows($user, '', 1000);
|
|
$days = array_values(array_unique(array_column($rows, 'day_key')));
|
|
rsort($days);
|
|
jsonResponse(['days' => $days]);
|
|
}
|