Files
IM-web-new/dist/api/message_action.php
2026-10-09 22:49:53 +07:00

289 lines
9.3 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';
class UserActionException extends RuntimeException
{
}
function resolveActionUserId(string $value): string
{
if ($value === '') {
return '';
}
$db = imDb();
$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 messageTimestampMs(string $messageUid): int
{
return (((int)$messageUid) >> 21) + 1514736000000;
}
function nowTimestampMs(): int
{
return (int)floor(microtime(true) * 1000);
}
function recallLimitMinutes(): float
{
$statement = imAppDb()->prepare(
'select `conf_value` from `system_conf`'
. ' where `conf_name` = ? and `is_valid` = 1 limit 1'
);
$statement->execute(['RetractMessage']);
$value = trim((string)($statement->fetchColumn() ?: '0'));
if ($value === '' || !is_numeric($value)) {
return 0.0;
}
return max(0.0, (float)$value);
}
function assertRecallWithinLimit(string $messageUid): void
{
$limitMinutes = recallLimitMinutes();
if ($limitMinutes <= 0) {
return;
}
$messageAgeMs = nowTimestampMs() - messageTimestampMs($messageUid);
$limitMs = (int)round($limitMinutes * 60 * 1000);
if ($messageAgeMs > $limitMs) {
throw new UserActionException('已超过可撤回时间');
}
}
function fetchActionMessageRow(string $messageUid): array
{
$table = imMessageTable($messageUid);
$statement = imDb()->prepare(
'select `_mid`, `_from`, `_content_type`, `_searchable_key` from `' . $table . '` where `_mid` = ? limit 1'
);
$statement->execute([$messageUid]);
$row = $statement->fetch();
return is_array($row) ? $row : [];
}
function ensureMessageEditsTable(): void
{
imDb()->exec(
'create table if not exists `t_web_message_edits` ('
. '`message_uid` varchar(32) not null,'
. '`editor_uid` varchar(64) not null,'
. '`edited_text` text null,'
. '`edited_at_ms` bigint not null,'
. '`created_at` datetime not null default current_timestamp,'
. '`updated_at` datetime null default current_timestamp on update current_timestamp,'
. 'primary key (`message_uid`),'
. 'key `idx_editor_uid` (`editor_uid`),'
. 'key `idx_edited_at_ms` (`edited_at_ms`)'
. ') engine=InnoDB default charset=utf8mb4'
);
}
function editTextMessage(string $operatorId, string $messageUid, string $text): array
{
$row = fetchActionMessageRow($messageUid);
if (!$row) {
return editWebGroupMessage($operatorId, $messageUid, $text);
}
if ((string)$row['_from'] !== $operatorId) {
throw new UserActionException('只能编辑自己发送的消息');
}
if ((int)$row['_content_type'] !== 1) {
throw new UserActionException('只能编辑文字消息');
}
$editedAt = nowTimestampMs();
$table = imMessageTable($messageUid);
$update = imDb()->prepare('update `' . $table . '` set `_searchable_key` = ? where `_mid` = ?');
$update->execute([$text, $messageUid]);
ensureMessageEditsTable();
$upsert = imDb()->prepare(
'insert into `t_web_message_edits` (`message_uid`, `editor_uid`, `edited_text`, `edited_at_ms`)'
. ' values (?, ?, ?, ?)'
. ' on duplicate key update'
. ' `editor_uid` = values(`editor_uid`),'
. ' `edited_text` = values(`edited_text`),'
. ' `edited_at_ms` = values(`edited_at_ms`),'
. ' `updated_at` = current_timestamp'
);
$upsert->execute([$messageUid, $operatorId, $text, $editedAt]);
return [
'messageUid' => $messageUid,
'text' => $text,
'editedAt' => $editedAt,
];
}
function fetchWebGroupActionMessage(string $messageUid): array
{
ensureWebImAppTables();
$statement = imAppDb()->prepare(
'select m.*, g.`category` from `t_web_group_messages` m'
. ' join `t_web_groups` g on g.`group_id` = m.`group_id` and g.`deleted` = 0'
. ' where m.`message_uid` = ? limit 1'
);
$statement->execute([$messageUid]);
$row = $statement->fetch();
return is_array($row) ? $row : [];
}
function editWebGroupMessage(string $operatorId, string $messageUid, string $text): array
{
$row = fetchWebGroupActionMessage($messageUid);
if (!$row) {
throw new UserActionException('消息不存在');
}
if ((string)$row['sender_uid'] !== $operatorId) {
throw new UserActionException('只能编辑自己发送的消息');
}
if ((int)($row['recalled'] ?? 0) !== 0) {
throw new UserActionException('已撤回的消息请重新编辑发送');
}
$editedAt = nowTimestampMs();
$update = imAppDb()->prepare(
'update `t_web_group_messages` set `text` = ?, `edited_at_ms` = ? where `message_uid` = ?'
);
$update->execute([$text, $editedAt, $messageUid]);
$groupUpdate = imAppDb()->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?');
$groupUpdate->execute([$editedAt, (string)$row['group_id']]);
return [
'messageUid' => $messageUid,
'text' => $text,
'editedAt' => $editedAt,
];
}
function webGroupMemberRoleForAction(string $groupId, string $userId): string
{
$statement = imAppDb()->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 : '';
}
function recallWebGroupMessage(string $operatorId, string $messageUid): bool
{
$row = fetchWebGroupActionMessage($messageUid);
if (!$row) {
return false;
}
$isSender = (string)$row['sender_uid'] === $operatorId;
if (!$isSender) {
$role = webGroupMemberRoleForAction((string)$row['group_id'], $operatorId);
if (!in_array($role, ['owner', 'admin'], true)) {
throw new UserActionException('只有群主或管理员可以撤回他人消息');
}
}
assertRecallWithinLimit($messageUid);
$now = nowTimestampMs();
$update = imAppDb()->prepare(
'update `t_web_group_messages` set `recalled` = 1 where `message_uid` = ?'
);
$update->execute([$messageUid]);
$groupUpdate = imAppDb()->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?');
$groupUpdate->execute([$now, (string)$row['group_id']]);
return true;
}
$payload = readJsonPayload();
$action = trim((string)($payload['action'] ?? ''));
$userId = trim((string)($payload['userId'] ?? ''));
$messageUidText = trim((string)($payload['messageUid'] ?? ''));
$messageUid = ctype_digit($messageUidText) ? (int)$messageUidText : 0;
if ($action === '' || $messageUid <= 0) {
respond(400, '缺少操作类型或消息ID');
}
try {
if ($action === 'recall') {
if ($userId === '') {
respond(400, '缺少操作者ID');
}
$userId = resolveActionUserId($userId);
$row = fetchActionMessageRow($messageUidText);
if (!$row && recallWebGroupMessage($userId, $messageUidText)) {
respond(0, 'success', [
'messageUid' => (string)$messageUid,
'action' => $action,
]);
}
if (!$row) {
throw new UserActionException('消息不存在');
}
assertRecallWithinLimit($messageUidText);
requireImSuccess(imAdminPost('/admin/message/recall', [
'operator' => $userId,
'messageUid' => $messageUid,
'userRecall' => true,
'isUserRecall' => true,
]), '撤回失败');
respond(0, 'success', [
'messageUid' => (string)$messageUid,
'action' => $action,
]);
}
if ($action === 'edit') {
if ($userId === '') {
respond(400, '缺少操作者ID');
}
$text = trim((string)($payload['text'] ?? ''));
if ($text === '') {
respond(400, '编辑内容不能为空');
}
if (mb_strlen($text, 'UTF-8') > 5000) {
respond(400, '编辑内容过长');
}
$userId = resolveActionUserId($userId);
respond(0, 'success', [
'message' => editTextMessage($userId, $messageUidText, $text),
'action' => $action,
]);
}
if ($action === 'delete') {
requireImSuccess(imAdminPost('/admin/message/delete', [
'messageUid' => $messageUid,
]), '删除失败');
respond(0, 'success', [
'messageUid' => (string)$messageUid,
'action' => $action,
]);
}
respond(400, '不支持的消息操作');
} catch (Throwable $error) {
respond($error instanceof UserActionException ? 400 : 500, $error->getMessage() ?: '消息操作失败');
}