0) { return ($id >> 21) + 1514736000000; } if ($datetime !== null && $datetime !== '') { $date = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $datetime, new DateTimeZone(IM_DB_TIMEZONE) ); if ($date instanceof DateTimeImmutable) { return $date->getTimestamp() * 1000; } } return (int)floor(microtime(true) * 1000); } function nowMs(): int { return (int)floor(microtime(true) * 1000); } function markUserDelivered(PDO $db, string $userId, int $timestamp): void { if ($userId === '' || $timestamp <= 0) { return; } $statement = $db->prepare( 'insert into `t_delivery_report` (`_uid`, `_dt`) values (?, ?)' . ' on duplicate key update `_dt` = if(`_dt` < values(`_dt`), values(`_dt`), `_dt`)' ); $statement->execute([$userId, $timestamp]); } function markConversationRead(PDO $db, string $userId, string $targetId, int $timestamp): void { if ($userId === '' || $targetId === '' || $timestamp <= 0) { return; } $statement = $db->prepare( 'select `id`, `_dt` from `t_read_report`' . ' where `_uid` = ? and `_type` = 0 and `_line` = 0 and `_target` = ?' . ' order by `_dt` desc limit 1' ); $statement->execute([$userId, $targetId]); $row = $statement->fetch(); if ($row) { if ($timestamp > (int)$row['_dt']) { $update = $db->prepare('update `t_read_report` set `_dt` = ? where `id` = ?'); $update->execute([$timestamp, (int)$row['id']]); } return; } $insert = $db->prepare( 'insert into `t_read_report` (`_uid`, `_type`, `_line`, `_target`, `_dt`) values (?, 0, 0, ?, ?)' ); $insert->execute([$userId, $targetId, $timestamp]); } function resolveUserId(PDO $db, string $value): string { if ($value === '') { return ''; } $statement = $db->prepare( 'select `_uid` from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ? limit 1' ); $statement->execute([$value, $value, $value]); $uid = $statement->fetchColumn(); return is_string($uid) && $uid !== '' ? $uid : $value; } function peerFromConversationTarget(string $userId, string $target): string { $parts = array_values(array_filter(explode('|', $target), static fn($part) => $part !== '')); if (count($parts) < 2) { return $target !== $userId ? $target : ''; } return $parts[0] === $userId ? $parts[1] : $parts[0]; } function fetchMessageRows(PDO $db, array $messageIds): array { $messageIds = array_values(array_unique(array_filter(array_map('strval', $messageIds)))); if (!$messageIds) { return []; } $idsByTable = []; foreach ($messageIds as $messageId) { $idsByTable[imMessageTable($messageId)][] = $messageId; } $messages = []; foreach ($idsByTable as $table => $ids) { $sql = 'select `_mid`, `_from`, `_type`, `_target`, `_line`, `_data`, `_searchable_key`, `_dt`, `_content_type`, `_to`' . ' from `' . $table . '` where `_mid` in (' . placeholders($ids) . ')'; $statement = $db->prepare($sql); $statement->execute($ids); foreach ($statement->fetchAll() as $row) { $messages[(string)$row['_mid']] = $row; } } return $messages; } function fetchMessageEditMap(PDO $db, array $messageIds): array { $messageIds = array_values(array_unique(array_filter(array_map('strval', $messageIds)))); if (!$messageIds) { return []; } try { $statement = $db->prepare( 'select `message_uid`, `edited_at_ms` from `t_web_message_edits`' . ' where `message_uid` in (' . placeholders($messageIds) . ')' ); $statement->execute($messageIds); } catch (Throwable) { return []; } $edits = []; foreach ($statement->fetchAll() as $row) { $edits[(string)$row['message_uid']] = [ 'editedAt' => (int)$row['edited_at_ms'], ]; } return $edits; } function normalizeAttachmentRow(array $row): array { return [ 'type' => (string)$row['attachment_type'], 'url' => (string)$row['url'], 'name' => (string)($row['name'] ?? ''), 'mime' => (string)($row['mime'] ?? ''), 'size' => (int)($row['size_bytes'] ?? 0), ]; } function fetchAttachmentMap(PDO $appDb, array $messageIds): array { $messageIds = array_values(array_unique(array_filter(array_map('strval', $messageIds)))); if (!$messageIds) { return []; } ensureWebImAppTables(); $statement = $appDb->prepare( 'select `message_uid`, `attachment_type`, `url`, `name`, `mime`, `size_bytes`' . ' from `t_web_message_attachments`' . ' where `message_uid` in (' . placeholders($messageIds) . ')' ); $statement->execute($messageIds); $attachments = []; foreach ($statement->fetchAll() as $row) { $attachments[(string)$row['message_uid']] = normalizeAttachmentRow($row); } return $attachments; } function attachmentFallbackText(?array $attachment): string { if (!$attachment) { return ''; } if ((string)$attachment['type'] === 'image') { return '[图片]'; } return '[文件] ' . ((string)($attachment['name'] ?? '') ?: '文件'); } function attachmentDisplayText(string $text, ?array $attachment): string { if (!$attachment) { return $text; } return $text === attachmentFallbackText($attachment) ? '' : $text; } function messagePreviewText(?array $message): string { if (!$message) { return ''; } $attachment = is_array($message['attachment'] ?? null) ? $message['attachment'] : null; $contentType = (int)($message['contentType'] ?? 0); $messageType = (string)($message['type'] ?? ''); $placeholderIsImage = $contentType === 3 || $messageType === 'image'; $text = trim((string)($message['text'] ?? '')); if ($contentType !== 1 && $text === '[暂不支持的消息类型]') { $text = ''; } if ($placeholderIsImage && ($text === '[图片]' || $text === '图片')) { $text = ''; } if ($attachment) { return $text !== '' ? $text : attachmentFallbackText($attachment); } return $text; } function fetchUserInfoMap(PDO $db, array $userIds, string $ownerId = ''): array { $userIds = array_values(array_unique(array_filter(array_map('strval', $userIds)))); if (!$userIds) { return []; } $statement = $db->prepare( 'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait`, `_address` from `t_user`' . ' where `_uid` in (' . placeholders($userIds) . ')' ); $statement->execute($userIds); $users = []; foreach ($statement->fetchAll() as $row) { $row['_is_friend'] = false; $row['_alias'] = ''; $row['_friend_alias'] = ''; $row['_remark_alias'] = ''; $users[(string)$row['_uid']] = $row; } if ($ownerId !== '') { $aliasStatement = $db->prepare( 'select `_friend_uid`, `_alias` from `t_friend` where `_uid` = ?' . ' and `_friend_uid` in (' . placeholders($userIds) . ')' ); $aliasStatement->execute(array_merge([$ownerId], $userIds)); foreach ($aliasStatement->fetchAll() as $row) { $friendUid = (string)$row['_friend_uid']; if (!isset($users[$friendUid])) { continue; } $users[$friendUid]['_is_friend'] = true; $users[$friendUid]['_friend_alias'] = trim((string)($row['_alias'] ?? '')); } try { ensureWebImAppTables(); $remarkMap = fetchWebUserRemarkMap(imAppDb(), $ownerId, $userIds); } catch (Throwable) { $remarkMap = []; } foreach ($remarkMap as $targetUid => $alias) { if (!isset($users[$targetUid])) { continue; } $users[$targetUid]['_remark_alias'] = $alias; } foreach ($users as $uid => $user) { $friendAlias = trim((string)($user['_friend_alias'] ?? '')); $remarkAlias = trim((string)($user['_remark_alias'] ?? '')); $users[$uid]['_alias'] = $friendAlias !== '' ? $friendAlias : $remarkAlias; } } return $users; } function userTitle(array $users, string $userId): string { $user = $users[$userId] ?? null; if (!$user) { return $userId; } return (string)($user['_alias'] ?? '') ?: (string)($user['_display_name'] ?? '') ?: (string)($user['_name'] ?? '') ?: (string)($user['_mobile'] ?? '') ?: $userId; } function loadPeerReceiptState(PDO $db, string $currentUserId, string $peerId): array { $deliveryStatement = $db->prepare('select `_dt` from `t_delivery_report` where `_uid` = ? limit 1'); $deliveryStatement->execute([$peerId]); $deliveredAt = $deliveryStatement->fetchColumn(); $readTargets = array_values(array_unique([ $currentUserId, $currentUserId . '|' . $peerId, $peerId . '|' . $currentUserId, ])); $readStatement = $db->prepare( 'select max(`_dt`) from `t_read_report`' . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' . ' and `_target` in (' . placeholders($readTargets) . ')' ); $readStatement->execute(array_merge([$peerId], $readTargets)); $readAt = $readStatement->fetchColumn(); return [ 'deliveredAt' => is_numeric($deliveredAt) ? (int)$deliveredAt : 0, 'readAt' => is_numeric($readAt) ? (int)$readAt : 0, ]; } function loadOwnReadStateMap(PDO $db, string $userId, array $peerIds): array { $peerIds = array_values(array_unique(array_filter(array_map('strval', $peerIds)))); if (!$peerIds) { return []; } $statement = $db->prepare( 'select `_target`, max(`_dt`) as `_read_at` from `t_read_report`' . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' . ' and `_target` in (' . placeholders($peerIds) . ')' . ' group by `_target`' ); $statement->execute(array_merge([$userId], $peerIds)); $readAtByPeer = []; foreach ($statement->fetchAll() as $row) { $readAtByPeer[(string)$row['_target']] = (int)$row['_read_at']; } return $readAtByPeer; } function outgoingDeliveryStatus(int $timestamp, array $receiptState): string { $readAt = (int)($receiptState['readAt'] ?? 0); if ($readAt > 0 && $timestamp <= $readAt) { return 'read'; } $deliveredAt = (int)($receiptState['deliveredAt'] ?? 0); if ($deliveredAt > 0 && $timestamp <= $deliveredAt) { return 'delivered'; } return 'sent'; } function messageActivityTimestamp(int $timestamp, array $editState = []): int { return max($timestamp, (int)($editState['editedAt'] ?? 0)); } function normalizeConversationType(mixed $value): string { $type = strtolower(trim((string)$value)); if (in_array($type, ['1', 'group', 'channel'], true)) { return 'group'; } return 'single'; } function isWebGroupTarget(PDO $appDb, string $targetId): bool { if ($targetId === '') { return false; } ensureWebImAppTables(); $statement = $appDb->prepare('select 1 from `t_web_groups` where `group_id` = ? and `deleted` = 0 limit 1'); $statement->execute([$targetId]); return (bool)$statement->fetchColumn(); } function nativeGroupRoleFromType(mixed $type): string { $value = (int)$type; if ($value === 2) { return 'owner'; } if ($value === 1) { return 'admin'; } return 'member'; } function isNativeGroupTarget(PDO $db, string $targetId): bool { if ($targetId === '') { return false; } $statement = $db->prepare('select 1 from `t_group` where `_gid` = ? and `_deleted` = 0 limit 1'); $statement->execute([$targetId]); return (bool)$statement->fetchColumn(); } function nativeGroupRole(PDO $db, string $userId, string $groupId): string { $statement = $db->prepare( 'select `_type` from `t_group_member` where `_gid` = ? and `_mid` = ? limit 1' ); $statement->execute([$groupId, $userId]); $type = $statement->fetchColumn(); return $type === false ? '' : nativeGroupRoleFromType($type); } function markWebGroupRead(PDO $appDb, string $userId, string $groupId, int $timestamp): void { if ($userId === '' || $groupId === '' || $timestamp <= 0) { return; } $now = nowMs(); $statement = $appDb->prepare( 'insert into `t_web_group_reads` (`group_id`, `user_id`, `read_at_ms`, `updated_at_ms`)' . ' values (?, ?, ?, ?)' . ' on duplicate key update' . ' `read_at_ms` = if(`read_at_ms` < values(`read_at_ms`), values(`read_at_ms`), `read_at_ms`),' . ' `updated_at_ms` = values(`updated_at_ms`)' ); $statement->execute([$groupId, $userId, $timestamp, $now]); } function markNativeGroupRead(PDO $db, string $userId, string $groupId, int $timestamp): void { if ($userId === '' || $groupId === '' || $timestamp <= 0) { return; } $statement = $db->prepare( 'select `id`, `_dt` from `t_read_report`' . ' where `_uid` = ? and `_type` = 1 and `_line` = 0 and `_target` = ?' . ' order by `_dt` desc limit 1' ); $statement->execute([$userId, $groupId]); $row = $statement->fetch(); if ($row) { if ($timestamp > (int)$row['_dt']) { $update = $db->prepare('update `t_read_report` set `_dt` = ? where `id` = ?'); $update->execute([$timestamp, (int)$row['id']]); } return; } $insert = $db->prepare( 'insert into `t_read_report` (`_uid`, `_type`, `_line`, `_target`, `_dt`) values (?, 1, 0, ?, ?)' ); $insert->execute([$userId, $groupId, $timestamp]); } function webGroupRole(PDO $appDb, string $userId, string $groupId): string { $statement = $appDb->prepare( 'select `role` from `t_web_group_members`' . ' where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1' ); $statement->execute([$groupId, $userId]); $role = $statement->fetchColumn(); return is_string($role) && $role !== '' ? $role : ''; } function loadNativeGroupReadStateMap(PDO $db, string $userId, array $groupIds): array { $groupIds = array_values(array_unique(array_filter(array_map('strval', $groupIds)))); if ($userId === '' || !$groupIds) { return []; } $statement = $db->prepare( 'select `_target`, max(`_dt`) as `_read_at` from `t_read_report`' . ' where `_uid` = ? and `_type` = 1 and `_line` = 0' . ' and `_target` in (' . placeholders($groupIds) . ')' . ' group by `_target`' ); $statement->execute(array_merge([$userId], $groupIds)); $readAtByGroup = []; foreach ($statement->fetchAll() as $row) { $readAtByGroup[(string)$row['_target']] = (int)$row['_read_at']; } return $readAtByGroup; } function fetchNativeGroupMemberMap(PDO $db, string $groupId, array $userIds): array { $userIds = array_values(array_unique(array_filter(array_map('strval', $userIds)))); if ($groupId === '' || !$userIds) { return []; } $statement = $db->prepare( 'select `_mid`, `_alias`, `_type`, `_dt`, `_create_dt` from `t_group_member`' . ' where `_gid` = ? and `_mid` in (' . placeholders($userIds) . ')' ); $statement->execute(array_merge([$groupId], $userIds)); $members = []; foreach ($statement->fetchAll() as $row) { $members[(string)$row['_mid']] = $row; } return $members; } function formatWebGroupMessage( array $row, string $currentUserId, array $group, ?array $attachment = null, array $users = [] ): array { $groupId = (string)$row['group_id']; $from = (string)$row['sender_uid']; $senderUser = $users[$from] ?? []; $senderAlias = trim((string)($row['sender_alias'] ?? '')); $senderContactAlias = trim((string)($senderUser['_alias'] ?? '')); $senderFriendAlias = trim((string)($senderUser['_friend_alias'] ?? '')); $senderRemarkAlias = trim((string)($senderUser['_remark_alias'] ?? '')); $senderRawName = trim((string)($senderUser['_display_name'] ?? '')) ?: trim((string)($senderUser['_name'] ?? '')) ?: trim((string)($senderUser['_mobile'] ?? '')) ?: $from; $senderName = $senderContactAlias ?: $senderAlias ?: $senderRawName; $senderRole = (string)($row['sender_role'] ?? 'member'); if (!in_array($senderRole, ['owner', 'admin'], true)) { $senderRole = 'member'; } $timestamp = (int)$row['created_at_ms']; $editedAt = (int)($row['edited_at_ms'] ?? 0); $activityTimestamp = max($timestamp, $editedAt); $direction = $from === $currentUserId ? 'out' : 'in'; $messageType = $attachment ? (string)$attachment['type'] : 'text'; $contentType = $messageType === 'image' ? 3 : ($messageType === 'file' ? 5 : 1); return [ 'id' => (string)$row['message_uid'], 'messageUid' => (string)$row['message_uid'], 'conversationId' => 'group-' . $groupId, 'from' => $from, 'senderRole' => $senderRole, 'senderName' => $senderName, 'senderPortrait' => (string)($senderUser['_portrait'] ?? ''), 'senderProfile' => [ 'uid' => $from, 'displayName' => $senderName, 'rawName' => $senderRawName, 'contactAlias' => $senderContactAlias, 'friendAlias' => $senderFriendAlias, 'remarkAlias' => $senderRemarkAlias, 'groupAlias' => $senderAlias, 'isContact' => (bool)($senderUser['_is_friend'] ?? false), 'mobile' => (string)($senderUser['_mobile'] ?? ''), 'address' => (string)($senderUser['_address'] ?? ''), 'portrait' => (string)($senderUser['_portrait'] ?? ''), 'role' => $senderRole, ], 'to' => $groupId, 'target' => $groupId, 'direction' => $direction, 'type' => $messageType, 'contentType' => $contentType, 'text' => attachmentDisplayText((string)$row['text'], $attachment), 'attachment' => $attachment, 'status' => $direction === 'out' ? 'sent' : 'sent', 'deliveryStatus' => $direction === 'out' ? 'sent' : '', 'recalled' => false, 'recallStatus' => '', 'editedAt' => $editedAt, 'timestamp' => $timestamp, 'activityTimestamp' => $activityTimestamp, 'displayTimestamp' => $timestamp, 'conversationType' => (string)($group['category'] ?? 'group'), ]; } function cleanupNativeMediaUrl(string $url): string { // WildFire's protobuf payload places the next field tag (0x40, "@") // and a control byte immediately after remoteMediaUrl. Keep this helper // defensive if it receives an unbounded match from older payloads. $url = preg_replace('/[\x00-\x1F\x7F].*$/s', '', $url) ?? $url; return rtrim($url, " \t\r\n\0\x0B\"'<>.,;)]}@"); } function nativeMediaUrlScore(string $url): int { $path = strtolower((string)(parse_url($url, PHP_URL_PATH) ?: $url)); $score = 0; if (preg_match('/\.(?:jpe?g|png|gif|webp|bmp|avif)$/i', $path)) { $score += 30; } if (strpos($path, 'image') !== false || strpos($path, 'media') !== false || strpos($path, 'upload') !== false) { $score += 5; } if (strpos($path, 'thumb') !== false || strpos($path, 'thumbnail') !== false) { $score -= 10; } return $score; } function extractNativeMediaUrlFromData($data): string { if (is_resource($data)) { $data = stream_get_contents($data); } if (!is_string($data) || $data === '') { return ''; } if (!preg_match_all("~https?://[^\\s\"'<>\\x00-\\x1F\\x7F]+~i", $data, $matches)) { return ''; } $urls = []; foreach ($matches[0] as $url) { $url = cleanupNativeMediaUrl((string)$url); if ($url !== '' && filter_var($url, FILTER_VALIDATE_URL)) { $urls[$url] = $url; } } if (!$urls) { return ''; } $urls = array_values($urls); usort($urls, static function (string $left, string $right): int { $scoreDelta = nativeMediaUrlScore($right) <=> nativeMediaUrlScore($left); return $scoreDelta !== 0 ? $scoreDelta : strlen($right) <=> strlen($left); }); return $urls[0] ?? ''; } function nativeAttachmentName(string $url, string $type): string { $path = (string)(parse_url($url, PHP_URL_PATH) ?: ''); $name = trim(rawurldecode((string)basename($path))); if ($name !== '' && $name !== '.' && $name !== '/') { return $name; } return $type === 'image' ? '图片' : '文件'; } function nativeAttachmentMime(string $url, string $type): string { if ($type !== 'image') { return ''; } $path = strtolower((string)(parse_url($url, PHP_URL_PATH) ?: '')); if (preg_match('/\.jpe?g$/', $path)) { return 'image/jpeg'; } if (preg_match('/\.png$/', $path)) { return 'image/png'; } if (preg_match('/\.gif$/', $path)) { return 'image/gif'; } if (preg_match('/\.webp$/', $path)) { return 'image/webp'; } if (preg_match('/\.bmp$/', $path)) { return 'image/bmp'; } if (preg_match('/\.avif$/', $path)) { return 'image/avif'; } return 'image/*'; } function nativeAttachmentFromRow(array $row, ?array $attachment): ?array { if ($attachment !== null) { return $attachment; } $contentType = (int)($row['_content_type'] ?? 0); if ($contentType !== 3 && $contentType !== 5) { return null; } $url = extractNativeMediaUrlFromData($row['_data'] ?? ''); if ($url === '') { return null; } $type = $contentType === 3 ? 'image' : 'file'; return [ 'type' => $type, 'url' => $url, 'name' => nativeAttachmentName($url, $type), 'mime' => nativeAttachmentMime($url, $type), 'size' => 0, ]; } function isNativePlaceholderText(string $text, int $contentType): bool { $text = trim($text); if ($text === '') { return false; } if ($contentType !== 1 && $text === '[暂不支持的消息类型]') { return true; } return $contentType === 3 && ($text === '[图片]' || $text === '图片'); } function nativeDisplayText(string $searchableText, int $contentType, bool $hasAttachment, bool $isRecalled = false): string { $text = trim($searchableText); if (isNativePlaceholderText($text, $contentType)) { return ''; } if ($text === '' && !$isRecalled) { return nativeMessageFallbackText($contentType, $hasAttachment); } return $text; } function nativeMessageFallbackText(int $contentType, bool $hasAttachment): string { if ($hasAttachment) { return ''; } if ($contentType === 5) { return '[文件]'; } return ''; } function nativeMessageTableForDate(DateTimeImmutable $date): string { $localDate = $date->setTimezone(new DateTimeZone(IM_DB_TIMEZONE)); $year = (int)$localDate->format('Y'); $month = (int)$localDate->format('n') - 1; return 't_messages_' . (($year % 3) * 12 + $month); } function recentNativeMessageTables(int $monthsBack = 3): array { $monthsBack = max(1, min($monthsBack, 12)); $cursor = (new DateTimeImmutable('now', new DateTimeZone(IM_DB_TIMEZONE))) ->modify('first day of this month'); $tables = []; for ($index = 0; $index < $monthsBack; $index++) { $tables[] = nativeMessageTableForDate($cursor); $cursor = $cursor->modify('-1 month'); } return array_values(array_unique($tables)); } function loadNativeGroupRowsFromGlobal(PDO $db, array $groupIds, int $limit): array { $groupIds = array_values(array_unique(array_filter(array_map('strval', $groupIds)))); if (!$groupIds) { return []; } $limit = min(max($limit, 50), 1000); $rows = []; foreach (recentNativeMessageTables(3) as $table) { try { $statement = $db->prepare( 'select `_mid`, `_from`, `_type`, `_target`, `_line`, `_data`, `_searchable_key`, `_dt`, `_content_type`, `_to`' . ' from `' . $table . '`' . ' where `_type` = 1 and `_line` = 0' . ' and `_target` in (' . placeholders($groupIds) . ')' . ' order by `_mid` desc limit ' . $limit ); $statement->execute($groupIds); } catch (Throwable) { continue; } foreach ($statement->fetchAll() as $row) { $rows[] = $row; } } usort($rows, static fn($left, $right) => strcmp((string)$right['_mid'], (string)$left['_mid'])); $uniqueRows = []; $seenMessageIds = []; foreach ($rows as $row) { $messageId = (string)$row['_mid']; if ($messageId === '' || isset($seenMessageIds[$messageId])) { continue; } $seenMessageIds[$messageId] = true; $uniqueRows[] = $row; if (count($uniqueRows) >= $limit) { break; } } return $uniqueRows; } function formatNativeGroupMessage( array $row, string $currentUserId, array $group, ?array $attachment = null, array $users = [], array $members = [] ): array { $groupId = (string)$row['_target']; $from = (string)$row['_from']; $senderUser = $users[$from] ?? []; $senderMember = $members[$from] ?? []; $senderAlias = trim((string)($senderMember['_alias'] ?? '')); $senderContactAlias = trim((string)($senderUser['_alias'] ?? '')); $senderFriendAlias = trim((string)($senderUser['_friend_alias'] ?? '')); $senderRemarkAlias = trim((string)($senderUser['_remark_alias'] ?? '')); $senderRawName = trim((string)($senderUser['_display_name'] ?? '')) ?: trim((string)($senderUser['_name'] ?? '')) ?: trim((string)($senderUser['_mobile'] ?? '')) ?: $from; $senderName = $senderContactAlias ?: $senderAlias ?: $senderRawName; $senderRole = nativeGroupRoleFromType($senderMember['_type'] ?? 0); $messageId = (string)$row['_mid']; $timestamp = timestampMs((string)($row['_dt'] ?? ''), $messageId); $contentType = (int)($row['_content_type'] ?? 0); $isRecalled = $contentType === 80; $direction = $from === $currentUserId ? 'out' : 'in'; $attachment = nativeAttachmentFromRow($row, $attachment); $messageType = $attachment ? (string)$attachment['type'] : (($contentType === 1 || $isRecalled) ? 'text' : 'unknown'); $text = nativeDisplayText((string)($row['_searchable_key'] ?? ''), $contentType, $attachment !== null, $isRecalled); return [ 'id' => $messageId, 'messageUid' => $messageId, 'conversationId' => 'group-' . $groupId, 'from' => $from, 'senderRole' => $senderRole, 'senderName' => $senderName, 'senderPortrait' => (string)($senderUser['_portrait'] ?? ''), 'senderProfile' => [ 'uid' => $from, 'displayName' => $senderName, 'rawName' => $senderRawName, 'contactAlias' => $senderContactAlias, 'friendAlias' => $senderFriendAlias, 'remarkAlias' => $senderRemarkAlias, 'groupAlias' => $senderAlias, 'isContact' => (bool)($senderUser['_is_friend'] ?? false), 'mobile' => (string)($senderUser['_mobile'] ?? ''), 'address' => (string)($senderUser['_address'] ?? ''), 'portrait' => (string)($senderUser['_portrait'] ?? ''), 'role' => $senderRole, ], 'to' => $groupId, 'target' => $groupId, 'direction' => $direction, 'type' => $messageType, 'contentType' => $contentType, 'text' => attachmentDisplayText($text, $attachment), 'attachment' => $attachment, 'status' => $isRecalled && $direction === 'out' ? 'recalled' : 'sent', 'deliveryStatus' => $direction === 'out' && !$isRecalled ? 'sent' : '', 'recalled' => $isRecalled, 'recallStatus' => $isRecalled ? '已撤回' : '', 'editedAt' => 0, 'timestamp' => $timestamp, 'activityTimestamp' => $timestamp, 'displayTimestamp' => $timestamp, 'conversationType' => 'group', 'raw' => [ 'native' => true, 'groupName' => (string)($group['_name'] ?? ''), ], ]; } function loadWebGroupMessages(PDO $appDb, string $userId, string $groupId, int $limit, bool $markRead): array { ensureWebImAppTables(); $role = webGroupRole($appDb, $userId, $groupId); if ($role === '') { return []; } $groupStatement = $appDb->prepare('select * from `t_web_groups` where `group_id` = ? and `deleted` = 0 limit 1'); $groupStatement->execute([$groupId]); $group = $groupStatement->fetch(); if (!$group) { return []; } $statement = $appDb->prepare( 'select msg.*,' . ' coalesce(sender_member.`role`, if(g.`owner_uid` = msg.`sender_uid`, "owner", "member")) as `sender_role`,' . ' coalesce(sender_member.`alias`, "") as `sender_alias`' . ' from `t_web_group_messages` msg' . ' join `t_web_groups` g on g.`group_id` = msg.`group_id` and g.`deleted` = 0' . ' left join `t_web_group_members` sender_member' . ' on sender_member.`group_id` = msg.`group_id`' . ' and sender_member.`user_id` = msg.`sender_uid`' . ' and sender_member.`status` = 0' . ' where msg.`group_id` = ? and msg.`recalled` = 0' . ' order by msg.`created_at_ms` desc, msg.`id` desc limit ' . $limit ); $statement->execute([$groupId]); $rows = array_reverse($statement->fetchAll()); $attachmentMap = fetchAttachmentMap($appDb, array_column($rows, 'message_uid')); $users = fetchUserInfoMap(imDb(), array_column($rows, 'sender_uid'), $userId); $readTimestamp = 0; $messages = []; foreach ($rows as $row) { $messageId = (string)$row['message_uid']; $message = formatWebGroupMessage($row, $userId, $group, $attachmentMap[$messageId] ?? null, $users); $readTimestamp = max($readTimestamp, (int)$message['activityTimestamp']); $messages[] = $message; } if ($markRead) { markWebGroupRead($appDb, $userId, $groupId, $readTimestamp); } return $messages; } function loadNativeGroupMessages(PDO $db, string $userId, string $groupId, int $limit, bool $markRead): array { $role = nativeGroupRole($db, $userId, $groupId); if ($role === '') { return []; } $groupStatement = $db->prepare('select * from `t_group` where `_gid` = ? and `_deleted` = 0 limit 1'); $groupStatement->execute([$groupId]); $group = $groupStatement->fetch(); if (!is_array($group)) { return []; } $table = imUserMessagesTable($userId); $scanLimit = min(max($limit * 4, $limit), 400); $statement = $db->prepare( 'select `_mid`, `_dt` from `' . $table . '`' . ' where `_uid` = ? and `_type` = 1 and `_line` = 0 and `_target` = ?' . ' order by `_mid` desc limit ' . $scanLimit ); $statement->execute([$userId, $groupId]); $userRows = $statement->fetchAll(); $messageRows = fetchMessageRows($db, array_column($userRows, '_mid')); $globalRows = loadNativeGroupRowsFromGlobal($db, [$groupId], $scanLimit); foreach ($globalRows as $row) { $messageId = (string)$row['_mid']; if ($messageId !== '') { $messageRows[$messageId] = $row; $userRows[] = [ '_mid' => $messageId, '_dt' => (string)($row['_dt'] ?? ''), ]; } } usort($userRows, static fn($left, $right) => strcmp((string)$right['_mid'], (string)$left['_mid'])); $attachmentMap = fetchAttachmentMap(imAppDb(), array_column($userRows, '_mid')); $messages = []; $seenMessageIds = []; foreach ($userRows as $userRow) { $messageId = (string)$userRow['_mid']; if (isset($seenMessageIds[$messageId]) || !isset($messageRows[$messageId])) { continue; } $seenMessageIds[$messageId] = true; if (!shouldDisplayMessageForUser($messageRows[$messageId], $userId)) { continue; } $messages[] = $messageRows[$messageId]; if (count($messages) >= $limit) { break; } } $messages = array_reverse($messages); $senderIds = array_values(array_unique(array_map(static fn(array $row): string => (string)$row['_from'], $messages))); $users = fetchUserInfoMap($db, $senderIds, $userId); $memberMap = fetchNativeGroupMemberMap($db, $groupId, $senderIds); $readTimestamp = 0; $formatted = []; foreach ($messages as $row) { $messageId = (string)$row['_mid']; $message = formatNativeGroupMessage($row, $userId, $group, $attachmentMap[$messageId] ?? null, $users, $memberMap); $readTimestamp = max($readTimestamp, (int)$message['activityTimestamp']); $formatted[] = $message; } if ($markRead) { markNativeGroupRead($db, $userId, $groupId, $readTimestamp); } return $formatted; } function loadWebGroupConversations(PDO $appDb, string $userId, int $limit): array { ensureWebImAppTables(); $statement = $appDb->prepare( 'select g.*, m.`role`' . ' from `t_web_groups` g' . ' join `t_web_group_members` m on m.`group_id` = g.`group_id` and m.`status` = 0' . ' where g.`deleted` = 0 and m.`user_id` = ?' . ' order by g.`updated_at_ms` desc limit ' . min(max($limit, 50), 100) ); $statement->execute([$userId]); $groups = $statement->fetchAll(); if (!$groups) { return []; } $lastMessageStatement = $appDb->prepare( 'select * from `t_web_group_messages`' . ' where `group_id` = ? and `recalled` = 0' . ' order by greatest(`created_at_ms`, `edited_at_ms`) desc, `id` desc limit 1' ); $readStatement = $appDb->prepare( 'select `read_at_ms` from `t_web_group_reads` where `group_id` = ? and `user_id` = ? limit 1' ); $lastAttachmentStatement = $appDb->prepare( 'select `attachment_type`, `url`, `name`, `mime`, `size_bytes`' . ' from `t_web_message_attachments` where `message_uid` = ? limit 1' ); $unreadStatement = $appDb->prepare( 'select count(*) from `t_web_group_messages`' . ' where `group_id` = ? and `sender_uid` <> ? and `recalled` = 0' . ' and greatest(`created_at_ms`, `edited_at_ms`) > ?' ); $memberCountStatement = $appDb->prepare( 'select count(*) from `t_web_group_members` where `group_id` = ? and `status` = 0' ); $conversations = []; foreach ($groups as $group) { $groupId = (string)$group['group_id']; $lastMessageStatement->execute([$groupId]); $lastMessage = $lastMessageStatement->fetch(); $readStatement->execute([$groupId, $userId]); $readAt = (int)($readStatement->fetchColumn() ?: 0); $unreadStatement->execute([$groupId, $userId, $readAt]); $unread = (int)$unreadStatement->fetchColumn(); $memberCountStatement->execute([$groupId]); $memberCount = (int)$memberCountStatement->fetchColumn(); $title = (string)$group['name']; $category = (string)$group['category']; $isChannel = $category === 'channel'; $lastAttachment = null; if ($lastMessage) { $lastAttachmentStatement->execute([(string)$lastMessage['message_uid']]); $lastAttachmentRow = $lastAttachmentStatement->fetch(); $lastAttachment = $lastAttachmentRow ? normalizeAttachmentRow($lastAttachmentRow) : null; } $subtitle = $lastMessage ? (messagePreviewText([ 'text' => attachmentDisplayText((string)$lastMessage['text'], $lastAttachment), 'attachment' => $lastAttachment, ])) : ($isChannel ? '公告群' : ($memberCount > 0 ? $memberCount . ' 位成员' : '群组')); $updatedAt = $lastMessage ? max((int)$lastMessage['created_at_ms'], (int)($lastMessage['edited_at_ms'] ?? 0)) : (int)$group['updated_at_ms']; $conversations[] = [ 'id' => 'group-' . $groupId, 'type' => $isChannel ? 'channel' : 'group', 'target' => $groupId, 'title' => $title, 'subtitle' => $subtitle, 'unread' => $unread, 'avatarText' => mb_strtoupper(mb_substr($title, 0, 1, 'UTF-8'), 'UTF-8'), 'updatedAt' => $updatedAt, 'role' => (string)$group['role'], 'memberCount' => $memberCount, 'raw' => [ 'type' => 'group', 'target' => $groupId, 'line' => 0, 'category' => $category, 'role' => (string)$group['role'], ], ]; } return $conversations; } function loadNativeGroupConversations(PDO $db, string $userId, int $limit): array { $statement = $db->prepare( 'select g.*, m.`_type` as `member_type`, m.`_alias` as `member_alias`' . ' from `t_group_member` m' . ' join `t_group` g on g.`_gid` = m.`_gid` and g.`_deleted` = 0' . ' where m.`_mid` = ?' . ' order by greatest(g.`_dt`, m.`_dt`) desc limit ' . min(max($limit, 50), 100) ); $statement->execute([$userId]); $groups = $statement->fetchAll(); if (!$groups) { return []; } $groupIds = array_values(array_unique(array_map(static fn(array $group): string => (string)$group['_gid'], $groups))); $table = imUserMessagesTable($userId); $scanLimit = min(max($limit * 20, 200), 1000); $messageStatement = $db->prepare( 'select `_mid`, `_target`, `_dt` from `' . $table . '`' . ' where `_uid` = ? and `_type` = 1 and `_line` = 0' . ' and `_target` in (' . placeholders($groupIds) . ')' . ' order by `_mid` desc limit ' . $scanLimit ); $messageStatement->execute(array_merge([$userId], $groupIds)); $scanRows = $messageStatement->fetchAll(); $messageRows = fetchMessageRows($db, array_column($scanRows, '_mid')); $globalRows = loadNativeGroupRowsFromGlobal($db, $groupIds, $scanLimit); foreach ($globalRows as $row) { $messageId = (string)$row['_mid']; if ($messageId !== '') { $messageRows[$messageId] = $row; $scanRows[] = [ '_mid' => $messageId, '_target' => (string)($row['_target'] ?? ''), '_dt' => (string)($row['_dt'] ?? ''), ]; } } usort($scanRows, static fn($left, $right) => strcmp((string)$right['_mid'], (string)$left['_mid'])); $attachmentMap = fetchAttachmentMap(imAppDb(), array_column($scanRows, '_mid')); $readAtByGroup = loadNativeGroupReadStateMap($db, $userId, $groupIds); $latestByGroup = []; $unreadByGroup = []; $seenMessageIds = []; foreach ($scanRows as $row) { $messageId = (string)$row['_mid']; if (isset($seenMessageIds[$messageId])) { continue; } $seenMessageIds[$messageId] = true; $groupId = (string)$row['_target']; $message = $messageRows[$messageId] ?? null; if ($groupId === '' || !$message || !shouldDisplayMessageForUser($message, $userId)) { continue; } $activityTimestamp = timestampMs((string)($message['_dt'] ?? ''), $messageId); if (!isset($latestByGroup[$groupId]) || $activityTimestamp > (int)($latestByGroup[$groupId]['_activity_at'] ?? 0)) { $row['_activity_at'] = $activityTimestamp; $latestByGroup[$groupId] = $row; } if ((string)$message['_from'] !== $userId && !isRecallMessage($message)) { if ($activityTimestamp > (int)($readAtByGroup[$groupId] ?? 0)) { $unreadByGroup[$groupId] = (int)($unreadByGroup[$groupId] ?? 0) + 1; } } } $groupsById = []; foreach ($groups as $group) { $groupsById[(string)$group['_gid']] = $group; } $senderIds = []; foreach ($latestByGroup as $row) { $message = $messageRows[(string)$row['_mid']] ?? null; if ($message) { $senderIds[] = (string)$message['_from']; } } $users = fetchUserInfoMap($db, $senderIds, $userId); $conversations = []; foreach ($groupsById as $groupId => $group) { $latestRow = $latestByGroup[$groupId] ?? null; $messageId = $latestRow ? (string)$latestRow['_mid'] : ''; $message = $messageId !== '' && isset($messageRows[$messageId]) ? formatNativeGroupMessage( $messageRows[$messageId], $userId, $group, $attachmentMap[$messageId] ?? null, $users, fetchNativeGroupMemberMap($db, $groupId, [(string)$messageRows[$messageId]['_from']]) ) : null; $title = (string)($group['_name'] ?? '') ?: $groupId; $updatedAt = $message ? (int)$message['activityTimestamp'] : (int)($group['_dt'] ?? 0); $conversations[] = [ 'id' => 'group-' . $groupId, 'type' => 'group', 'target' => $groupId, 'title' => $title, 'subtitle' => $message ? (messagePreviewText($message) ?: nativeMessageFallbackText((int)($message['contentType'] ?? 0), false)) : (((int)($group['_member_count'] ?? 0) > 0) ? (int)$group['_member_count'] . ' 位成员' : '群组'), 'unread' => (int)($unreadByGroup[$groupId] ?? 0), 'portrait' => (string)($group['_portrait'] ?? ''), 'avatarText' => mb_strtoupper(mb_substr($title, 0, 1, 'UTF-8'), 'UTF-8'), 'updatedAt' => $updatedAt, 'role' => nativeGroupRoleFromType($group['member_type'] ?? 0), 'memberCount' => (int)($group['_member_count'] ?? 0), 'raw' => [ 'type' => 'group', 'target' => $groupId, 'line' => 0, 'native' => true, 'role' => nativeGroupRoleFromType($group['member_type'] ?? 0), ], ]; } return $conversations; } function mergeConversationsById(array $conversations, array $extraConversations): array { $merged = []; foreach (array_merge($conversations, $extraConversations) as $conversation) { $id = (string)($conversation['id'] ?? ''); if ($id === '') { continue; } if (!isset($merged[$id])) { $merged[$id] = $conversation; continue; } if ((int)($conversation['updatedAt'] ?? 0) > (int)($merged[$id]['updatedAt'] ?? 0)) { $merged[$id] = array_merge($merged[$id], $conversation); } } return array_values($merged); } function isRecallMessage(array $row): bool { return (int)($row['_content_type'] ?? 0) === 80; } function shouldDisplayMessageForUser(array $row, string $currentUserId): bool { if (!isRecallMessage($row)) { return true; } return (string)$row['_from'] === $currentUserId; } function formatMessage( array $row, string $currentUserId, string $peerId, array $receiptState = [], array $editState = [], ?array $attachment = null ): array { $messageId = (string)$row['_mid']; $from = (string)$row['_from']; $contentType = (int)($row['_content_type'] ?? 0); $isRecalled = $contentType === 80; $text = (string)($row['_searchable_key'] ?? ''); $timestamp = timestampMs((string)($row['_dt'] ?? ''), $messageId); $direction = $from === $currentUserId ? 'out' : 'in'; $attachment = nativeAttachmentFromRow($row, $attachment); $messageType = $attachment ? (string)$attachment['type'] : (($contentType === 1 || $isRecalled) ? 'text' : 'unknown'); $messageText = nativeDisplayText($text, $contentType, $attachment !== null, $isRecalled); $messageText = attachmentDisplayText($messageText, $attachment); $editedAt = (int)($editState['editedAt'] ?? 0); $activityTimestamp = messageActivityTimestamp($timestamp, $editState); $deliveryStatus = $direction === 'out' && !$isRecalled ? outgoingDeliveryStatus($activityTimestamp, $receiptState) : ''; return [ 'id' => $messageId, 'messageUid' => $messageId, 'conversationId' => 'single-' . $peerId, 'from' => $from, 'to' => $from === $currentUserId ? $peerId : $currentUserId, 'target' => $peerId, 'direction' => $direction, 'type' => $messageType, 'contentType' => $attachment ? ((string)$attachment['type'] === 'image' ? 3 : 5) : $contentType, 'text' => $messageText, 'attachment' => $attachment, 'status' => $isRecalled && $direction === 'out' ? 'recalled' : ($direction === 'out' ? $deliveryStatus : 'sent'), 'deliveryStatus' => $deliveryStatus, 'recalled' => $isRecalled, 'recallStatus' => $isRecalled ? '已撤回' : '', 'editedAt' => $editedAt, 'timestamp' => $timestamp, 'activityTimestamp' => $activityTimestamp, 'displayTimestamp' => $timestamp, ]; } function loadConversationMessages(PDO $db, string $userId, string $targetId, int $limit, bool $markRead): array { $table = imUserMessagesTable($userId); $conversationKeys = [$userId . '|' . $targetId, $targetId . '|' . $userId]; $scanLimit = min(max($limit * 4, $limit), 400); $statement = $db->prepare( 'select `_mid`, `_dt` from `' . $table . '`' . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' . ' and `_target` in (?, ?)' . ' order by `_mid` desc limit ' . $scanLimit ); $statement->execute([$userId, $conversationKeys[0], $conversationKeys[1]]); $userRows = $statement->fetchAll(); $messageRows = fetchMessageRows($db, array_column($userRows, '_mid')); $editMap = fetchMessageEditMap($db, array_column($userRows, '_mid')); $attachmentMap = fetchAttachmentMap(imAppDb(), array_column($userRows, '_mid')); $receiptState = loadPeerReceiptState($db, $userId, $targetId); $messages = []; $seenMessageIds = []; $readTimestamp = 0; foreach ($userRows as $userRow) { $messageId = (string)$userRow['_mid']; if (isset($seenMessageIds[$messageId]) || !isset($messageRows[$messageId])) { continue; } $seenMessageIds[$messageId] = true; if (!shouldDisplayMessageForUser($messageRows[$messageId], $userId)) { continue; } $message = formatMessage( $messageRows[$messageId], $userId, $targetId, $receiptState, $editMap[$messageId] ?? [], $attachmentMap[$messageId] ?? null ); $messages[] = $message; $readTimestamp = max($readTimestamp, (int)$message['activityTimestamp']); if (count($messages) >= $limit) { break; } } $messages = array_reverse($messages); if ($markRead) { markConversationRead($db, $userId, $targetId, $readTimestamp); } return $messages; } function loadRecentConversations(PDO $db, string $userId, int $limit): array { $table = imUserMessagesTable($userId); $scanLimit = min(max($limit * 20, 200), 1000); $statement = $db->prepare( 'select `_mid`, `_target`, `_dt` from `' . $table . '`' . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' . ' order by `_mid` desc limit ' . $scanLimit ); $statement->execute([$userId]); $scanRows = $statement->fetchAll(); $messageRows = fetchMessageRows($db, array_column($scanRows, '_mid')); $editMap = fetchMessageEditMap($db, array_column($scanRows, '_mid')); $attachmentMap = fetchAttachmentMap(imAppDb(), array_column($scanRows, '_mid')); $latestByPeer = []; foreach ($scanRows as $row) { $messageId = (string)$row['_mid']; $message = $messageRows[$messageId] ?? null; $peerId = peerFromConversationTarget($userId, (string)$row['_target']); if ($peerId === '' || !$message) { continue; } if (!shouldDisplayMessageForUser($message, $userId)) { continue; } $activityTimestamp = messageActivityTimestamp( timestampMs((string)($message['_dt'] ?? ''), $messageId), $editMap[$messageId] ?? [] ); if (!isset($latestByPeer[$peerId]) || $activityTimestamp > (int)($latestByPeer[$peerId]['_activity_at'] ?? 0)) { $row['_activity_at'] = $activityTimestamp; $latestByPeer[$peerId] = $row; } } uasort($latestByPeer, static fn($a, $b) => (int)($b['_activity_at'] ?? 0) <=> (int)($a['_activity_at'] ?? 0)); $latestByPeer = array_slice($latestByPeer, 0, $limit, true); $users = fetchUserInfoMap($db, array_keys($latestByPeer), $userId); $readAtByPeer = loadOwnReadStateMap($db, $userId, array_keys($latestByPeer)); $unreadByPeer = []; $seenUnreadMessageIds = []; foreach ($scanRows as $row) { $messageId = (string)$row['_mid']; if (isset($seenUnreadMessageIds[$messageId])) { continue; } $seenUnreadMessageIds[$messageId] = true; $peerId = peerFromConversationTarget($userId, (string)$row['_target']); $message = $messageRows[$messageId] ?? null; if ($peerId === '' || !$message || !isset($latestByPeer[$peerId])) { continue; } if ((string)$message['_from'] === $userId) { continue; } if (isRecallMessage($message)) { continue; } $timestamp = timestampMs((string)($message['_dt'] ?? ''), $messageId); $activityTimestamp = messageActivityTimestamp($timestamp, $editMap[$messageId] ?? []); if ($activityTimestamp > (int)($readAtByPeer[$peerId] ?? 0)) { $unreadByPeer[$peerId] = (int)($unreadByPeer[$peerId] ?? 0) + 1; } } $conversations = []; foreach ($latestByPeer as $peerId => $userRow) { $messageId = (string)$userRow['_mid']; $message = isset($messageRows[$messageId]) ? formatMessage($messageRows[$messageId], $userId, $peerId, [], $editMap[$messageId] ?? [], $attachmentMap[$messageId] ?? null) : null; $title = userTitle($users, $peerId); $conversations[] = [ 'id' => 'single-' . $peerId, 'type' => 'single', 'target' => $peerId, 'title' => $title, 'subtitle' => $message ? (messagePreviewText($message) !== '' ? messagePreviewText($message) : (empty($message['recalled']) ? '' : '已撤回')) : '', 'unread' => (int)($unreadByPeer[$peerId] ?? 0), 'portrait' => (string)($users[$peerId]['_portrait'] ?? ''), 'avatarText' => mb_strtoupper(mb_substr($title, 0, 1, 'UTF-8'), 'UTF-8'), 'updatedAt' => $message ? $message['activityTimestamp'] : timestampMs((string)($userRow['_dt'] ?? ''), $messageId), 'raw' => ['type' => 'single', 'target' => $peerId, 'line' => 0], ]; } try { $conversations = mergeConversationsById($conversations, loadWebGroupConversations(imAppDb(), $userId, $limit)); } catch (Throwable $error) { error_log('web group conversation sync failed: ' . $error->getMessage()); // Keep single-chat sync available if appdata group metadata is temporarily unavailable. } try { $conversations = mergeConversationsById($conversations, loadNativeGroupConversations($db, $userId, $limit)); } catch (Throwable $error) { error_log('native group conversation sync failed: ' . $error->getMessage()); } usort($conversations, static fn($a, $b) => $b['updatedAt'] <=> $a['updatedAt']); return array_slice($conversations, 0, $limit); } $payload = readJsonPayload(); $userId = trim((string)($payload['userId'] ?? '')); $target = trim((string)($payload['target'] ?? '')); $limit = clampLimit($payload['limit'] ?? 50, 50, 100); $markRead = payloadBool($payload['markRead'] ?? false); $conversationType = normalizeConversationType($payload['conversationType'] ?? $payload['type'] ?? 0); if ($userId === '') { respond(400, '缺少用户ID'); } try { $db = imDb(); $userId = resolveUserId($db, $userId); markUserDelivered($db, $userId, nowMs()); if ($target !== '') { if ($conversationType === 'group') { if (isWebGroupTarget(imAppDb(), $target)) { respond(0, 'success', [ 'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead), ]); } if (isNativeGroupTarget($db, $target)) { respond(0, 'success', [ 'messages' => loadNativeGroupMessages($db, $userId, $target, $limit, $markRead), ]); } respond(0, 'success', [ 'messages' => [], ]); } if (isWebGroupTarget(imAppDb(), $target)) { respond(0, 'success', [ 'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead), ]); } $targetId = resolveUserId($db, $target); respond(0, 'success', [ 'messages' => loadConversationMessages($db, $userId, $targetId, $limit, $markRead), ]); } respond(0, 'success', [ 'conversations' => loadRecentConversations($db, $userId, $limit), ]); } catch (Throwable $error) { respond(500, $error->getMessage() ?: '消息同步失败'); }