identity_id; $contacts = $request->role === 'teacher' ? Database::select( "SELECT DISTINCT ai.id AS user_id, s.uuid AS user_uuid, s.full_name, 'student' AS role, s.grade_level AS specialization FROM course_access_passes cap JOIN courses c ON c.id = cap.course_id JOIN students s ON s.id = cap.student_id JOIN auth_identities ai ON ai.id = s.identity_id AND ai.status = 'active' WHERE c.teacher_id = ? AND cap.is_active = 1", [$request->user_id] ) : Database::select( "SELECT DISTINCT ai.id AS user_id, t.uuid AS user_uuid, t.full_name, 'teacher' AS role, t.specialization FROM course_access_passes cap JOIN courses c ON c.id = cap.course_id JOIN teachers t ON t.id = c.teacher_id JOIN auth_identities ai ON ai.id = t.identity_id AND ai.status = 'active' WHERE cap.student_id = ? AND cap.is_active = 1", [$request->user_id] ); foreach ($contacts as &$contact) { $other = (int)$contact['user_id']; $last = Database::selectOne( "SELECT message, message_type, created_at FROM chat_messages WHERE (sender_identity_id = ? AND receiver_identity_id = ?) OR (sender_identity_id = ? AND receiver_identity_id = ?) ORDER BY id DESC LIMIT 1", [$identityId, $other, $other, $identityId] ); $unread = Database::selectOne( "SELECT COUNT(*) AS count_value FROM chat_messages WHERE sender_identity_id = ? AND receiver_identity_id = ? AND is_read = 0", [$other, $identityId] ); $contact['last_message'] = $last['message'] ?? ''; $contact['last_message_type'] = $last['message_type'] ?? ''; $contact['last_message_at'] = $last['created_at'] ?? null; $contact['unread_count'] = (int)($unread['count_value'] ?? 0); } unset($contact); $response->json(['status' => 'success', 'data' => $contacts]); } public function getMessages(Request $request, Response $response): void { $me = (int)$request->identity_id; $other = (int)($request->getQuery('other_user_id', $request->getQuery('user_id', 0))); if (!$other || !$this->isAllowedContact($request, $other)) { $response->status(403)->json(['status' => 'error', 'message' => 'المحادثة غير مصرح بها']); return; } $limit = min(100, max(1, (int)$request->getQuery('limit', 50))); $messages = Database::select( "SELECT id, uuid, sender_identity_id AS sender_id, receiver_identity_id AS receiver_id, course_id, lesson_id, message, message_type, media_url, is_read, read_at, created_at FROM chat_messages WHERE (sender_identity_id = ? AND receiver_identity_id = ?) OR (sender_identity_id = ? AND receiver_identity_id = ?) ORDER BY created_at ASC LIMIT ?", [$me, $other, $other, $me, $limit] ); foreach ($messages as &$message) $message['is_mine'] = (int)$message['sender_id'] === $me; unset($message); Database::query("UPDATE chat_messages SET is_read = 1, read_at = NOW() WHERE sender_identity_id = ? AND receiver_identity_id = ? AND is_read = 0", [$other, $me]); $response->json(['status' => 'success', 'data' => ['other_user' => $this->identityProfile($other), 'messages' => $messages]]); } public function sendMessage(Request $request, Response $response): void { $body = $request->getBody(); $receiver = (int)($body['receiver_id'] ?? 0); $message = trim((string)($body['message'] ?? '')); $type = (string)($body['message_type'] ?? 'text'); if (!$receiver || $message === '' || !$this->isAllowedContact($request, $receiver)) { $response->status(403)->json(['status' => 'error', 'message' => 'المستلم أو الرسالة غير صالحين لهذه المحادثة']); return; } if (!in_array($type, ['text', 'voice', 'image', 'file'], true)) $type = 'text'; $uuid = $this->uuid(); $id = Database::insert( "INSERT INTO chat_messages (uuid, sender_identity_id, receiver_identity_id, course_id, lesson_id, message, message_type, media_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [$uuid, $request->identity_id, $receiver, !empty($body['course_id']) ? (int)$body['course_id'] : null, !empty($body['lesson_id']) ? (int)$body['lesson_id'] : null, $message, $type, $body['media_url'] ?? null] ); $payload = ['id' => $id, 'uuid' => $uuid, 'sender_id' => $request->identity_id, 'receiver_id' => $receiver, 'message' => $message, 'message_type' => $type, 'media_url' => $body['media_url'] ?? null, 'is_read' => false, 'is_mine' => true, 'created_at' => date('Y-m-d H:i:s')]; self::pushToWorkerman('push_chat', $receiver, $payload); $response->status(201)->json(['status' => 'success', 'data' => $payload]); } public function markAsRead(Request $request, Response $response): void { $sender = (int)($request->getBody()['sender_id'] ?? 0); if (!$sender || !$this->isAllowedContact($request, $sender)) { $response->status(403)->json(['status' => 'error', 'message' => 'المحادثة غير مصرح بها']); return; } Database::query("UPDATE chat_messages SET is_read = 1, read_at = NOW() WHERE sender_identity_id = ? AND receiver_identity_id = ? AND is_read = 0", [$sender, $request->identity_id]); $response->json(['status' => 'success']); } public function getUnreadCount(Request $request, Response $response): void { $row = Database::selectOne("SELECT COUNT(*) AS unread_total FROM chat_messages WHERE receiver_identity_id = ? AND is_read = 0", [$request->identity_id]); $response->json(['status' => 'success', 'data' => ['unread_count' => (int)($row['unread_total'] ?? 0)]]); } private function isAllowedContact(Request $request, int $otherIdentityId): bool { if ($request->role === 'teacher') { return (bool)Database::selectOne( "SELECT 1 FROM course_access_passes cap JOIN courses c ON c.id = cap.course_id JOIN students s ON s.id = cap.student_id WHERE c.teacher_id = ? AND s.identity_id = ? AND cap.is_active = 1 LIMIT 1", [$request->user_id, $otherIdentityId] ); } return (bool)Database::selectOne( "SELECT 1 FROM course_access_passes cap JOIN courses c ON c.id = cap.course_id JOIN teachers t ON t.id = c.teacher_id WHERE cap.student_id = ? AND t.identity_id = ? AND cap.is_active = 1 LIMIT 1", [$request->user_id, $otherIdentityId] ); } private function identityProfile(int $identityId): array { $profile = Database::selectOne("SELECT id, uuid, full_name, 'student' AS role FROM students WHERE identity_id = ? LIMIT 1", [$identityId]); if (!$profile) $profile = Database::selectOne("SELECT id, uuid, full_name, 'teacher' AS role FROM teachers WHERE identity_id = ? LIMIT 1", [$identityId]); return $profile ?: []; } public static function pushToWorkerman(string $action, int $targetIdentityId, array $payload): bool { $ch = curl_init('http://127.0.0.1:4241'); curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query(['action' => $action, 'target_user_id' => $targetIdentityId, 'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE)]), CURLOPT_TIMEOUT_MS => 400, CURLOPT_RETURNTRANSFER => true]); $ok = curl_exec($ch) !== false; curl_close($ch); return $ok; } private function uuid(): 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)); } }