d0e4a870be
Admin tier assignment without payment via Users > Edit > Tier Override. Fixed live group membership refresh, polling, unread indicators, and incoming voice clip population. Added asset cache busting so clients receive the fixes immediately.
187 lines
8.7 KiB
PHP
187 lines
8.7 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'] ?? '';
|
|
|
|
switch ($action) {
|
|
case 'send': sendTextMessage();
|
|
case 'send_voice': sendVoiceMessage();
|
|
case 'recording': setRecordingStatus();
|
|
case 'poll': pollMessages();
|
|
case 'history': archiveDays();
|
|
default: jsonResponse(['error' => 'Unknown action'], 400);
|
|
}
|
|
|
|
function messagePayload(array $row): array {
|
|
return [
|
|
'id' => (int)$row['id'],
|
|
'group_id' => isset($row['group_id']) && $row['group_id'] !== null ? (int)$row['group_id'] : null,
|
|
'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 requestedGroup(array $user): ?int {
|
|
$groupId = normalizeGroupId($_POST['group_id'] ?? $_GET['group_id'] ?? null);
|
|
requireGroupAccess($user, $groupId);
|
|
return $groupId;
|
|
}
|
|
|
|
function sendTextMessage(): never {
|
|
$user = authRequired();
|
|
$groupId = requestedGroup($user);
|
|
$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,group_id,username,color,message,created_at,day_key)
|
|
VALUES (?,?,?,?,?,?,?)")->execute([
|
|
$user['id'], $groupId, $user['username'], $user['color'], $message, $created, dayKey(),
|
|
]);
|
|
if ($groupId !== null) {
|
|
$db->prepare("UPDATE chat_groups SET updated_at=? WHERE id=?")->execute([$created, $groupId]);
|
|
}
|
|
trackEvent('message_text', '/api/messages.php', (int)$user['id']);
|
|
jsonResponse(['success' => true, 'message' => messagePayload([
|
|
'id' => $db->lastInsertId(), 'group_id' => $groupId, 'username' => $user['username'],
|
|
'color' => $user['color'], 'message' => $message, 'message_type' => 'text',
|
|
'created_at' => $created, 'day_key' => dayKey(),
|
|
])]);
|
|
}
|
|
|
|
function sendVoiceMessage(): never {
|
|
$user = authRequired();
|
|
$groupId = requestedGroup($user);
|
|
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,group_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key)
|
|
VALUES (?,?,?,?,?,'voice',?,?,?,?,?)")->execute([
|
|
$user['id'], $groupId, $user['username'], $user['color'], '[Voice clip]',
|
|
$filename, $storedMime, $duration, $created, dayKey(),
|
|
]);
|
|
if ($groupId !== null) {
|
|
$db->prepare("UPDATE chat_groups SET updated_at=? WHERE id=?")->execute([$created, $groupId]);
|
|
}
|
|
} catch (Throwable $e) {
|
|
deleteVoiceFile($filename);
|
|
throw $e;
|
|
}
|
|
getDB()->prepare("DELETE FROM recording_status WHERE group_key=? AND user_id=?")
|
|
->execute([groupKey($groupId), $user['id']]);
|
|
trackEvent('message_voice', '/api/messages.php', (int)$user['id']);
|
|
jsonResponse(['success' => true, 'message' => messagePayload([
|
|
'id' => $db->lastInsertId(), 'group_id' => $groupId, '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);
|
|
}
|
|
$groupId = requestedGroup($user);
|
|
$active = filter_var($_POST['active'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
|
$db = getDB();
|
|
if ($active) {
|
|
$db->prepare("INSERT INTO recording_status (group_key,user_id,expires_at) VALUES (?,?,?)
|
|
ON CONFLICT(group_key,user_id) DO UPDATE SET expires_at=excluded.expires_at")
|
|
->execute([groupKey($groupId), $user['id'], time() + 10]);
|
|
} else {
|
|
$db->prepare("DELETE FROM recording_status WHERE group_key=? AND user_id=?")
|
|
->execute([groupKey($groupId), $user['id']]);
|
|
}
|
|
jsonResponse(['success' => true]);
|
|
}
|
|
|
|
function pollMessages(): never {
|
|
$user = authRequired();
|
|
$groupId = requestedGroup($user);
|
|
$since = max(0, (int)($_GET['since'] ?? 0));
|
|
$limit = max(1, min(250, (int)getConfigVal('chat.messages_per_page', 100)));
|
|
$db = getDB();
|
|
$whereGroup = $groupId === null ? 'group_id IS NULL' : 'group_id = ?';
|
|
$params = $groupId === null ? [dayKey()] : [dayKey(), $groupId];
|
|
if ($since === 0) {
|
|
$stmt = $db->prepare("SELECT * FROM messages WHERE day_key=? AND $whereGroup ORDER BY id DESC LIMIT ?");
|
|
$params[] = $limit;
|
|
$stmt->execute($params);
|
|
$rows = array_reverse($stmt->fetchAll());
|
|
} else {
|
|
$stmt = $db->prepare("SELECT * FROM messages WHERE day_key=? AND $whereGroup AND id>? ORDER BY id LIMIT ?");
|
|
$params[] = $since;
|
|
$params[] = $limit;
|
|
$stmt->execute($params);
|
|
$rows = $stmt->fetchAll();
|
|
}
|
|
|
|
$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.group_key=? AND r.expires_at>? AND r.user_id!=?");
|
|
$recordStmt->execute([groupKey($groupId), time(), $user['id']]);
|
|
$recording = $recordStmt->fetchAll();
|
|
}
|
|
jsonResponse([
|
|
'messages' => array_map('messagePayload', $rows),
|
|
'online' => (int)$onlineStmt->fetchColumn(),
|
|
'recording' => $recording,
|
|
'groups' => userGroups((int)$user['id']),
|
|
'group_id' => $groupId,
|
|
'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]);
|
|
}
|