Files
IM-web-new/dist/api/message_detail.php
T
2026-06-30 10:13:48 +07:00

388 lines
13 KiB
PHP

<?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 detailPlaceholders(array $values): string
{
return implode(',', array_fill(0, count($values), '?'));
}
function detailTimestampMs(?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 0;
}
function detailResolveUserId(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 detailConversationType(mixed $value): string
{
$type = strtolower(trim((string)$value));
return in_array($type, ['1', 'group', 'channel'], true) ? 'group' : 'single';
}
function detailDeliveryMap(PDO $db, array $userIds): array
{
$userIds = array_values(array_unique(array_filter(array_map('strval', $userIds))));
if (!$userIds) {
return [];
}
$statement = $db->prepare(
'select `_uid`, max(`_dt`) as `_dt` from `t_delivery_report`'
. ' where `_uid` in (' . detailPlaceholders($userIds) . ') group by `_uid`'
);
$statement->execute($userIds);
$items = [];
foreach ($statement->fetchAll() as $row) {
$items[(string)$row['_uid']] = (int)$row['_dt'];
}
return $items;
}
function detailReadAt(PDO $db, string $readerId, array $targets): int
{
$targets = array_values(array_unique(array_filter(array_map('strval', $targets))));
if ($readerId === '' || !$targets) {
return 0;
}
$statement = $db->prepare(
'select max(`_dt`) from `t_read_report`'
. ' where `_uid` = ? and `_type` = 0 and `_line` = 0'
. ' and `_target` in (' . detailPlaceholders($targets) . ')'
);
$statement->execute(array_merge([$readerId], $targets));
$value = $statement->fetchColumn();
return is_numeric($value) ? (int)$value : 0;
}
function detailSingleMessageVisible(PDO $db, string $userId, string $targetId, string $messageUid): bool
{
$table = imUserMessagesTable($userId);
$conversationKeys = [$userId . '|' . $targetId, $targetId . '|' . $userId];
$statement = $db->prepare(
'select 1 from `' . $table . '`'
. ' where `_uid` = ? and `_mid` = ? and `_type` = 0 and `_line` = 0'
. ' and `_target` in (' . detailPlaceholders($conversationKeys) . ') limit 1'
);
$statement->execute(array_merge([$userId, $messageUid], $conversationKeys));
return (bool)$statement->fetchColumn();
}
function detailUserMap(PDO $imDb, PDO $appDb, array $userIds, string $ownerId, string $groupId = ''): array
{
$userIds = array_values(array_unique(array_filter(array_map('strval', $userIds))));
if (!$userIds) {
return [];
}
$statement = $imDb->prepare(
'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait` from `t_user`'
. ' where `_uid` in (' . detailPlaceholders($userIds) . ')'
);
$statement->execute($userIds);
$users = [];
foreach ($statement->fetchAll() as $row) {
$uid = (string)$row['_uid'];
$rawName = trim((string)($row['_display_name'] ?? ''))
?: trim((string)($row['_name'] ?? ''))
?: trim((string)($row['_mobile'] ?? ''))
?: $uid;
$users[$uid] = [
'uid' => $uid,
'rawName' => $rawName,
'mobile' => (string)($row['_mobile'] ?? ''),
'portrait' => (string)($row['_portrait'] ?? ''),
'contactAlias' => '',
'remarkAlias' => '',
'groupAlias' => '',
'role' => '',
'displayName' => $rawName,
];
}
if ($ownerId !== '') {
$aliasStatement = $imDb->prepare(
'select `_friend_uid`, `_alias` from `t_friend` where `_uid` = ?'
. ' and `_friend_uid` in (' . detailPlaceholders($userIds) . ')'
);
$aliasStatement->execute(array_merge([$ownerId], $userIds));
foreach ($aliasStatement->fetchAll() as $row) {
$uid = (string)$row['_friend_uid'];
if (isset($users[$uid])) {
$users[$uid]['contactAlias'] = trim((string)($row['_alias'] ?? ''));
}
}
try {
$remarkMap = fetchWebUserRemarkMap($appDb, $ownerId, $userIds);
} catch (Throwable) {
$remarkMap = [];
}
foreach ($remarkMap as $uid => $alias) {
if (isset($users[$uid])) {
$users[$uid]['remarkAlias'] = trim((string)$alias);
}
}
}
if ($groupId !== '') {
$memberStatement = $appDb->prepare(
'select `user_id`, `alias`, `role` from `t_web_group_members`'
. ' where `group_id` = ? and `user_id` in (' . detailPlaceholders($userIds) . ')'
);
$memberStatement->execute(array_merge([$groupId], $userIds));
foreach ($memberStatement->fetchAll() as $row) {
$uid = (string)$row['user_id'];
if (isset($users[$uid])) {
$users[$uid]['groupAlias'] = trim((string)($row['alias'] ?? ''));
$users[$uid]['role'] = (string)($row['role'] ?? '');
}
}
}
foreach ($users as $uid => $user) {
$alias = (string)$user['contactAlias'] !== ''
? (string)$user['contactAlias']
: ((string)$user['remarkAlias'] !== '' ? (string)$user['remarkAlias'] : (string)$user['groupAlias']);
$users[$uid]['displayName'] = $alias !== '' ? $alias : (string)$user['rawName'];
}
foreach ($userIds as $uid) {
if (!isset($users[$uid])) {
$users[$uid] = [
'uid' => $uid,
'rawName' => $uid,
'mobile' => '',
'portrait' => '',
'contactAlias' => '',
'remarkAlias' => '',
'groupAlias' => '',
'role' => '',
'displayName' => $uid,
];
}
}
return $users;
}
function detailMember(array $user, int $time = 0): array
{
return [
'uid' => (string)$user['uid'],
'name' => (string)$user['displayName'],
'rawName' => (string)$user['rawName'],
'mobile' => (string)($user['mobile'] ?? ''),
'portrait' => (string)($user['portrait'] ?? ''),
'role' => (string)($user['role'] ?? ''),
'time' => $time,
];
}
function singleDetail(PDO $imDb, string $userId, string $targetId, string $messageUid): array
{
if (!preg_match('/^\d+$/', $messageUid)) {
respond(400, '缺少可查询的消息ID');
}
$table = imMessageTable($messageUid);
$statement = $imDb->prepare(
'select `_mid`, `_from`, `_target`, `_dt`, `_content_type`, `_searchable_key`, `_to`'
. ' from `' . $table . '` where `_mid` = ? limit 1'
);
$statement->execute([$messageUid]);
$message = $statement->fetch();
if (!is_array($message)) {
respond(404, '没有找到该消息');
}
$senderId = (string)$message['_from'];
if (!detailSingleMessageVisible($imDb, $userId, $targetId, $messageUid)) {
respond(403, '没有权限查看该消息');
}
$receiverId = $senderId === $userId ? $targetId : $userId;
if ($senderId !== $userId && $senderId !== $targetId) {
respond(403, '没有权限查看该消息');
}
$sentAt = detailTimestampMs((string)($message['_dt'] ?? ''), $messageUid);
$targets = [$senderId, $senderId . '|' . $receiverId, $receiverId . '|' . $senderId];
$deliveredRaw = detailDeliveryMap($imDb, [$receiverId])[$receiverId] ?? 0;
$readRaw = detailReadAt($imDb, $receiverId, $targets);
$users = detailUserMap($imDb, imAppDb(), [$senderId, $receiverId], $userId);
return [
'messageUid' => $messageUid,
'conversationType' => 'single',
'sentAt' => $sentAt,
'sender' => detailMember($users[$senderId]),
'receiver' => detailMember($users[$receiverId]),
'deliveredAt' => $deliveredRaw >= $sentAt ? $deliveredRaw : 0,
'readAt' => $readRaw >= $sentAt ? $readRaw : 0,
];
}
function groupDetail(PDO $imDb, PDO $appDb, string $userId, string $groupId, string $messageUid): array
{
ensureWebImAppTables();
if (!preg_match('/^\d+$/', $messageUid)) {
respond(400, '缺少可查询的消息ID');
}
$myMember = $appDb->prepare(
'select `role` from `t_web_group_members` where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1'
);
$myMember->execute([$groupId, $userId]);
if (!$myMember->fetchColumn()) {
respond(403, '你不在该群组中');
}
$messageStatement = $appDb->prepare(
'select `message_uid`, `group_id`, `sender_uid`, `created_at_ms`, `edited_at_ms`, `recalled`'
. ' from `t_web_group_messages` where `group_id` = ? and `message_uid` = ? limit 1'
);
$messageStatement->execute([$groupId, $messageUid]);
$message = $messageStatement->fetch();
if (!is_array($message)) {
respond(404, '没有找到该群消息');
}
$sentAt = (int)$message['created_at_ms'];
$editedAt = (int)($message['edited_at_ms'] ?? 0);
$activityAt = max($sentAt, $editedAt);
$senderId = (string)$message['sender_uid'];
$memberStatement = $appDb->prepare(
'select `user_id`, `role`, `alias`, `joined_at_ms` from `t_web_group_members`'
. ' where `group_id` = ? and `status` = 0 and `joined_at_ms` <= ?'
. ' order by (`role` = "owner") desc, (`role` = "admin") desc, `joined_at_ms` asc'
);
$memberStatement->execute([$groupId, $sentAt]);
$members = array_values(array_filter($memberStatement->fetchAll(), static function (array $member) use ($senderId): bool {
return (string)$member['user_id'] !== $senderId;
}));
$memberIds = array_values(array_unique(array_map(static fn(array $member): string => (string)$member['user_id'], $members)));
$users = detailUserMap($imDb, $appDb, array_merge([$senderId], $memberIds), $userId, $groupId);
$deliveryMap = detailDeliveryMap($imDb, $memberIds);
$readMap = [];
if ($memberIds) {
$readStatement = $appDb->prepare(
'select `user_id`, `read_at_ms` from `t_web_group_reads`'
. ' where `group_id` = ? and `user_id` in (' . detailPlaceholders($memberIds) . ')'
);
$readStatement->execute(array_merge([$groupId], $memberIds));
foreach ($readStatement->fetchAll() as $row) {
$readMap[(string)$row['user_id']] = (int)$row['read_at_ms'];
}
}
$read = [];
$unread = [];
$undelivered = [];
foreach ($memberIds as $uid) {
$readAt = (int)($readMap[$uid] ?? 0);
$deliveredAt = (int)($deliveryMap[$uid] ?? 0);
$member = detailMember($users[$uid], $readAt >= $activityAt ? $readAt : ($deliveredAt >= $sentAt ? $deliveredAt : 0));
if ($readAt >= $activityAt) {
$read[] = $member;
} elseif ($deliveredAt >= $sentAt) {
$unread[] = $member;
} else {
$undelivered[] = $member;
}
}
return [
'messageUid' => $messageUid,
'conversationType' => 'group',
'groupId' => $groupId,
'sentAt' => $sentAt,
'editedAt' => $editedAt,
'recalled' => (bool)$message['recalled'],
'sender' => detailMember($users[$senderId] ?? ['uid' => $senderId, 'displayName' => $senderId, 'rawName' => $senderId]),
'group' => [
'read' => $read,
'unread' => $unread,
'undelivered' => $undelivered,
'counts' => [
'read' => count($read),
'unread' => count($unread),
'undelivered' => count($undelivered),
'total' => count($memberIds),
],
],
];
}
$payload = readJsonPayload();
$userId = trim((string)($payload['userId'] ?? ''));
$target = trim((string)($payload['target'] ?? ''));
$messageUid = trim((string)($payload['messageUid'] ?? $payload['messageId'] ?? ''));
$conversationType = detailConversationType($payload['conversationType'] ?? $payload['type'] ?? 'single');
if ($userId === '') {
respond(400, '缺少用户ID');
}
if ($target === '') {
respond(400, '缺少会话对象');
}
if ($messageUid === '') {
respond(400, '缺少消息ID');
}
try {
$imDb = imDb();
$appDb = imAppDb();
$userId = detailResolveUserId($imDb, $userId);
if ($conversationType === 'group') {
respond(0, 'success', ['detail' => groupDetail($imDb, $appDb, $userId, $target, $messageUid)]);
}
$targetId = detailResolveUserId($imDb, $target);
respond(0, 'success', ['detail' => singleDetail($imDb, $userId, $targetId, $messageUid)]);
} catch (Throwable $error) {
respond(500, $error->getMessage() ?: '消息详情加载失败');
}