Import JC Web IM
This commit is contained in:
@@ -0,0 +1,900 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
require __DIR__ . '/im_web_bootstrap.php';
|
||||
|
||||
function clampLimit(mixed $value, int $default, int $max): int
|
||||
{
|
||||
$limit = (int)$value;
|
||||
if ($limit <= 0) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return min($limit, $max);
|
||||
}
|
||||
|
||||
function placeholders(array $values): string
|
||||
{
|
||||
return implode(',', array_fill(0, count($values), '?'));
|
||||
}
|
||||
|
||||
function payloadBool(mixed $value): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
function timestampMs(?string $datetime, string $messageId = '0'): int
|
||||
{
|
||||
$id = (int)$messageId;
|
||||
if ($id > 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`, `_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;
|
||||
if ($attachment) {
|
||||
$text = trim((string)($message['text'] ?? ''));
|
||||
return $text !== '' ? $text : attachmentFallbackText($attachment);
|
||||
}
|
||||
|
||||
return (string)($message['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 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 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 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 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 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,
|
||||
]) ?: (string)$lastMessage['text'])
|
||||
: ($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 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';
|
||||
$messageType = $attachment ? (string)$attachment['type'] : (($contentType === 1 || $isRecalled) ? 'text' : 'unknown');
|
||||
$messageText = $text !== '' ? $text : ($isRecalled || $attachment ? '' : '[暂不支持的消息类型]');
|
||||
$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 = array_merge($conversations, loadWebGroupConversations(imAppDb(), $userId, $limit));
|
||||
} catch (Throwable) {
|
||||
// Keep single-chat sync available if appdata group metadata is temporarily unavailable.
|
||||
}
|
||||
|
||||
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 !== '') {
|
||||
$targetId = resolveUserId($db, $target);
|
||||
if ($conversationType === 'group' || isWebGroupTarget(imAppDb(), $target)) {
|
||||
respond(0, 'success', [
|
||||
'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead),
|
||||
]);
|
||||
}
|
||||
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() ?: '消息同步失败');
|
||||
}
|
||||
Reference in New Issue
Block a user