prepare("SELECT user_id FROM api_keys WHERE id = ?"); $ownerStmt->execute([$keyData['id']]); $ownerId = $ownerStmt->fetchColumn(); // Cek jika chat private sudah ada if ($roomType === 'private') { $targetId = (int)$members[0]; $exist = $db->prepare(" SELECT cr.id, cr.room_uuid FROM chat_rooms cr JOIN chat_room_members m1 ON m1.room_id = cr.id AND m1.user_id = ? JOIN chat_room_members m2 ON m2.room_id = cr.id AND m2.user_id = ? WHERE cr.room_type = 'private' LIMIT 1 "); $exist->execute([$ownerId, $targetId]); $existing = $exist->fetch(); if ($existing) { Security::respond(['room_uuid' => $existing['room_uuid'], 'existing' => true], 200, 'Room sudah ada.'); } } $roomUUID = self::generateUUID(); $db->prepare(" INSERT INTO chat_rooms (room_uuid, room_name, room_type, created_by) VALUES (?, ?, ?, ?) ")->execute([$roomUUID, $roomName ?: null, $roomType, $ownerId]); $roomId = $db->lastInsertId(); // Tambahkan owner sebagai admin $db->prepare("INSERT INTO chat_room_members (room_id, user_id, role) VALUES (?, ?, 'admin')") ->execute([$roomId, $ownerId]); // Tambahkan anggota lain foreach ($members as $memberId) { $memberId = (int)$memberId; if ($memberId !== $ownerId) { $db->prepare("INSERT IGNORE INTO chat_room_members (room_id, user_id, role) VALUES (?, ?, 'member')") ->execute([$roomId, $memberId]); } } Security::respond([ 'room_uuid' => $roomUUID, 'room_name' => $roomName, 'room_type' => $roomType, ], 201, 'Room berhasil dibuat.'); } // ── GET /chat/rooms ─────────────────────────────────────── public static function listRooms(): void { $keyData = Security::validateApiKey('chat'); $db = Database::getConnection(); $ownerStmt = $db->prepare("SELECT user_id FROM api_keys WHERE id = ?"); $ownerStmt->execute([$keyData['id']]); $userId = $ownerStmt->fetchColumn(); $stmt = $db->prepare(" SELECT cr.room_uuid, cr.room_name, cr.room_type, cr.created_at, (SELECT COUNT(*) FROM chat_room_members WHERE room_id = cr.id) AS member_count, (SELECT content FROM chat_messages WHERE room_id = cr.id AND is_deleted = 0 ORDER BY sent_at DESC LIMIT 1) AS last_message_enc, (SELECT sent_at FROM chat_messages WHERE room_id = cr.id ORDER BY sent_at DESC LIMIT 1) AS last_message_at FROM chat_rooms cr JOIN chat_room_members crm ON crm.room_id = cr.id WHERE crm.user_id = ? ORDER BY last_message_at DESC "); $stmt->execute([$userId]); $rooms = $stmt->fetchAll(); // Dekripsi preview pesan terakhir foreach ($rooms as &$room) { if ($room['last_message_enc']) { $room['last_message_preview'] = mb_substr( Security::decryptMessage($room['last_message_enc']), 0, 50 ) . '...'; } unset($room['last_message_enc']); } Security::respond($rooms); } // ── POST /chat/rooms/{uuid}/messages ────────────────────── public static function sendMessage(string $roomUUID): void { $keyData = Security::validateApiKey('chat'); $body = self::getBody(); $content = trim($body['content'] ?? ''); $messageType = $body['message_type'] ?? 'text'; $fileUUID = $body['file_uuid'] ?? null; $replyTo = $body['reply_to_uuid'] ?? null; if ($messageType === 'text' && empty($content)) { Security::abort(422, 'Konten pesan tidak boleh kosong.'); } $db = Database::getConnection(); // Ambil user_id $ownerStmt = $db->prepare("SELECT user_id FROM api_keys WHERE id = ?"); $ownerStmt->execute([$keyData['id']]); $senderId = $ownerStmt->fetchColumn(); // Verifikasi room & keanggotaan $room = self::getRoomByUUID($roomUUID); if (!$room) Security::abort(404, 'Room tidak ditemukan.'); $isMember = $db->prepare(" SELECT id FROM chat_room_members WHERE room_id = ? AND user_id = ? "); $isMember->execute([$room['id'], $senderId]); if (!$isMember->fetch()) { Security::abort(403, 'Anda bukan anggota room ini.'); } // Enkripsi pesan $encryptedContent = $content ? Security::encryptMessage($content) : null; $msgUUID = self::generateUUID(); $db->prepare(" INSERT INTO chat_messages (room_id, sender_id, message_uuid, message_type, content, file_uuid, reply_to_uuid) VALUES (?, ?, ?, ?, ?, ?, ?) ")->execute([ $room['id'], $senderId, $msgUUID, $messageType, $encryptedContent, $fileUUID, $replyTo, ]); Security::respond([ 'message_uuid' => $msgUUID, 'room_uuid' => $roomUUID, 'message_type' => $messageType, 'content' => $content, 'sent_at' => date('c'), ], 201, 'Pesan berhasil dikirim.'); } // ── GET /chat/rooms/{uuid}/messages ─────────────────────── public static function getMessages(string $roomUUID): void { $keyData = Security::validateApiKey('chat'); $db = Database::getConnection(); $ownerStmt = $db->prepare("SELECT user_id FROM api_keys WHERE id = ?"); $ownerStmt->execute([$keyData['id']]); $userId = $ownerStmt->fetchColumn(); $room = self::getRoomByUUID($roomUUID); if (!$room) Security::abort(404, 'Room tidak ditemukan.'); // Verifikasi keanggotaan $isMember = $db->prepare("SELECT id FROM chat_room_members WHERE room_id = ? AND user_id = ?"); $isMember->execute([$room['id'], $userId]); if (!$isMember->fetch()) Security::abort(403, 'Anda bukan anggota room ini.'); $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = min(100, (int)($_GET['per_page'] ?? 50)); $offset = ($page - 1) * $perPage; $before = $_GET['before'] ?? null; // cursor-based pagination $params = [$room['id']]; $beforeClause = ''; if ($before) { $beforeClause = " AND cm.sent_at < (SELECT sent_at FROM chat_messages WHERE message_uuid = ?)"; $params[] = $before; } $stmt = $db->prepare(" SELECT cm.message_uuid, cm.message_type, cm.content, cm.file_uuid, cm.reply_to_uuid, cm.is_deleted, cm.is_edited, cm.sent_at, u.username AS sender_username, u.id AS sender_id FROM chat_messages cm JOIN users u ON u.id = cm.sender_id WHERE cm.room_id = ? $beforeClause ORDER BY cm.sent_at DESC LIMIT $perPage OFFSET $offset "); $stmt->execute($params); $messages = $stmt->fetchAll(); // Dekripsi pesan foreach ($messages as &$msg) { if ($msg['is_deleted']) { $msg['content'] = '[Pesan dihapus]'; } elseif ($msg['content']) { $msg['content'] = Security::decryptMessage($msg['content']); } // Tandai sudah dibaca if ($msg['sender_id'] != $userId) { $db->prepare(" INSERT IGNORE INTO chat_message_status (message_id, user_id, status) SELECT id, ?, 'read' FROM chat_messages WHERE message_uuid = ? ")->execute([$userId, $msg['message_uuid']]); } unset($msg['sender_id']); } // Urutkan dari lama ke baru untuk tampilan $messages = array_reverse($messages); Security::respond([ 'room_uuid' => $roomUUID, 'messages' => $messages, 'count' => count($messages), ]); } // ── DELETE /chat/messages/{uuid} ────────────────────────── public static function deleteMessage(string $msgUUID): void { $keyData = Security::validateApiKey('chat'); $db = Database::getConnection(); $ownerStmt = $db->prepare("SELECT user_id FROM api_keys WHERE id = ?"); $ownerStmt->execute([$keyData['id']]); $userId = $ownerStmt->fetchColumn(); $stmt = $db->prepare("SELECT * FROM chat_messages WHERE message_uuid = ? AND sender_id = ?"); $stmt->execute([$msgUUID, $userId]); $msg = $stmt->fetch(); if (!$msg) Security::abort(404, 'Pesan tidak ditemukan atau Anda bukan pengirimnya.'); // Soft delete (seperti WhatsApp "This message was deleted") $db->prepare("UPDATE chat_messages SET is_deleted = 1, content = NULL WHERE id = ?") ->execute([$msg['id']]); Security::respond(['deleted' => true, 'message_uuid' => $msgUUID]); } // ── GET /chat/rooms/{uuid}/members ──────────────────────── public static function getMembers(string $roomUUID): void { $keyData = Security::validateApiKey('chat'); $db = Database::getConnection(); $room = self::getRoomByUUID($roomUUID); if (!$room) Security::abort(404, 'Room tidak ditemukan.'); $stmt = $db->prepare(" SELECT u.username, u.full_name, crm.role, crm.joined_at FROM chat_room_members crm JOIN users u ON u.id = crm.user_id WHERE crm.room_id = ? ORDER BY crm.role DESC, crm.joined_at ASC "); $stmt->execute([$room['id']]); Security::respond($stmt->fetchAll()); } // ─── HELPERS ───────────────────────────────────────────── private static function getRoomByUUID(string $uuid): ?array { $db = Database::getConnection(); $stmt = $db->prepare("SELECT * FROM chat_rooms WHERE room_uuid = ? LIMIT 1"); $stmt->execute([$uuid]); return $stmt->fetch() ?: null; } private static function getBody(): array { return json_decode(file_get_contents('php://input'), true) ?? []; } private static function generateUUID(): string { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } }